Zero-copy FTP/HTTP Daemon compatible with all POSIX systems
| 1 | /* |
| 2 | * MIT License — Copyright (c) 2026 SeregonWar |
| 3 | * See LICENSE for full text. |
| 4 | */ |
| 5 | |
| 6 | /** |
| 7 | * @file ps5_net_filter_hook.c |
| 8 | * @brief Kernel-context hook functions for the PS5 network filter |
| 9 | * |
| 10 | * ╔══════════════════════════════════════════════════════════════════════╗ |
| 11 | * ║ CRITICAL — THIS FILE RUNS IN KERNEL CONTEXT (ring 0) ║ |
| 12 | * ║ ║ |
| 13 | * ║ CONSTRAINTS (enforced by compiler flags and code rules): ║ |
| 14 | * ║ • No stack canary / stack protector (-fno-stack-protector) ║ |
| 15 | * ║ • No red zone (-mno-red-zone) ║ |
| 16 | * ║ • Position-independent code (-fPIC, -mcmodel=large) ║ |
| 17 | * ║ • No PLT / GOT accesses (-fno-plt) ║ |
| 18 | * ║ • No userland pointer dereferences ║ |
| 19 | * ║ • No dynamic allocation (no malloc) ║ |
| 20 | * ║ • Bounded loops only ║ |
| 21 | * ║ • No floating point (kernel disables FPU in ring 0) ║ |
| 22 | * ║ • All external data accessed via the shared block (RIP-relative) ║ |
| 23 | * ╚══════════════════════════════════════════════════════════════════════╝ |
| 24 | * |
| 25 | * |
| 26 | * BUILD INSTRUCTIONS |
| 27 | * ------------------ |
| 28 | * This file is compiled separately with kernel-safe flags. The resulting |
| 29 | * machine code is extracted and embedded in ps5_net_filter.c as byte arrays. |
| 30 | * |
| 31 | * Makefile excerpt (added automatically when TARGET=ps5): |
| 32 | * |
| 33 | * # Compile hook with kernel-safe flags |
| 34 | * $(CC_PS5) \ |
| 35 | * -DPS5_HOOK_BUILD \ |
| 36 | * -DPS5_HOOK_CONNECT_OFFSET=0x000 \ |
| 37 | * -DPS5_HOOK_SHARED_OFFSET=0x480 \ |
| 38 | * -O2 \ |
| 39 | * -fno-stack-protector \ |
| 40 | * -mno-red-zone \ |
| 41 | * -fPIC \ |
| 42 | * -mcmodel=large \ |
| 43 | * -fno-plt \ |
| 44 | * -fno-common \ |
| 45 | * -fno-builtin \ |
| 46 | * -fno-exceptions \ |
| 47 | * -nostdinc \ |
| 48 | * -I include/ \ |
| 49 | * -c src/ps5_net_filter_hook.c \ |
| 50 | * -o build/ps5/ps5_net_filter_hook.o |
| 51 | * |
| 52 | * # Extract .text.hook section as raw binary |
| 53 | * $(OBJCOPY) -O binary \ |
| 54 | * --only-section=.text.hook_connect \ |
| 55 | * --only-section=.text.hook_sendto \ |
| 56 | * build/ps5/ps5_net_filter_hook.o \ |
| 57 | * build/ps5/ps5_net_filter_hook.bin |
| 58 | * |
| 59 | * # Generate C byte array for inclusion |
| 60 | * xxd -i build/ps5/ps5_net_filter_hook.bin \ |
| 61 | * > src/ps5_net_filter_hook_blob.h |
| 62 | * |
| 63 | * The generated blob is then used in ps5_net_filter.c to replace the |
| 64 | * placeholder arrays (g_hook_connect_code[], g_hook_sendto_code[]). |
| 65 | * |
| 66 | * |
| 67 | * CALLING CONVENTION (FreeBSD amd64 sy_call_t) |
| 68 | * --------------------------------------------- |
| 69 | * int sy_call(struct thread *td, void *uap); |
| 70 | * |
| 71 | * Registers on entry: |
| 72 | * rdi = struct thread *td (current thread) |
| 73 | * rsi = struct connect_args *uap (syscall arguments) |
| 74 | * |
| 75 | * Return: |
| 76 | * eax = 0 → success (pass to original handler) |
| 77 | * eax = errno → error (returned directly to userland) |
| 78 | * |
| 79 | * |
| 80 | * SHARED BLOCK LAYOUT (ps5_hook_shared_t — defined in ps5_net_filter.c) |
| 81 | * ----------------------------------------------------------------------- |
| 82 | * This struct is embedded at HOOK_SHARED_DATA_OFFSET (0x480) in the kernel |
| 83 | * hook page. The hooks access it via a known kernel virtual address that is |
| 84 | * patched into the code at install time. |
| 85 | * |
| 86 | * Offset Field |
| 87 | * +0x00 uintptr_t original_connect (8 bytes) |
| 88 | * +0x08 uintptr_t original_sendto (8 bytes) |
| 89 | * +0x10 int32_t zftpd_pid (4 bytes) |
| 90 | * +0x14 uint32_t rule_count (4 bytes) |
| 91 | * +0x18 rule_t rules[32] (32 × 8 = 256 bytes) |
| 92 | * +0x118 uint32_t td_proc_offset (4 bytes) |
| 93 | * +0x11C uint32_t proc_pid_offset (4 bytes) |
| 94 | * +0x120 int64_t stat_blocked (8 bytes) |
| 95 | * +0x128 int64_t stat_allowed_self (8 bytes) |
| 96 | * +0x130 int64_t stat_allowed_local (8 bytes) |
| 97 | * +0x138 int64_t stat_allowed_other (8 bytes) |
| 98 | * +0x140 int64_t stat_hook_calls (8 bytes) |
| 99 | */ |
| 100 | |
| 101 | #ifdef PS5_HOOK_BUILD |
| 102 | |
| 103 | /* |
| 104 | * Kernel-mode headers only. |
| 105 | * We cannot include standard libc headers here. |
| 106 | */ |
| 107 | #include <stdint.h> |
| 108 | #include <stddef.h> |
| 109 | |
| 110 | /*===========================================================================* |
| 111 | * PRIMITIVE TYPE ALIASES (no libc) |
| 112 | *===========================================================================*/ |
| 113 | |
| 114 | typedef unsigned char u8; |
| 115 | typedef unsigned short u16; |
| 116 | typedef unsigned int u32; |
| 117 | typedef unsigned long long u64; |
| 118 | typedef signed int s32; |
| 119 | typedef signed long long s64; |
| 120 | |
| 121 | /*===========================================================================* |
| 122 | * KERNEL CONSTANTS |
| 123 | *===========================================================================*/ |
| 124 | |
| 125 | #define AF_INET 2U |
| 126 | |
| 127 | /** ENETUNREACH (errno 51 on FreeBSD) */ |
| 128 | #define ENETUNREACH 51 |
| 129 | |
| 130 | /** Address families */ |
| 131 | #define SA_FAMILY_OFFSET 0U /* sockaddr.sa_family offset */ |
| 132 | #define SA_DATA_OFFSET 2U /* sockaddr.sa_data[0] offset */ |
| 133 | |
| 134 | /** sockaddr_in layout */ |
| 135 | #define SIN_FAMILY_OFFSET 0U /* sin_family (u16) */ |
| 136 | #define SIN_PORT_OFFSET 2U /* sin_port (u16) */ |
| 137 | #define SIN_ADDR_OFFSET 4U /* sin_addr (u32) — network byte order */ |
| 138 | |
| 139 | /*===========================================================================* |
| 140 | * SHARED BLOCK FIELD OFFSETS |
| 141 | * (must match ps5_hook_shared_t layout in ps5_net_filter.c) |
| 142 | *===========================================================================*/ |
| 143 | |
| 144 | #define SHARED_ORIGINAL_CONNECT_OFF 0x00U |
| 145 | #define SHARED_ORIGINAL_SENDTO_OFF 0x08U |
| 146 | #define SHARED_ZFTPD_PID_OFF 0x10U |
| 147 | #define SHARED_RULE_COUNT_OFF 0x14U |
| 148 | #define SHARED_RULES_OFF 0x18U /* rule_t rules[32] */ |
| 149 | #define SHARED_TD_PROC_OFF 0x118U |
| 150 | #define SHARED_PROC_PID_OFF 0x11CU |
| 151 | #define SHARED_STAT_BLOCKED_OFF 0x120U |
| 152 | #define SHARED_STAT_SELF_OFF 0x128U |
| 153 | #define SHARED_STAT_LOCAL_OFF 0x130U |
| 154 | #define SHARED_STAT_OTHER_OFF 0x138U |
| 155 | #define SHARED_STAT_CALLS_OFF 0x140U |
| 156 | |
| 157 | /** Size of one rule_t entry (network u32 + mask u32) */ |
| 158 | #define RULE_SIZE 8U |
| 159 | |
| 160 | /*===========================================================================* |
| 161 | * KERNEL copyin / copyout PROTOTYPES |
| 162 | * |
| 163 | * These are kernel functions available in ring-0 context. |
| 164 | * They safely copy between kernel and user memory. |
| 165 | *===========================================================================*/ |
| 166 | |
| 167 | /** |
| 168 | * Copy 'len' bytes from userland address 'uaddr' into kernel buffer 'kaddr'. |
| 169 | * Returns 0 on success, EFAULT on bad user pointer. |
| 170 | */ |
| 171 | extern int copyin(const void *uaddr, void *kaddr, size_t len); |
| 172 | |
| 173 | /** |
| 174 | * Copy 'len' bytes from kernel buffer 'kaddr' to userland address 'uaddr'. |
| 175 | * Returns 0 on success, EFAULT. |
| 176 | */ |
| 177 | extern int copyout(const void *kaddr, void *uaddr, size_t len); |
| 178 | |
| 179 | /*===========================================================================* |
| 180 | * HELPER: RFC-1918 / LOOPBACK CHECK |
| 181 | *===========================================================================*/ |
| 182 | |
| 183 | /** |
| 184 | * @brief Check if an IPv4 address (network byte order) is local/private. |
| 185 | * |
| 186 | * Returns 1 for: |
| 187 | * 127.0.0.0/8 (loopback) |
| 188 | * 10.0.0.0/8 (RFC-1918) |
| 189 | * 172.16.0.0/12 (RFC-1918) |
| 190 | * 192.168.0.0/16 (RFC-1918) |
| 191 | * 169.254.0.0/16 (link-local) |
| 192 | * |
| 193 | * @note All comparisons are in network byte order (big-endian). |
| 194 | * On x86-64 (little-endian), the first byte of a network-order u32 |
| 195 | * occupies the MOST significant byte position: |
| 196 | * ip = 0xC0A80101 → 192.168.1.1 (0xC0 = 192, 0xA8 = 168, ...) |
| 197 | * |
| 198 | * @note No branches involving external data — all constants are immediate. |
| 199 | * WCET: O(1), no loops, no memory reads. |
| 200 | */ |
| 201 | static inline __attribute__((always_inline)) |
| 202 | int is_local_address(u32 ip_nbo) |
| 203 | { |
| 204 | /* |
| 205 | * Network-byte-order comparisons. |
| 206 | * |
| 207 | * On amd64 (little-endian), a 32-bit value stored in memory as |
| 208 | * { 0x7F, 0x00, 0x00, 0x01 } (127.0.0.1) is loaded as 0x0100007F. |
| 209 | * BUT here ip_nbo is already in network byte order (big-endian in memory, |
| 210 | * loaded as a u32 integer). So we compare the HIGH byte. |
| 211 | * |
| 212 | * EXAMPLE: |
| 213 | * ip_nbo = 0x7F000001 (127.0.0.1 in big-endian as integer) |
| 214 | * High byte = (ip_nbo >> 24) = 0x7F = 127 ← loopback |
| 215 | * |
| 216 | * Wait — this depends on whether the kernel stores it in the u32 with |
| 217 | * big-endian semantics. FreeBSD sin_addr.s_addr is in network byte order |
| 218 | * (big-endian). When we do a u32 read on amd64 (little-endian), the bytes |
| 219 | * are reversed: |
| 220 | * |
| 221 | * sin_addr for 192.168.1.1: |
| 222 | * Stored in memory: C0 A8 01 01 |
| 223 | * Loaded as u32 on LE: 0x0101A8C0 |
| 224 | * |
| 225 | * So we must compare the LOW byte to get the first octet! |
| 226 | * (ip_nbo & 0xFF) == 127 → loopback |
| 227 | * (ip_nbo & 0xFF) == 10 → 10.x.x.x |
| 228 | * (ip_nbo & 0xFF) == 192 AND ((ip_nbo >> 8) & 0xFF) == 168 → 192.168.x.x |
| 229 | * (ip_nbo & 0xFF) == 172 AND (((ip_nbo >> 8) & 0xFF) & 0xF0) == 16 → 172.16-31.x.x |
| 230 | * (ip_nbo & 0xFF) == 169 AND ((ip_nbo >> 8) & 0xFF) == 254 → 169.254.x.x |
| 231 | */ |
| 232 | |
| 233 | u32 b0 = ip_nbo & 0xFFU; |
| 234 | u32 b1 = (ip_nbo >> 8U) & 0xFFU; |
| 235 | |
| 236 | /* 127.x.x.x — loopback */ |
| 237 | if (b0 == 127U) { return 1; } |
| 238 | |
| 239 | /* 10.x.x.x — RFC-1918 Class A */ |
| 240 | if (b0 == 10U) { return 1; } |
| 241 | |
| 242 | /* 192.168.x.x — RFC-1918 Class C */ |
| 243 | if ((b0 == 192U) && (b1 == 168U)) { return 1; } |
| 244 | |
| 245 | /* 172.16.0.0 – 172.31.255.255 — RFC-1918 Class B */ |
| 246 | if ((b0 == 172U) && (b1 >= 16U) && (b1 <= 31U)) { return 1; } |
| 247 | |
| 248 | /* 169.254.x.x — link-local */ |
| 249 | if ((b0 == 169U) && (b1 == 254U)) { return 1; } |
| 250 | |
| 251 | return 0; |
| 252 | } |
| 253 | |
| 254 | /*===========================================================================* |
| 255 | * HELPER: MATCH IP AGAINST RULE TABLE |
| 256 | *===========================================================================*/ |
| 257 | |
| 258 | /** |
| 259 | * @brief Check if ip_nbo matches any rule in the block table. |
| 260 | * |
| 261 | * @param ip_nbo Destination IP (network byte order, LE-loaded u32). |
| 262 | * @param rules_ptr Kernel VA of rules[] in the shared block. |
| 263 | * @param rule_count Number of rules (bounded by PS5_NET_FILTER_MAX_RULES). |
| 264 | * |
| 265 | * @return 1 if the IP should be blocked, 0 otherwise. |
| 266 | * |
| 267 | * @note WCET: O(rule_count) — at most 32 iterations. |
| 268 | * Each iteration: 3 reads + 2 ANDs + 1 compare = ~8 cycles. |
| 269 | * Total worst-case: ~256 cycles ≈ 0.1 µs @ 3.5 GHz. Negligible. |
| 270 | */ |
| 271 | static inline __attribute__((always_inline)) |
| 272 | int ip_matches_blocklist(u32 ip_nbo, |
| 273 | const u64 *rules_ptr, |
| 274 | u32 rule_count) |
| 275 | { |
| 276 | /* |
| 277 | * rules_ptr points to an array of rule_t: |
| 278 | * struct rule_t { u32 network; u32 mask; }; |
| 279 | * |
| 280 | * Stored as two consecutive u32 (network byte order in memory). |
| 281 | * We read them as two u32 from the kernel page directly. |
| 282 | */ |
| 283 | |
| 284 | if (rule_count > 32U) { |
| 285 | rule_count = 32U; /* Defensive clamp — should never be needed */ |
| 286 | } |
| 287 | |
| 288 | for (u32 i = 0U; i < rule_count; i++) { |
| 289 | /* |
| 290 | * Each rule is 8 bytes: [network:4][mask:4] |
| 291 | * We cast rules_ptr to a byte stream for clarity. |
| 292 | */ |
| 293 | const u8 *rule_bytes = (const u8 *)rules_ptr + (u64)(i * 8U); |
| 294 | |
| 295 | u32 network; |
| 296 | u32 mask; |
| 297 | |
| 298 | /* Safe kernel-memory reads (already in kernel space, no copyin needed) */ |
| 299 | __builtin_memcpy(&network, rule_bytes, sizeof(u32)); |
| 300 | __builtin_memcpy(&mask, rule_bytes + 4U, sizeof(u32)); |
| 301 | |
| 302 | /* |
| 303 | * Match condition: (ip & mask) == (network & mask) |
| 304 | * |
| 305 | * Both ip_nbo and the stored network/mask are in the same byte |
| 306 | * order (LE-loaded network-byte-order u32), so the comparison |
| 307 | * is direct. |
| 308 | */ |
| 309 | if ((ip_nbo & mask) == (network & mask)) { |
| 310 | return 1; |
| 311 | } |
| 312 | } |
| 313 | |
| 314 | return 0; |
| 315 | } |
| 316 | |
| 317 | /*===========================================================================* |
| 318 | * hook_sys_connect |
| 319 | *===========================================================================*/ |
| 320 | |
| 321 | /** |
| 322 | * @brief Replacement for the kernel's sys_connect(2) handler. |
| 323 | * |
| 324 | * Called for EVERY connect(2) syscall on the system. |
| 325 | * |
| 326 | * Function signature matches FreeBSD sy_call_t: |
| 327 | * int (*sy_call_t)(struct thread *, void *); |
| 328 | * |
| 329 | * @param td Pointer to the current kernel thread structure. |
| 330 | * @param uap Pointer to connect_args on the kernel stack: |
| 331 | * struct connect_args { int s; caddr_t name; int namelen; }; |
| 332 | * |
| 333 | * @return 0 → pass to original handler (allow). |
| 334 | * @return ENETUNREACH → deny connection without packet generation. |
| 335 | * |
| 336 | * @note Placed in a named section so objcopy can extract it precisely. |
| 337 | */ |
| 338 | __attribute__((section(".text.hook_connect"), noinline)) |
| 339 | int hook_sys_connect(void *td, void *uap) |
| 340 | { |
| 341 | /* |
| 342 | * ── SHARED BLOCK ACCESS ────────────────────────────────────────────── |
| 343 | * |
| 344 | * The shared block is at a known kernel VA patched in at install time. |
| 345 | * We access it via a pointer stored in a variable that the compiler |
| 346 | * will address via a RIP-relative load from the code section. |
| 347 | * |
| 348 | * IMPORTANT: This pointer is PATCHED at install time by ps5_net_filter.c. |
| 349 | * At compile time it is initialised to 0; the install routine overwrites |
| 350 | * the 8 bytes at HOOK_CONNECT_JMP_PATCH_OFFSET with the actual kaddr. |
| 351 | * |
| 352 | * We declare it as a volatile pointer to prevent the compiler from |
| 353 | * caching the value — the kernel VA is only valid after patching. |
| 354 | */ |
| 355 | static volatile u64 g_shared_kaddr = 0ULL; /* PATCHED at install */ |
| 356 | |
| 357 | /* Cast to byte pointer for field access by offset */ |
| 358 | const volatile u8 *sh = (const volatile u8 *)g_shared_kaddr; |
| 359 | |
| 360 | if (sh == NULL) { |
| 361 | /* |
| 362 | * Hook was called before the shared block pointer was patched. |
| 363 | * This should never happen in normal operation — it would mean the |
| 364 | * sysent was patched but the shared pointer was not written. |
| 365 | * Fall through to original handler (safe fallback). |
| 366 | */ |
| 367 | goto call_original; |
| 368 | } |
| 369 | |
| 370 | /* ── stat: increment total hook calls ──────────────────────────────── */ |
| 371 | { |
| 372 | volatile s64 *p_calls = (volatile s64 *)(sh + SHARED_STAT_CALLS_OFF); |
| 373 | __asm__ __volatile__ ( |
| 374 | "lock xaddq %0, (%1)" |
| 375 | : "+r" ((s64){1}) |
| 376 | : "r" (p_calls) |
| 377 | : "memory" |
| 378 | ); |
| 379 | } |
| 380 | |
| 381 | /* ── Extract zftpd PID and current process PID ──────────────────────── */ |
| 382 | s32 zftpd_pid; |
| 383 | __builtin_memcpy(&zftpd_pid, sh + SHARED_ZFTPD_PID_OFF, sizeof(s32)); |
| 384 | |
| 385 | /* |
| 386 | * Access current process PID via td->td_proc->p_pid. |
| 387 | * |
| 388 | * Offsets are in the shared block (firmware-specific): |
| 389 | * td_proc_off = td->td_proc field offset |
| 390 | * proc_pid_off = proc->p_pid field offset |
| 391 | */ |
| 392 | u32 td_proc_off; |
| 393 | u32 proc_pid_off; |
| 394 | __builtin_memcpy(&td_proc_off, sh + SHARED_TD_PROC_OFF, sizeof(u32)); |
| 395 | __builtin_memcpy(&proc_pid_off, sh + SHARED_PROC_PID_OFF, sizeof(u32)); |
| 396 | |
| 397 | /* Dereference td->td_proc */ |
| 398 | void *td_proc = NULL; |
| 399 | { |
| 400 | const u8 *td_bytes = (const u8 *)td; |
| 401 | __builtin_memcpy(&td_proc, td_bytes + td_proc_off, sizeof(void *)); |
| 402 | } |
| 403 | |
| 404 | if (td_proc == NULL) { |
| 405 | goto call_original; /* Defensive: corrupt td, pass through */ |
| 406 | } |
| 407 | |
| 408 | /* Read p_pid from struct proc */ |
| 409 | s32 caller_pid = -1; |
| 410 | { |
| 411 | const u8 *proc_bytes = (const u8 *)td_proc; |
| 412 | __builtin_memcpy(&caller_pid, proc_bytes + proc_pid_off, sizeof(s32)); |
| 413 | } |
| 414 | |
| 415 | /* ── FAST PATH: zftpd's own connections always allowed ──────────────── */ |
| 416 | if (caller_pid == zftpd_pid) { |
| 417 | volatile s64 *p_self = (volatile s64 *)(sh + SHARED_STAT_SELF_OFF); |
| 418 | __asm__ __volatile__ ("lock xaddq %0, (%1)" |
| 419 | : "+r" ((s64){1}) : "r" (p_self) : "memory"); |
| 420 | goto call_original; |
| 421 | } |
| 422 | |
| 423 | /* ── Extract destination IP from uap->name (struct sockaddr *) ──────── */ |
| 424 | /* |
| 425 | * connect_args layout (FreeBSD amd64): |
| 426 | * [0x00] int s (4 bytes) |
| 427 | * [0x04] padding (4 bytes) |
| 428 | * [0x08] caddr_t name (8 bytes) ← pointer to struct sockaddr in USER space |
| 429 | * [0x10] int namelen (4 bytes) |
| 430 | */ |
| 431 | void *sockaddr_uptr = NULL; |
| 432 | { |
| 433 | const u8 *uap_bytes = (const u8 *)uap; |
| 434 | __builtin_memcpy(&sockaddr_uptr, uap_bytes + 0x08U, sizeof(void *)); |
| 435 | } |
| 436 | |
| 437 | if (sockaddr_uptr == NULL) { |
| 438 | goto call_original; /* No address — let kernel handle it */ |
| 439 | } |
| 440 | |
| 441 | /* |
| 442 | * copyin() can sleep while resolving a page fault. FreeBSD forbids |
| 443 | * sleeping while a non-sleepable lock is held (INVARIANTS will panic). |
| 444 | * |
| 445 | * Some system threads (e.g. SceSpZeroConfMain) call connect() while |
| 446 | * holding a non-sleepable lock. Detect this by checking the thread's |
| 447 | * critical-section nesting counter and lock count: |
| 448 | * |
| 449 | * struct thread: |
| 450 | * td_critnest — incremented by critical_enter(), must be 0 to sleep |
| 451 | * td_locks — number of non-sleepable locks held, must be 0 |
| 452 | * |
| 453 | * Offsets on FreeBSD 11 amd64 (PS5): td_critnest=0x1CC, td_locks=0x1D0. |
| 454 | * These are stable across all PS5 firmware versions that match our table. |
| 455 | * If we cannot confirm both are zero, fall through to the original handler |
| 456 | * (safe: we skip filtering for that call rather than panic the kernel). |
| 457 | * |
| 458 | * NOTE: FreeBSD td_critnest is u_int (4 bytes); td_locks is int (4 bytes). |
| 459 | */ |
| 460 | { |
| 461 | const u8 *td_bytes = (const u8 *)td; |
| 462 | u32 critnest = 0U; |
| 463 | s32 locks = 0; |
| 464 | __builtin_memcpy(&critnest, td_bytes + 0x1CCU, sizeof(u32)); |
| 465 | __builtin_memcpy(&locks, td_bytes + 0x1D0U, sizeof(s32)); |
| 466 | if ((critnest != 0U) || (locks != 0)) { |
| 467 | /* Cannot sleep — skip copyin, allow the connection */ |
| 468 | goto allow_other; |
| 469 | } |
| 470 | } |
| 471 | |
| 472 | /* |
| 473 | * copyin() the first 8 bytes of the sockaddr from userland. |
| 474 | * We only need sa_family (2 bytes) and sin_addr (4 bytes at offset 4). |
| 475 | * Reading 8 bytes covers both safely. |
| 476 | */ |
| 477 | u8 sa_buf[8] = {0}; |
| 478 | if (copyin(sockaddr_uptr, sa_buf, sizeof(sa_buf)) != 0) { |
| 479 | goto call_original; /* Bad user pointer — let kernel reject it */ |
| 480 | } |
| 481 | |
| 482 | u16 sa_family; |
| 483 | __builtin_memcpy(&sa_family, sa_buf + SIN_FAMILY_OFFSET, sizeof(u16)); |
| 484 | |
| 485 | /* ── Only filter IPv4 (AF_INET = 2) ────────────────────────────────── */ |
| 486 | if (sa_family != (u16)AF_INET) { |
| 487 | goto allow_other; |
| 488 | } |
| 489 | |
| 490 | /* Extract destination IP (network byte order, loaded as LE u32) */ |
| 491 | u32 dest_ip; |
| 492 | __builtin_memcpy(&dest_ip, sa_buf + SIN_ADDR_OFFSET, sizeof(u32)); |
| 493 | |
| 494 | /* ── Allow RFC-1918 / loopback (fast path, no rule scan needed) ─────── */ |
| 495 | if (is_local_address(dest_ip) != 0) { |
| 496 | goto allow_local; |
| 497 | } |
| 498 | |
| 499 | /* ── Check against the block list ──────────────────────────────────── */ |
| 500 | u32 rule_count; |
| 501 | __builtin_memcpy(&rule_count, sh + SHARED_RULE_COUNT_OFF, sizeof(u32)); |
| 502 | |
| 503 | const u64 *rules_ptr = (const u64 *)(sh + SHARED_RULES_OFF); |
| 504 | |
| 505 | if (ip_matches_blocklist(dest_ip, rules_ptr, rule_count) != 0) { |
| 506 | /* BLOCK: increment stat and return ENETUNREACH */ |
| 507 | volatile s64 *p_blocked = (volatile s64 *)(sh + SHARED_STAT_BLOCKED_OFF); |
| 508 | __asm__ __volatile__ ("lock xaddq %0, (%1)" |
| 509 | : "+r" ((s64){1}) : "r" (p_blocked) : "memory"); |
| 510 | return ENETUNREACH; |
| 511 | } |
| 512 | |
| 513 | allow_other: |
| 514 | { |
| 515 | volatile s64 *p_other = (volatile s64 *)(sh + SHARED_STAT_OTHER_OFF); |
| 516 | __asm__ __volatile__ ("lock xaddq %0, (%1)" |
| 517 | : "+r" ((s64){1}) : "r" (p_other) : "memory"); |
| 518 | } |
| 519 | goto call_original; |
| 520 | |
| 521 | allow_local: |
| 522 | { |
| 523 | volatile s64 *p_local = (volatile s64 *)(sh + SHARED_STAT_LOCAL_OFF); |
| 524 | __asm__ __volatile__ ("lock xaddq %0, (%1)" |
| 525 | : "+r" ((s64){1}) : "r" (p_local) : "memory"); |
| 526 | } |
| 527 | |
| 528 | call_original: |
| 529 | { |
| 530 | /* |
| 531 | * Tail-call the original sys_connect handler. |
| 532 | * |
| 533 | * We load the function pointer from the shared block and jump to it. |
| 534 | * Using a tail call avoids adding a stack frame — important in |
| 535 | * kernel context where stack depth is limited. |
| 536 | * |
| 537 | * The original_connect field is at sh + SHARED_ORIGINAL_CONNECT_OFF. |
| 538 | */ |
| 539 | uintptr_t orig_fn = 0U; |
| 540 | __builtin_memcpy(&orig_fn, |
| 541 | (const void *)((u64)sh + SHARED_ORIGINAL_CONNECT_OFF), |
| 542 | sizeof(uintptr_t)); |
| 543 | |
| 544 | typedef int (*sy_call_fn)(void *, void *); |
| 545 | sy_call_fn fn = (sy_call_fn)orig_fn; |
| 546 | return fn(td, uap); |
| 547 | } |
| 548 | } |
| 549 | |
| 550 | /*===========================================================================* |
| 551 | * hook_sys_sendto |
| 552 | *===========================================================================*/ |
| 553 | |
| 554 | /** |
| 555 | * @brief Replacement for the kernel's sys_sendto(2) handler. |
| 556 | * |
| 557 | * Intercepts UDP sendto() calls targeting Sony CDN/PSN IPs. |
| 558 | * Sony's RNPS (React Native PlayStation) runtime uses sendto() for |
| 559 | * some telemetry and configuration push events over UDP. |
| 560 | * |
| 561 | * sendto_args layout (FreeBSD amd64): |
| 562 | * [0x00] int s (4 bytes) |
| 563 | * [0x04] padding (4 bytes) |
| 564 | * [0x08] caddr_t buf (8 bytes) |
| 565 | * [0x10] size_t len (8 bytes) |
| 566 | * [0x18] int flags (4 bytes) |
| 567 | * [0x1C] padding (4 bytes) |
| 568 | * [0x20] caddr_t to (8 bytes) ← struct sockaddr* in user space |
| 569 | * [0x28] int tolen (4 bytes) |
| 570 | * |
| 571 | * @return 0 (pass to original) or ENETUNREACH (block). |
| 572 | */ |
| 573 | __attribute__((section(".text.hook_sendto"), noinline)) |
| 574 | int hook_sys_sendto(void *td, void *uap) |
| 575 | { |
| 576 | static volatile u64 g_shared_kaddr_sendto = 0ULL; /* PATCHED at install */ |
| 577 | |
| 578 | const volatile u8 *sh = (const volatile u8 *)g_shared_kaddr_sendto; |
| 579 | if (sh == NULL) { |
| 580 | goto sendto_call_original; |
| 581 | } |
| 582 | |
| 583 | /* Increment total call count */ |
| 584 | { |
| 585 | volatile s64 *p_calls = (volatile s64 *)(sh + SHARED_STAT_CALLS_OFF); |
| 586 | __asm__ __volatile__ ("lock xaddq %0, (%1)" |
| 587 | : "+r" ((s64){1}) : "r" (p_calls) : "memory"); |
| 588 | } |
| 589 | |
| 590 | /* Quick PID check */ |
| 591 | s32 zftpd_pid; |
| 592 | u32 td_proc_off, proc_pid_off; |
| 593 | __builtin_memcpy(&zftpd_pid, sh + SHARED_ZFTPD_PID_OFF, sizeof(s32)); |
| 594 | __builtin_memcpy(&td_proc_off, sh + SHARED_TD_PROC_OFF, sizeof(u32)); |
| 595 | __builtin_memcpy(&proc_pid_off, sh + SHARED_PROC_PID_OFF, sizeof(u32)); |
| 596 | |
| 597 | void *td_proc = NULL; |
| 598 | __builtin_memcpy(&td_proc, (const u8 *)td + td_proc_off, sizeof(void *)); |
| 599 | if (td_proc != NULL) { |
| 600 | s32 caller_pid = -1; |
| 601 | __builtin_memcpy(&caller_pid, (const u8 *)td_proc + proc_pid_off, sizeof(s32)); |
| 602 | if (caller_pid == zftpd_pid) { |
| 603 | volatile s64 *p = (volatile s64 *)(sh + SHARED_STAT_SELF_OFF); |
| 604 | __asm__ __volatile__ ("lock xaddq %0, (%1)" |
| 605 | : "+r" ((s64){1}) : "r" (p) : "memory"); |
| 606 | goto sendto_call_original; |
| 607 | } |
| 608 | } |
| 609 | |
| 610 | /* Extract the 'to' argument (struct sockaddr *) at uap+0x20 */ |
| 611 | void *sockaddr_uptr = NULL; |
| 612 | __builtin_memcpy(&sockaddr_uptr, (const u8 *)uap + 0x20U, sizeof(void *)); |
| 613 | |
| 614 | if (sockaddr_uptr == NULL) { |
| 615 | goto sendto_call_original; |
| 616 | } |
| 617 | |
| 618 | /* Same sleep-safety check as in hook_sys_connect — see comment there. */ |
| 619 | { |
| 620 | const u8 *td_bytes = (const u8 *)td; |
| 621 | u32 critnest = 0U; |
| 622 | s32 locks = 0; |
| 623 | __builtin_memcpy(&critnest, td_bytes + 0x1CCU, sizeof(u32)); |
| 624 | __builtin_memcpy(&locks, td_bytes + 0x1D0U, sizeof(s32)); |
| 625 | if ((critnest != 0U) || (locks != 0)) { |
| 626 | goto sendto_call_original; |
| 627 | } |
| 628 | } |
| 629 | |
| 630 | u8 sa_buf[8] = {0}; |
| 631 | if (copyin(sockaddr_uptr, sa_buf, sizeof(sa_buf)) != 0) { |
| 632 | goto sendto_call_original; |
| 633 | } |
| 634 | |
| 635 | u16 sa_family; |
| 636 | __builtin_memcpy(&sa_family, sa_buf + SIN_FAMILY_OFFSET, sizeof(u16)); |
| 637 | if (sa_family != (u16)AF_INET) { |
| 638 | goto sendto_allow_other; |
| 639 | } |
| 640 | |
| 641 | u32 dest_ip; |
| 642 | __builtin_memcpy(&dest_ip, sa_buf + SIN_ADDR_OFFSET, sizeof(u32)); |
| 643 | |
| 644 | if (is_local_address(dest_ip) != 0) { |
| 645 | volatile s64 *p = (volatile s64 *)(sh + SHARED_STAT_LOCAL_OFF); |
| 646 | __asm__ __volatile__ ("lock xaddq %0, (%1)" |
| 647 | : "+r" ((s64){1}) : "r" (p) : "memory"); |
| 648 | goto sendto_call_original; |
| 649 | } |
| 650 | |
| 651 | u32 rule_count; |
| 652 | __builtin_memcpy(&rule_count, sh + SHARED_RULE_COUNT_OFF, sizeof(u32)); |
| 653 | const u64 *rules_ptr = (const u64 *)(sh + SHARED_RULES_OFF); |
| 654 | |
| 655 | if (ip_matches_blocklist(dest_ip, rules_ptr, rule_count) != 0) { |
| 656 | volatile s64 *p = (volatile s64 *)(sh + SHARED_STAT_BLOCKED_OFF); |
| 657 | __asm__ __volatile__ ("lock xaddq %0, (%1)" |
| 658 | : "+r" ((s64){1}) : "r" (p) : "memory"); |
| 659 | return ENETUNREACH; |
| 660 | } |
| 661 | |
| 662 | sendto_allow_other: |
| 663 | { |
| 664 | volatile s64 *p = (volatile s64 *)(sh + SHARED_STAT_OTHER_OFF); |
| 665 | __asm__ __volatile__ ("lock xaddq %0, (%1)" |
| 666 | : "+r" ((s64){1}) : "r" (p) : "memory"); |
| 667 | } |
| 668 | |
| 669 | sendto_call_original: |
| 670 | { |
| 671 | uintptr_t orig_fn = 0U; |
| 672 | __builtin_memcpy(&orig_fn, |
| 673 | (const void *)((u64)sh + SHARED_ORIGINAL_SENDTO_OFF), |
| 674 | sizeof(uintptr_t)); |
| 675 | if (orig_fn == 0U) { |
| 676 | return 0; /* sendto hook not installed — passthrough */ |
| 677 | } |
| 678 | typedef int (*sy_call_fn)(void *, void *); |
| 679 | return ((sy_call_fn)orig_fn)(td, uap); |
| 680 | } |
| 681 | } |
| 682 | |
| 683 | #endif /* PS5_HOOK_BUILD */ |
| 684 |