Zero-copy FTP/HTTP Daemon compatible with all POSIX systems
| 1 | /* |
| 2 | MIT License |
| 3 | |
| 4 | Copyright (c) 2026 Seregon |
| 5 | |
| 6 | Permission is hereby granted, free of charge, to any person obtaining a copy |
| 7 | of this software and associated documentation files (the "Software"), to deal |
| 8 | in the Software without restriction, including without limitation the rights |
| 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| 10 | copies of the Software, and to permit persons to whom the Software is |
| 11 | furnished to do so, subject to the following conditions: |
| 12 | |
| 13 | The above copyright notice and this permission notice shall be included in all |
| 14 | copies or substantial portions of the Software. |
| 15 | |
| 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
| 22 | SOFTWARE. |
| 23 | */ |
| 24 | /** |
| 25 | * @file pal_filesystem.h |
| 26 | * @brief Unified filesystem abstraction (PS4/PS5) |
| 27 | * |
| 28 | * @author Seregon |
| 29 | * @version 1.0.0 |
| 30 | * |
| 31 | * PLATFORMS: FreeBSD (PS4/PS5 kqueue), Linux (epoll) |
| 32 | * DESIGN: Single-threaded, non-blocking I/O |
| 33 | * |
| 34 | */ |
| 35 | #include "pal_filesystem.h" |
| 36 | |
| 37 | #include "pal_fileio.h" |
| 38 | #include <errno.h> |
| 39 | #include <fcntl.h> |
| 40 | #include <stdio.h> |
| 41 | #include <string.h> |
| 42 | #include <sys/stat.h> |
| 43 | #include <sys/types.h> |
| 44 | |
| 45 | #if defined(PLATFORM_PS4) || defined(PLATFORM_PS5) |
| 46 | #include <sys/mount.h> /* fstatfs, struct statfs */ |
| 47 | #if defined(PLATFORM_PS4) || defined(PLATFORM_PS5) |
| 48 | /* PS4/PS5 libkernel exports _fstatfs, not fstatfs */ |
| 49 | extern int _fstatfs(int, struct statfs *); |
| 50 | #define pal_fstatfs _fstatfs |
| 51 | #else |
| 52 | #define pal_fstatfs fstatfs |
| 53 | #endif |
| 54 | #endif |
| 55 | |
| 56 | #if defined(PLATFORM_PS4) || defined(PLATFORM_PS5) |
| 57 | int psx_vfs_try_open_self(vfs_node_t *node, const char *path); |
| 58 | ftp_error_t psx_vfs_stat(const char *path, vfs_stat_t *out); |
| 59 | ssize_t psx_vfs_read(vfs_node_t *node, void *buffer, size_t length); |
| 60 | #endif |
| 61 | |
| 62 | ftp_error_t vfs_stat(const char *path, vfs_stat_t *out) |
| 63 | { |
| 64 | if ((path == NULL) || (out == NULL)) { |
| 65 | return FTP_ERR_INVALID_PARAM; |
| 66 | } |
| 67 | |
| 68 | #if defined(PLATFORM_PS4) || defined(PLATFORM_PS5) |
| 69 | ftp_error_t psx_err = psx_vfs_stat(path, out); |
| 70 | if (psx_err != FTP_ERR_UNKNOWN) { |
| 71 | return psx_err; |
| 72 | } |
| 73 | #endif |
| 74 | |
| 75 | struct stat st; |
| 76 | ftp_error_t err = pal_file_stat(path, &st); |
| 77 | if (err != FTP_OK) { |
| 78 | return err; |
| 79 | } |
| 80 | |
| 81 | out->mode = (uint32_t)st.st_mode; |
| 82 | out->size = (uint64_t)st.st_size; |
| 83 | out->mtime = (int64_t)st.st_mtime; |
| 84 | return FTP_OK; |
| 85 | } |
| 86 | |
| 87 | ftp_error_t vfs_open(vfs_node_t *node, const char *path) |
| 88 | { |
| 89 | if ((node == NULL) || (path == NULL)) { |
| 90 | return FTP_ERR_INVALID_PARAM; |
| 91 | } |
| 92 | |
| 93 | memset(node, 0, sizeof(*node)); |
| 94 | node->fd = -1; |
| 95 | |
| 96 | /* |
| 97 | * PLATFORM PS4/PS5 — raw file transfer, NOT MAP_SELF |
| 98 | * |
| 99 | * Previous code tried psx_vfs_try_open_self() first. If the file was a |
| 100 | * SELF (encrypted PS4/PS5 executable), the VFS node was set up with: |
| 101 | * - node->size = elf_size (logical ELF bytes, much smaller than disk) |
| 102 | * - node->caps = VFS_CAP_STREAM_ONLY (reads via MAP_SELF = decryption) |
| 103 | * |
| 104 | * This caused two combined defects for FTP downloads: |
| 105 | * |
| 106 | * 1. SIZE / MLSD reported elf_size (e.g. 419 MB) instead of st_size |
| 107 | * (e.g. 12 GB). FTP clients closed the connection after 419 MB, |
| 108 | * believing the transfer was complete. |
| 109 | * |
| 110 | * 2. The bytes actually sent were the DECRYPTED ELF payload — not the |
| 111 | * raw on-disk SELF container. The received file is unusable for |
| 112 | * backup, copying, or re-installation. |
| 113 | * |
| 114 | * zftpd is a file-transfer daemon. Its job is to move bits from disk to |
| 115 | * the client exactly as they exist on storage. It must never silently |
| 116 | * decrypt or transform file content. |
| 117 | * |
| 118 | * psx_vfs_try_open_self is intentionally NOT called here. It remains |
| 119 | * available for platform-internal use (e.g. module loading) but must not |
| 120 | * be on the data-transfer path. |
| 121 | * |
| 122 | * @note The sendfile-safety check below (VFS_CAP_SENDFILE) still applies: |
| 123 | * exFAT / nullfs / pfsmnt vnodes KP with sendfile(2) on PS5. |
| 124 | */ |
| 125 | |
| 126 | int fd = pal_file_open(path, O_RDONLY, 0); |
| 127 | if (fd < 0) { |
| 128 | return FTP_ERR_FILE_OPEN; |
| 129 | } |
| 130 | |
| 131 | struct stat st; |
| 132 | if (pal_file_fstat(fd, &st) != FTP_OK) { |
| 133 | pal_file_close(fd); |
| 134 | return FTP_ERR_FILE_STAT; |
| 135 | } |
| 136 | |
| 137 | node->fd = fd; |
| 138 | node->private_ctx = NULL; |
| 139 | node->size = (uint64_t)st.st_size; |
| 140 | node->offset = 0U; |
| 141 | |
| 142 | /* |
| 143 | * SENDFILE SAFETY CHECK (PS4/PS5) |
| 144 | * |
| 145 | * DESIGN RATIONALE: |
| 146 | * FreeBSD's sendfile(2) uses the kernel VM pager to DMA file pages |
| 147 | * directly into the socket buffer (zero userspace copy). This requires |
| 148 | * the source vnode's pager to implement the vm_pager_ops interface. |
| 149 | * |
| 150 | * On PS5's modified FreeBSD kernel, the exFAT (exfatfs) and FAT32 |
| 151 | * (msdosfs) drivers used for USB storage do NOT implement this |
| 152 | * interface correctly. Calling sendfile() on a vnode backed by these |
| 153 | * filesystems dereferences a null/invalid pager function pointer, |
| 154 | * causing an immediate kernel panic (KP). |
| 155 | * |
| 156 | * nullfs mirrors the underlying vnode — if the origin is exFAT, the |
| 157 | * nullfs vnode inherits the same broken pager ops. |
| 158 | * |
| 159 | * Detection: fstatfs() on the open fd returns the filesystem type |
| 160 | * name without any additional syscall cost. For USB-backed filesystems |
| 161 | * we clear VFS_CAP_SENDFILE, forcing the buffered read/write path. |
| 162 | * |
| 163 | * @see pal_sendfile() in pal_fileio.c — callers must check this cap |
| 164 | * @see https://github.com/seregonwar/zftpd — KP report from USB download |
| 165 | */ |
| 166 | #if defined(PLATFORM_PS4) || defined(PLATFORM_PS5) |
| 167 | { |
| 168 | struct statfs sfs; |
| 169 | int sendfile_safe = 1; /* assume safe until proven otherwise */ |
| 170 | |
| 171 | if (pal_fstatfs(fd, &sfs) == 0) { |
| 172 | /* |
| 173 | * Filesystems known to cause KP with sendfile() on PS4/PS5: |
| 174 | * - exfatfs : USB drives formatted as exFAT (most common) |
| 175 | * - msdosfs : USB drives formatted as FAT32 |
| 176 | * - nullfs : bind mount — inherits origin pager; unsafe if |
| 177 | * origin is exFAT/msdosfs (/mnt/usb* game mounts) |
| 178 | * - pfsmnt : PlayStation FS mount (/user/av_contents, etc.) |
| 179 | * sendfile() on pfsmnt vnodes sends corrupt data |
| 180 | * - pfs : raw PFS — same broken pager ops as pfsmnt |
| 181 | * |
| 182 | * Add new entries here if additional filesystems are identified. |
| 183 | */ |
| 184 | if ((strcmp(sfs.f_fstypename, "exfatfs") == 0) || |
| 185 | (strcmp(sfs.f_fstypename, "msdosfs") == 0) || |
| 186 | (strcmp(sfs.f_fstypename, "nullfs") == 0) || |
| 187 | (strcmp(sfs.f_fstypename, "pfsmnt") == 0) || |
| 188 | (strcmp(sfs.f_fstypename, "pfs") == 0)) { |
| 189 | sendfile_safe = 0; |
| 190 | } |
| 191 | } |
| 192 | /* fstatfs failure: assume unsafe — tolerate the performance hit */ |
| 193 | else { |
| 194 | sendfile_safe = 0; |
| 195 | } |
| 196 | |
| 197 | node->caps = sendfile_safe ? VFS_CAP_SENDFILE : 0U; |
| 198 | } |
| 199 | #else |
| 200 | node->caps = VFS_CAP_SENDFILE; |
| 201 | #endif |
| 202 | |
| 203 | return FTP_OK; |
| 204 | } |
| 205 | |
| 206 | void vfs_close(vfs_node_t *node) |
| 207 | { |
| 208 | if (node == NULL) { |
| 209 | return; |
| 210 | } |
| 211 | |
| 212 | #if defined(PLATFORM_PS4) || defined(PLATFORM_PS5) |
| 213 | if ((node->caps & VFS_CAP_STREAM_ONLY) != 0U) { |
| 214 | if (node->psx.self_fd >= 0) { |
| 215 | pal_file_close(node->psx.self_fd); |
| 216 | } |
| 217 | node->psx.self_fd = -1; |
| 218 | node->private_ctx = NULL; |
| 219 | return; |
| 220 | } |
| 221 | #endif |
| 222 | |
| 223 | if (node->fd >= 0) { |
| 224 | pal_file_close(node->fd); |
| 225 | node->fd = -1; |
| 226 | } |
| 227 | } |
| 228 | |
| 229 | void vfs_set_offset(vfs_node_t *node, uint64_t offset) |
| 230 | { |
| 231 | if (node == NULL) { |
| 232 | return; |
| 233 | } |
| 234 | |
| 235 | node->offset = offset; |
| 236 | |
| 237 | #if defined(PLATFORM_PS4) || defined(PLATFORM_PS5) |
| 238 | if ((node->caps & VFS_CAP_STREAM_ONLY) != 0U) { |
| 239 | return; |
| 240 | } |
| 241 | #endif |
| 242 | |
| 243 | if (node->fd >= 0) { |
| 244 | (void)pal_file_seek(node->fd, (off_t)offset, SEEK_SET); |
| 245 | } |
| 246 | } |
| 247 | |
| 248 | ssize_t vfs_read(vfs_node_t *node, void *buffer, size_t length) |
| 249 | { |
| 250 | if ((node == NULL) || (buffer == NULL) || (length == 0U)) { |
| 251 | errno = EINVAL; |
| 252 | return -1; |
| 253 | } |
| 254 | |
| 255 | #if defined(PLATFORM_PS4) || defined(PLATFORM_PS5) |
| 256 | if ((node->caps & VFS_CAP_STREAM_ONLY) != 0U) { |
| 257 | return psx_vfs_read(node, buffer, length); |
| 258 | } |
| 259 | #endif |
| 260 | |
| 261 | if (node->fd < 0) { |
| 262 | errno = EBADF; |
| 263 | return -1; |
| 264 | } |
| 265 | |
| 266 | ssize_t n = pal_file_read(node->fd, buffer, length); |
| 267 | if (n > 0) { |
| 268 | node->offset += (uint64_t)n; |
| 269 | } |
| 270 | return n; |
| 271 | } |
| 272 |