Seregon/zftpd

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

C/11.0 KB/No license
src/pal_filesystem_psx.c
zftpd / src / pal_filesystem_psx.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#if defined(PLATFORM_PS4) || defined(PLATFORM_PS5)
38 
39#include "ftp_types.h"
40#include "pal_fileio.h"
41#include <elf.h>
42#include <errno.h>
43#include <fcntl.h>
44#include <pthread.h>
45#include <stdint.h>
46#include <stdatomic.h>
47#include <string.h>
48#include <sys/mman.h>
49#include <sys/stat.h>
50#include <sys/types.h>
51#include <unistd.h>
52 
53#ifndef MAP_SELF
54#define MAP_SELF 0x80000
55#endif
56 
57ftp_error_t psx_vfs_stat(const char *path, vfs_stat_t *out);
58int psx_vfs_try_open_self(vfs_node_t *node, const char *path);
59ssize_t psx_vfs_read(vfs_node_t *node, void *buffer, size_t length);
60 
61#if defined(PLATFORM_PS5) && defined(__has_include)
62#if __has_include(<ps5/kernel.h>)
63#include <ps5/kernel.h>
64#define PS5_HAVE_KERNEL 1
65#endif
66#endif
67 
68#if defined(PLATFORM_PS5) && defined(PS5_HAVE_KERNEL)
69#define PS5_SUPERPAGE_SIZE 0x200000U
70 
71static atomic_int g_ps5_pager_resolved = ATOMIC_VAR_INIT(0);
72static intptr_t g_ps5_pager_table = 0;
73static intptr_t g_ps5_pager_ops_vnode = 0;
74static intptr_t g_ps5_pager_ops_self = 0;
75 
76static void ps5_resolve_pager_addresses(void)
77{
78 int expected = 0;
79 if (!atomic_compare_exchange_strong(&g_ps5_pager_resolved, &expected, 1)) {
80 return;
81 }
82 
83 uint32_t fw = kernel_get_fw_version();
84 switch (fw >> 16) {
85 case 0x100:
86 case 0x101:
87 case 0x102:
88 case 0x105:
89 case 0x110:
90 case 0x111:
91 case 0x112:
92 g_ps5_pager_table = (intptr_t)KERNEL_ADDRESS_DATA_BASE + 0xC27C40;
93 break;
94 case 0x113:
95 case 0x114:
96 g_ps5_pager_table = (intptr_t)KERNEL_ADDRESS_DATA_BASE + 0xC27CA0;
97 break;
98 case 0x200:
99 g_ps5_pager_table = (intptr_t)KERNEL_ADDRESS_DATA_BASE + 0xC4EF60;
100 break;
101 case 0x220:
102 case 0x225:
103 case 0x226:
104 g_ps5_pager_table = (intptr_t)KERNEL_ADDRESS_DATA_BASE + 0xC4EFA0;
105 break;
106 case 0x230:
107 case 0x250:
108 case 0x270:
109 g_ps5_pager_table = (intptr_t)KERNEL_ADDRESS_DATA_BASE + 0xC4F120;
110 break;
111 case 0x300:
112 case 0x310:
113 case 0x320:
114 case 0x321:
115 g_ps5_pager_table = (intptr_t)KERNEL_ADDRESS_DATA_BASE + 0xCAF8C0;
116 break;
117 case 0x400:
118 case 0x402:
119 case 0x403:
120 case 0x450:
121 case 0x451:
122 g_ps5_pager_table = (intptr_t)KERNEL_ADDRESS_DATA_BASE + 0xD20840;
123 break;
124 case 0x500:
125 case 0x502:
126 case 0x510:
127 case 0x550:
128 g_ps5_pager_table = (intptr_t)KERNEL_ADDRESS_DATA_BASE + 0xE0FEF0;
129 break;
130 case 0x600:
131 case 0x602:
132 case 0x650:
133 g_ps5_pager_table = (intptr_t)KERNEL_ADDRESS_DATA_BASE + 0xE30410;
134 break;
135 case 0x700:
136 case 0x701:
137 g_ps5_pager_table = (intptr_t)KERNEL_ADDRESS_DATA_BASE + 0xE310C0;
138 break;
139 case 0x720:
140 case 0x740:
141 case 0x760:
142 case 0x761:
143 g_ps5_pager_table = (intptr_t)KERNEL_ADDRESS_DATA_BASE + 0xE31180;
144 break;
145 case 0x800:
146 case 0x820:
147 case 0x840:
148 case 0x860:
149 g_ps5_pager_table = (intptr_t)KERNEL_ADDRESS_DATA_BASE + 0xE31250;
150 break;
151 case 0x900:
152 case 0x905:
153 case 0x920:
154 case 0x940:
155 case 0x960:
156 g_ps5_pager_table = (intptr_t)KERNEL_ADDRESS_DATA_BASE + 0xDE0420;
157 break;
158 case 0x1000:
159 case 0x1001:
160 case 0x1020:
161 case 0x1040:
162 case 0x1060:
163 g_ps5_pager_table = (intptr_t)KERNEL_ADDRESS_DATA_BASE + 0xDE04F0;
164 break;
165 case 0x1100:
166 case 0x1120:
167 g_ps5_pager_table = (intptr_t)KERNEL_ADDRESS_DATA_BASE + 0xDF1940;
168 break;
169 case 0x1140:
170 case 0x1160:
171 g_ps5_pager_table = (intptr_t)KERNEL_ADDRESS_DATA_BASE + 0xDF1960;
172 break;
173 case 0x1200:
174 case 0x1202:
175 case 0x1220:
176 case 0x1240:
177 case 0x1260:
178 case 0x1270:
179 g_ps5_pager_table = (intptr_t)KERNEL_ADDRESS_DATA_BASE + 0xDF2860;
180 break;
181 default:
182 g_ps5_pager_table = 0;
183 return;
184 }
185 
186 g_ps5_pager_ops_vnode = (intptr_t)kernel_getlong(g_ps5_pager_table + 2 * 8);
187 g_ps5_pager_ops_self = (intptr_t)kernel_getlong(g_ps5_pager_table + 7 * 8);
188}
189 
190static void *ps5_mmap_self(void *addr, size_t len, int prot, int flags, int fd, off_t offset)
191{
192 ps5_resolve_pager_addresses();
193 
194 if ((g_ps5_pager_table == 0) || (g_ps5_pager_ops_vnode == 0) || (g_ps5_pager_ops_self == 0)) {
195 errno = ENOSYS;
196 return MAP_FAILED;
197 }
198 
199 if (kernel_setlong(g_ps5_pager_table + 2 * 8, (uint64_t)g_ps5_pager_ops_self) != 0) {
200 return MAP_FAILED;
201 }
202 
203 void *data = mmap(addr, len, prot, flags, fd, offset);
204 
205 if (kernel_setlong(g_ps5_pager_table + 2 * 8, (uint64_t)g_ps5_pager_ops_vnode) != 0) {
206 return MAP_FAILED;
207 }
208 
209 return data;
210}
211#endif
212 
213typedef struct self_head {
214 uint32_t magic;
215 uint8_t version;
216 uint8_t mode;
217 uint8_t endian;
218 uint8_t attrs;
219 uint32_t key_type;
220 uint16_t header_size;
221 uint16_t meta_size;
222 uint64_t file_size;
223 uint16_t num_entries;
224 uint16_t flags;
225} self_head_t;
226 
227typedef struct self_entry {
228 struct __attribute__((packed)) {
229 uint8_t is_ordered : 1;
230 uint8_t is_encrypted : 1;
231 uint8_t is_signed : 1;
232 uint8_t is_compressed : 1;
233 uint8_t unknown0 : 4;
234 uint8_t window_bits : 3;
235 uint8_t has_blocks : 1;
236 uint8_t block_bits : 4;
237 uint8_t has_digest : 1;
238 uint8_t has_extents : 1;
239 uint8_t unknown1 : 2;
240 uint16_t segment_index : 16;
241 uint32_t unknown2 : 28;
242 } props;
243 uint64_t offset;
244 uint64_t enc_size;
245 uint64_t dec_size;
246} self_entry_t;
247 
248static const uint32_t SELF_PS4_MAGIC = 0x1D3D154FU;
249static const uint32_t SELF_PS5_MAGIC = 0xEEF51454U;
250 
251static pthread_mutex_t g_self_map_lock = PTHREAD_MUTEX_INITIALIZER;
252 
253static int read_exact(int fd, void *buf, size_t size, off_t off)
254{
255 uint8_t *p = (uint8_t *)buf;
256 size_t remaining = size;
257 off_t cur = off;
258 
259 while (remaining > 0U) {
260 ssize_t n = pread(fd, p, remaining, cur);
261 if (n < 0) {
262 if (errno == EINTR) {
263 continue;
264 }
265 return -1;
266 }
267 if (n == 0) {
268 errno = EIO;
269 return -1;
270 }
271 p += (size_t)n;
272 remaining -= (size_t)n;
273 cur += (off_t)n;
274 }
275 
276 return 0;
277}
278 
279static int self_parse_headers(int fd, self_head_t *head, uint64_t *elf_off, Elf64_Ehdr *ehdr)
280{
281 if (read_exact(fd, head, sizeof(*head), 0) != 0) {
282 return -1;
283 }
284 
285 if ((head->magic != SELF_PS4_MAGIC) && (head->magic != SELF_PS5_MAGIC)) {
286 errno = EINVAL;
287 return -1;
288 }
289 
290 uint64_t off = (uint64_t)sizeof(*head) + (uint64_t)head->num_entries * (uint64_t)sizeof(self_entry_t);
291 if (elf_off != NULL) {
292 *elf_off = off;
293 }
294 
295 if (read_exact(fd, ehdr, sizeof(*ehdr), (off_t)off) != 0) {
296 return -1;
297 }
298 
299 if ((ehdr->e_ident[EI_MAG0] != ELFMAG0) || (ehdr->e_ident[EI_MAG1] != ELFMAG1) ||
300 (ehdr->e_ident[EI_MAG2] != ELFMAG2) || (ehdr->e_ident[EI_MAG3] != ELFMAG3)) {
301 errno = EINVAL;
302 return -1;
303 }
304 
305 return 0;
306}
307 
308static int self_find_entry(int fd, uint16_t num_entries, uint16_t segment_index, uint64_t entry_table_off,
309 self_entry_t *out)
310{
311 for (uint16_t i = 0; i < num_entries; i++) {
312 self_entry_t ent;
313 off_t off = (off_t)(entry_table_off + (uint64_t)i * (uint64_t)sizeof(self_entry_t));
314 if (read_exact(fd, &ent, sizeof(ent), off) != 0) {
315 return -1;
316 }
317 if ((ent.props.segment_index == segment_index) && (ent.props.has_blocks != 0U)) {
318 *out = ent;
319 return 0;
320 }
321 }
322 
323 errno = ENOENT;
324 return -1;
325}
326 
327static void *self_map_segment(int fd, const Elf64_Phdr *phdr, uint16_t ind)
328{
329 if (phdr->p_filesz == 0U) {
330 errno = EINVAL;
331 return NULL;
332 }
333 
334 int flags = MAP_PRIVATE | MAP_SELF;
335#ifdef MAP_ALIGNED
336 if (phdr->p_align != 0U) {
337 flags |= MAP_ALIGNED((int)phdr->p_align);
338 }
339#endif
340 
341 off_t off = (off_t)((uint64_t)ind << 32);
342 
343#if defined(PLATFORM_PS5) && defined(PS5_HAVE_KERNEL)
344 if (kernel_get_fw_version() >= 0x9000000U) {
345 uint64_t aligned_vaddr = (uint64_t)phdr->p_vaddr;
346 if (phdr->p_align != 0U) {
347 aligned_vaddr &= ~((uint64_t)phdr->p_align - 1U);
348 }
349 off |= (off_t)(aligned_vaddr & (PS5_SUPERPAGE_SIZE - 1U));
350 }
351 
352 void *p = ps5_mmap_self(NULL, (size_t)phdr->p_filesz, PROT_READ, flags, fd, off);
353#else
354 void *p = mmap(NULL, (size_t)phdr->p_filesz, PROT_READ, flags, fd, off);
355#endif
356 if (p == MAP_FAILED) {
357 return NULL;
358 }
359 
360 return p;
361}
362 
363static uint64_t self_compute_elf_size(int fd, uint64_t elf_off, const Elf64_Ehdr *ehdr)
364{
365 uint64_t max_end = 0U;
366 
367 for (uint16_t i = 0; i < (uint16_t)ehdr->e_phnum; i++) {
368 Elf64_Phdr phdr;
369 off_t off = (off_t)(elf_off + (uint64_t)ehdr->e_phoff + (uint64_t)i * (uint64_t)sizeof(phdr));
370 if (read_exact(fd, &phdr, sizeof(phdr), off) != 0) {
371 return 0U;
372 }
373 
374 if (phdr.p_filesz == 0U) {
375 continue;
376 }
377 
378 uint64_t end = (uint64_t)phdr.p_offset + (uint64_t)phdr.p_filesz;
379 if (end > max_end) {
380 max_end = end;
381 }
382 }
383 
384 return max_end;
385}
386 
387ftp_error_t psx_vfs_stat(const char *path, vfs_stat_t *out)
388{
389 if ((path == NULL) || (out == NULL)) {
390 return FTP_ERR_INVALID_PARAM;
391 }
392 
393 struct stat st;
394 ftp_error_t err = pal_file_stat(path, &st);
395 if (err != FTP_OK) {
396 return err;
397 }
398 
399 out->mode = (uint32_t)st.st_mode;
400 out->mtime = (int64_t)st.st_mtime;
401 out->size = (uint64_t)st.st_size;
402 
403 /*
404 * DESIGN RATIONALE — why we do NOT override size with self_compute_elf_size()
405 *
406 * Previous code opened every file here and, when it detected a SELF header
407 * (PS4/PS5 encrypted executable), replaced out->size with the logical ELF
408 * size computed from the program-header table.
409 *
410 * This caused a critical file-transfer bug:
411 *
412 * - A SELF container may be 12 GB on disk but its embedded ELF segments
413 * occupy only ~419 MB (the rest is SELF metadata, signatures, and
414 * encrypted padding).
415 *
416 * - FTP clients use the size reported by SIZE / MLSD to determine how
417 * many bytes constitute a complete file. When they see 419 MB they
418 * stop reading after 419 MB, even though the actual file is 12 GB.
419 *
420 * - vfs_open() then opened the same file via psx_vfs_try_open_self
421 * (MAP_SELF path), sending 419 MB of DECRYPTED ELF bytes instead of
422 * the raw on-disk content. The client receives a truncated, decrypted
423 * file — useless for backup or copying.
424 *
425 * zftpd is a file-transfer daemon: it must report and transfer the ACTUAL
426 * bytes present on disk (st_size), not the logical ELF payload. Anyone
427 * who needs to inspect the ELF structure should use platform tools.
428 *
429 * The self_parse_headers / self_compute_elf_size machinery is kept for
430 * psx_vfs_try_open_self (the MAP_SELF execution path) but must not
431 * influence size reporting for transfer purposes.
432 */
433 return FTP_OK;
434}
435 
436int psx_vfs_try_open_self(vfs_node_t *node, const char *path)
437{
438 if ((node == NULL) || (path == NULL)) {
439 errno = EINVAL;
440 return -1;
441 }
442 
443 int fd = pal_file_open(path, O_RDONLY, 0);
444 if (fd < 0) {
445 return -1;
446 }
447 
448 self_head_t head;
449 uint64_t elf_off = 0U;
450 Elf64_Ehdr ehdr;
451 
452 if (self_parse_headers(fd, &head, &elf_off, &ehdr) != 0) {
453 pal_file_close(fd);
454 return 0;
455 }
456 
457 uint64_t elf_size = self_compute_elf_size(fd, elf_off, &ehdr);
458 if (elf_size == 0U) {
459 pal_file_close(fd);
460 return -1;
461 }
462 
463 node->caps = VFS_CAP_STREAM_ONLY;
464 node->fd = -1;
465 node->size = elf_size;
466 node->offset = 0U;
467 node->private_ctx = &node->psx;
468 node->psx.self_fd = fd;
469 node->psx.elf_off = elf_off;
470 node->psx.num_entries = head.num_entries;
471 node->psx.phnum = (uint16_t)ehdr.e_phnum;
472 node->psx.phoff = (uint64_t)ehdr.e_phoff;
473 node->psx.file_size = head.file_size;
474 node->psx.magic = head.magic;
475 
476 return 1;
477}
478 
479static int find_covering_phdr(int fd, const vfs_node_t *node, uint64_t offset, Elf64_Phdr *out, uint16_t *out_index)
480{
481 uint64_t elf_off = node->psx.elf_off;
482 uint64_t phoff = node->psx.phoff;
483 uint16_t phnum = node->psx.phnum;
484 
485 uint64_t best_next = UINT64_MAX;
486 int found = 0;
487 
488 for (uint16_t i = 0; i < phnum; i++) {
489 Elf64_Phdr phdr;
490 off_t off = (off_t)(elf_off + phoff + (uint64_t)i * (uint64_t)sizeof(phdr));
491 if (read_exact(fd, &phdr, sizeof(phdr), off) != 0) {
492 return -1;
493 }
494 
495 uint64_t start = (uint64_t)phdr.p_offset;
496 uint64_t end = start + (uint64_t)phdr.p_filesz;
497 
498 if ((phdr.p_filesz != 0U) && (offset >= start) && (offset < end)) {
499 *out = phdr;
500 if (out_index != NULL) {
501 *out_index = i;
502 }
503 return 1;
504 }
505 
506 if ((phdr.p_filesz != 0U) && (start > offset) && (start < best_next)) {
507 best_next = start;
508 found = 0;
509 }
510 }
511 
512 (void)found;
513 return 0;
514}
515 
516ssize_t psx_vfs_read(vfs_node_t *node, void *buffer, size_t length)
517{
518 if ((node == NULL) || (buffer == NULL) || (length == 0U)) {
519 errno = EINVAL;
520 return -1;
521 }
522 
523 if (node->psx.self_fd < 0) {
524 errno = EBADF;
525 return -1;
526 }
527 
528 uint64_t pos = node->offset;
529 if (pos >= node->size) {
530 return 0;
531 }
532 
533 size_t to_read = length;
534 if ((uint64_t)to_read > (node->size - pos)) {
535 to_read = (size_t)(node->size - pos);
536 }
537 
538 uint8_t *dst = (uint8_t *)buffer;
539 size_t done = 0U;
540 
541 const uint64_t entry_table_off = (uint64_t)sizeof(self_head_t);
542 
543 while (done < to_read) {
544 uint64_t cur_off = pos + (uint64_t)done;
545 
546 Elf64_Phdr phdr;
547 uint16_t seg_index = 0U;
548 int phdr_res = find_covering_phdr(node->psx.self_fd, node, cur_off, &phdr, &seg_index);
549 
550 if (phdr_res < 0) {
551 return -1;
552 }
553 
554 if (phdr_res == 0) {
555 size_t zero_len = to_read - done;
556 memset(dst + done, 0, zero_len);
557 done += zero_len;
558 continue;
559 }
560 
561 uint64_t seg_start = (uint64_t)phdr.p_offset;
562 uint64_t seg_end = seg_start + (uint64_t)phdr.p_filesz;
563 size_t seg_avail = (size_t)(seg_end - cur_off);
564 size_t chunk = to_read - done;
565 if (chunk > seg_avail) {
566 chunk = seg_avail;
567 }
568 
569 uint64_t delta = cur_off - seg_start;
570 
571 if (phdr.p_type == 0x6fffff01U) {
572 off_t src_off = (off_t)((uint64_t)node->psx.file_size + delta);
573 if (read_exact(node->psx.self_fd, dst + done, chunk, src_off) != 0) {
574 return -1;
575 }
576 done += chunk;
577 continue;
578 }
579 
580 self_entry_t ent;
581 if (self_find_entry(node->psx.self_fd, node->psx.num_entries, seg_index, entry_table_off, &ent) != 0) {
582 memset(dst + done, 0, chunk);
583 done += chunk;
584 continue;
585 }
586 
587 int encrypted = (ent.props.is_encrypted != 0U) || (ent.props.is_compressed != 0U);
588 if (!encrypted) {
589 off_t src_off = (off_t)((uint64_t)ent.offset + delta);
590 if (read_exact(node->psx.self_fd, dst + done, chunk, src_off) != 0) {
591 return -1;
592 }
593 done += chunk;
594 continue;
595 }
596 
597 pthread_mutex_lock(&g_self_map_lock);
598 void *map = self_map_segment(node->psx.self_fd, &phdr, seg_index);
599 if (map == NULL) {
600 pthread_mutex_unlock(&g_self_map_lock);
601 return -1;
602 }
603 
604 memcpy(dst + done, (const uint8_t *)map + delta, chunk);
605 (void)munmap(map, (size_t)phdr.p_filesz);
606 pthread_mutex_unlock(&g_self_map_lock);
607 
608 done += chunk;
609 }
610 
611 node->offset += (uint64_t)done;
612 return (ssize_t)done;
613}
614 
615#endif
616