DF-2984 / verdict.json
{ "finding_id": "DF-2984", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "verdict": "syscall_deregister() (kern_syscalls.c:75-76) writes sysent[*offset] = *old_sysent with no bounds check on the index (NO_SYSCALL == -1 passes the old 'if (*offset)' test) and no proof that a successful syscall_register() established the pair. The MOD_LOAD-failure rollback in module_register_init() (kern_module.c:112 module_unload -> MOD_UNLOAD -> syscall_module_handler kern_syscalls.c:99-106) reaches this sink with a never-initialized (offset, old_sysent). Verified on the guest: two legitimate fixed-slot-210 KLDs; loading the second fails syscall_register with EEXIST yet kldload(2) returns 0, and the rollback executes sysent[210] = {0,0,NULL,NULL}, destroying the first module's LIVE syscall; every subsequent unprivileged syscall(210) (verified as uid 1001) performs a kernel NULL function call - deterministic 'Fatal trap 12 ... instruction pointer = 0x8:0x0' panic persistent until reboot. The EINVAL variant (fixed offset 99999) was likewise driven to the sink (MOD_LOAD error 22, kldload rc=0), executing sysent[99999]=zeros ~2.3MB past the table (silent on that boot: target was already-zero .bss); ENFILE gives sysent[-1] identically. Unprivileged ceiling is persistent panic: the written value is the module's static zero initializer, and controlling it (or the OOB index) requires authoring a KLD, i.e. root via SYSCAP_NOKLD. fix.diff (bounds re-validation + registered gate + proper rollback on chainevh failure) applied in-guest, kernel rebuilt: same steps leave slot 210 live (call210 still returns 4242, no panic), modC writes nothing, guest healthy.", "exploit_chain": "root one-time setup: kldload legitimate module A (fixed slot 210) -> kldload legitimate module B (same fixed slot): syscall_register EEXIST -> MOD_LOAD fails -> kern_module.c:112 module_unload dispatches MOD_UNLOAD -> syscall_module_handler:105 syscall_deregister(&210, &{0,0,NULL,NULL}) -> sysent[210].sy_call = NULL (kern_syscalls.c:76) while kldload(B) returns 0. Then ANY unprivileged user: syscall(210) -> trap.c:1224 callp=&sysent[210], :1235 narg=0, :1285 (*NULL)(&sysmsg, argp) -> Fatal trap 12, RIP=0, reboot required. No uid0 route for unprivileged users: written value is fixed zeros from stock module initializers; controlling value/index requires module authorship = root.", "evidence": [ "panic.txt: 'Fatal user address access from kernel mode from call210 at 0000000000000000' / 'instruction pointer = 0x8:0x0' after unprivileged syscall(210) as uid 1001", "run.log: KLDLOAD_A_RC=0 ... KLDLOAD_B_RC=0 with dmesg 'module_register_init: MOD_LOAD (dfpocb, ...) error 17' (EEXIST rollback fired, kldload still reports success)", "run.modc.log: 'MOD_LOAD (dfpocc, ...) error 22' (EINVAL variant reached the same sink; kldload rc=0)", "run.fix.log: on fix.diff kernel #1, same kldload B produces 'error 17' but unprivileged call210 'survived: syscall(210) returned 4242' (twice, plus after modC load), guest up", "VERDICT.md: full narrative + exploitability-ceiling analysis" ], "kernel_refs": [ "sys/kern/kern_syscalls.c:72-78", "sys/kern/kern_syscalls.c:99-106", "sys/kern/kern_module.c:110-116", "sys/kern/kern_linker.c:342-351", "sys/platform/pc64/x86_64/trap.c:1219-1224", "sys/platform/pc64/x86_64/trap.c:1235", "sys/platform/pc64/x86_64/trap.c:1285", "sys/sys/sysent.h:84-94" ], "poc_changes": "Seed lead assumed only 'root-loaded modules with fixed offsets' could mis-index. Deepened: the reachable path is the MOD_LOAD-failure rollback (kern_module.c:112), so NO module bug is needed - two legitimate fixed-offset KLDs conflicting (EEXIST) corrupts a live slot; ENFILE(*offset=-1)/EINVAL(arbitrary index) variants added. Stock SYSCALL_MODULE macro is bit-rotted ({0,NULL} vs 4-field sysent, -Werror), so the PoC hand-rolls the exact macro expansion to reach syscall_module_handler. call210 uses plain syscall(210); run.sh switched sudo->run_user (no sudo in guest).", "attempts": 3, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "runtime_sec": 1260, "guest_dirty": 0, "build_cmd": "sh /tmp/df2984/build.sh (make -C modA; make -C modB; make -C modC against /usr/obj/usr/src/sys/X86_64_GENERIC; cc call210.c)", "run_cmd": "root: sh /tmp/df2984/run.sh ; then unprivileged: /tmp/df2984/call210", "code_hash": "556912c4fec0dedbf71c7e10b30ff04682885907b2a0bcb97328f194271fc5f2", "recommended_fix": "Re-validate the index in syscall_deregister (reject <=0 or >=SYS_MAXSYSCALL) and gate deregistration on a 'registered' flag set only after a successful syscall_register (also roll back the registration when the module's chainevh init fails).", "fix_status": "fixed", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Fri Sep 4 17:32:24 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "bounds re-validation in syscall_deregister plus a registered flag in syscall_module_data gating MOD_UNLOAD deregistration (and rolling back the registration when the chainevh init fails) removes the unvalidated write entirely; the exact baseline panic input (unprivileged call210 after kldload A+B) is inert on the patched kernel (call210 still returns the live marker 4242, also after loading the out-of-range modC), guest stays up; kldload success-misreporting is DF-2936 and intentionally unchanged", "fix_evidence": [ "fix_run.log (patched-kernel sequence: error 17 + error 22 rollbacks leave slot live)", "VERDICT.md fix-validation section (kernel #1 uname, KBUILD_RC=0, IK_RC=0)" ], "notes": "fix_status=fixed. fix_kernel_uname='DragonFly dfbsd 6.5-DEVELOPMENT #1: Fri Sep 4 17:32:24 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64'. fix_baseline_reproduced=1 (panic at RIP=0 from uid 1001). fix_patched_reproduced=0 (call210 returns 4242 after kldload B and C; guest up). fix_verdict='bounds re-validation in syscall_deregister plus a registered flag in syscall_module_data gating MOD_UNLOAD deregistration (and rolling back the registration when chainevh init fails) removes the unvalidated write entirely; baseline panic input is inert on the patched kernel with no visible regression (kldload-success mis-reporting is DF-2936 and intentionally unchanged)'. fix_evidence=['run.fix.log','/tmp/kbuild.log in-guest (KBUILD_RC=0)','kernel #1 uname in VERDICT.md']. Guest reset with-src after validation. Related: DF-2936 (same rollback machinery, module-registry aspect), DF-2966/DF-2918 (registry discipline family)." } |