VERDICT — DF-2898 (sys/kern/init_sysent.c pass 2) ================================================= status: reproduced impact: panic (unprivileged, deterministic, local DoS) confidence: certain guest: DragonFly dfbsd 6.5-DEVELOPMENT #0 X86_64_GENERIC (stock INVARIANTS) source tree bit-identical to the audited repo (md5 trap.c fa54899450dc815d52a503319315154e, init_sysent.c a58e1fe6a06d2acce88097d691da272d) WHAT REPRODUCED --------------- `syscall(SYS_syscall, 0)` — a single unprivileged libc call — panics the stock kernel with a double fault on the kernel-stack guard page. Observed five times on three fresh boots (sysent_edge stage 5 twice, sysent_var d, libcind p, probe 4), always with identical signature: DOUBLE FAULT - KERNEL STACK GUARD HIT! rip = sys_xsyscall+0x84 rsp = rbp = rsp+0x48 panic: double fault ROOT CAUSE (path:line) ---------------------- - sys/kern/init_sysent.c:17 — sysent[0] (`syscall`) → sys_xsyscall - sys/kern/init_sysent.c:215 — sysent[198] (`__syscall`) → sys_xsyscall - sys/platform/pc64/x86_64/trap.c:1402-1406 — code = (u_int)frame->tf_rdi; if (code >= p->p_sysent->sv_size) code = SYS___nosys; callp = &p->p_sysent->sv_table[code]; … no rejection of code ∈ {SYS_syscall, SYS___syscall}; trap.c:1445 then calls callp->sy_call == sys_xsyscall again with the same trapframe. Identical code exists in sys/platform/vkernel64/x86_64/trap.c (sys_xsyscall, same shape). - libc contract that feeds it: lib/libc/x86_64/SYS.h KERNCALL stubs — `mov $198,%rax; syscall` with the number left in RDI (disassembled from /usr/lib/libc.a syscall.o/__syscall.o in the guest; kernel-side instrumentation independently observed tf_rax=198 with the caller's number in tf_rdi). sysent[0]/[198] are the only self-referential entries in the entire table, so the recursion is exclusive to the indirect gateway this file defines. INVESTIGATION PATH (why this took several rounds) ------------------------------------------------- 1. First crash: edge-case harness (OOR numbers, obsolete slots, 7-arg mmap, indirect mmap). Suspected: nargs/extargs copyin. Refuted by machine cross-check (sysent_xcheck.py): every AS(x_args) struct exists, is a sysunion member, max narg (7, mmap) == union capacity (7) — the extargs copyin cannot overflow; 556 rows == SYS_MAXSYSCALL == syscallnames[]; no NULL sy_call; all literal-0 entries have dummy-only args structs. 2. Suspected kernel-stack-pointer leak (delayed crashes). Refuted by the instrumented kernel: entry rsp/td_pcb/td_kstack identical and healthy on every indirect syscall; ~10.5KB consumed strictly inside one dispatch ⇒ recursion. 3. Raw-asm variants: two of my own stubs had a register-allocation bug (input allocated to RAX, clobbered by `mov $0,%eax` → rdi=0) which accidentally took the same kernel path — recognized and replaced with explicit-register asm (probe.c). The libc-based crashers (stage 5, probe 4/5/6/7) never involved my asm. 4. `ps` at DDB named the faulting thread = the unprivileged user process. EXPLOIT CEILING --------------- Stack-exhaustion recursion writing only return addresses into the guard page. No controlled data reaches past the guard; nothing corruptible escapes. Ceiling = kernel panic (all-user availability loss). No uid=0 path exists for this primitive. FIX VALIDATION -------------- fix.diff (2 lines, pc64 + vkernel64): remap code∈{0,198} to SYS___nosys, mirroring the adjacent out-of-range remap. Rebuilt in-guest (make nativekernel, kernel #1 03:11:47 UTC 2026): - baseline (stock): P2-P8 all panic (see panic-baseline.txt) - patched: P2-P8 → SIGSYS "Bad system call", guest up; P1 (valid indirect getpid, rax=0/rdi=20) still returns the pid — no regression of the indirect gateway. fix_status: fixed. PASS-2 NEGATIVE RESULT (machine-verified, gives the clean parts weight) ----------------------------------------------------------------------- - nargs vs args-struct words: 556/556 consistent (AS() computed from the same struct at compile time; every referenced struct exists in sysproto.h and as a union sysunion member; max narg 7 == union capacity 7 ⇒ trap.c:1415-1437 copyin can never overflow sysmsg.extargs). - bounds: SYS_MAXSYSCALL 556 == sysent rows == syscallnames[] rows; dispatch clamps `code >= sv_size` (trap.c:1220,1403); sv_size wired from SYS_MAXSYSCALL (sys/kern/init_main.c:341). - no entry has NULL sy_call; obsolete/nosys/lkm placeholders all route to sys_nosys (SIGSYS+EINVAL) or sys_lkmnosys. - all literal-0-narg entries correspond to dummy-only args structs (fork/vfork/getpid/.../lwp_gettid) — no uninitialized-args reads. - sy_rsize ∈ {4,8} only; rsize=8 impls verified to store 64-bit results (read/write/extpread/v/iommap/lseek/shmat/vmspace_pread/write/getrandom/ __realpath/sbrk → sysmsg_szresult/offset/resultp; mq_receive stores 32-bit iresult under rsize=8 — cosmetic, dispatch pre-zeroes the full 8-byte result word, msg sizes bounded « 2^31; sys_mqueue.c nit, not a table defect). - no native-table entry points into an unloadable KLD (all sy_call targets are built-in; the lkmnosys slots 210-219 mutate only via root-only kldload/ syscall_register, covered by DF-2739/2740). - new-in-pass-1 syscalls present: __realpath 551, getrandom 550, futimesat 555 … table complete through 555.