Seregon/zftpd

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

C/11.0 KB/No license
src/ps5_net_filter_hook.c
zftpd / src / ps5_net_filter_hook.c
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 
114typedef unsigned char u8;
115typedef unsigned short u16;
116typedef unsigned int u32;
117typedef unsigned long long u64;
118typedef signed int s32;
119typedef 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 */
171extern 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 */
177extern 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 */
201static inline __attribute__((always_inline))
202int 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 */
271static inline __attribute__((always_inline))
272int 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))
339int 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 
513allow_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 
521allow_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 
528call_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))
574int 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 
662sendto_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 
669sendto_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