Seregon/zftpd

Zero-copy FTP/HTTP Daemon compatible with all POSIX systems

C/11.0 KB/No license
src/pal_filesystem.c
zftpd / src / pal_filesystem.c
1/*
2MIT License
3 
4Copyright (c) 2026 Seregon
5 
6Permission is hereby granted, free of charge, to any person obtaining a copy
7of this software and associated documentation files (the "Software"), to deal
8in the Software without restriction, including without limitation the rights
9to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10copies of the Software, and to permit persons to whom the Software is
11furnished to do so, subject to the following conditions:
12 
13The above copyright notice and this permission notice shall be included in all
14copies or substantial portions of the Software.
15 
16THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22SOFTWARE.
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 */
49extern 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)
57int psx_vfs_try_open_self(vfs_node_t *node, const char *path);
58ftp_error_t psx_vfs_stat(const char *path, vfs_stat_t *out);
59ssize_t psx_vfs_read(vfs_node_t *node, void *buffer, size_t length);
60#endif
61 
62ftp_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 
87ftp_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 
206void 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 
229void 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 
248ssize_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