DF-2851 / verdict.json
{ "finding_id": "DF-2851", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "verdict": "Unprivileged local user panics the stock INVARIANTS kernel (2/2 stress runs, <=2 min) by racing a signal-aborted blocking accept(2) against concurrent connection completion: soaccept_predicate reassigns msg->base.nm_so (uipc_syscalls.c:261) while the notify message is still abortable, so netmsg_so_notify_abort (uipc_msg.c:744-751) locks/lists/replies against the wrong socket, unserialized against the foreign-CPU sowakeup completion. Observed: 'panic: lwkt_reltoken: illegal release' in netmsg_so_notify_abort; the wrong-list TAILQ_REMOVE at uipc_msg.c:749 executes before the panicking release. On non-INVARIANTS kernels the same interleaving yields wrong-list TAILQ_REMOVE with stale tqe pointers plus double lwkt_replymsg (kernel heap corruption); uid=0 chain not pursued within budget on this INVARIANTS guest which asserts first. Fix validated in-guest: patched kernel survived 2x240s of the identical stress (~45k accepts, ~359k aborted notifies, 6.5M connects) with zero panics.", "exploit_chain": "socket/listen -> N threads blocked in accept(2) (stack netmsg_so_notify queued on head->so_rcv.ssb_mlist) -> signal flood (SIGUSR1, sa_flags=0) drives netmsg_so_notify_doabort/abort on the listener's netisr while -> connector flood steers connection completion onto a different netisr CPU -> soisconnected/sorwakeup(head) runs soaccept_predicate, reassigns nm_so to the accepted socket in the window before MSGF_REPLY is set -> abort path takes the accepted socket's pool token (different hash slot), passes DONE|REPLY recheck, TAILQ_REMOVEs the message from the accepted socket's ssb_mlist (never queued there) and double-replies. INVARIANTS: panic at token release (reproduced). non-INVARIANTS: corrupted ssb_mlist/tqh_last + stale-pointer unlink writes -> subsequent netmsg_so_notify inserts / sowakeup scans write through dangling pointers -> memory-corruption escalation surface.", "evidence": [ "panic.txt / panic_run2.txt: 'panic: lwkt_reltoken: illegal release' with backtrace lwkt_reltoken <- lwkt_reltoken <- netmsg_so_notify_abort+0xe9 <- netmsg_service_loop (runs #1 and #2, #2 after fresh vm.sh reset with-src)", "run.log: full transcript of the decisive run #2 (unprivileged maxx, ~2 min to panic)", "fixrun.log: post-fix 2x240s stress on kernel #1 (with fix.diff): accepts=25734/19742, eintr=226130/132855, no panic", "VERDICT.md: full interleaving analysis with path:line citations", "boot_full*.txt: complete serial console captures" ], "kernel_refs": [ "sys/kern/uipc_msg.c:744", "sys/kern/uipc_msg.c:745", "sys/kern/uipc_msg.c:746", "sys/kern/uipc_msg.c:749", "sys/kern/uipc_msg.c:751", "sys/kern/uipc_msg.c:702", "sys/kern/uipc_msg.c:710", "sys/kern/uipc_syscalls.c:261", "sys/kern/uipc_socket2.c:616", "sys/kern/uipc_socket2.c:621", "sys/kern/uipc_socket2.c:623", "sys/kern/lwkt_token.c:816" ], "poc_changes": "Original analysis PoC designed fresh for this finding (no seed). Tuned: 8 blocking acceptors, 2 killer threads (SIGUSR1, sa_flags=0 => EINTR, no SA_RESTART), 6 connector threads on 127.0.0.1, killers usleep(300us) so accepts still get to block; run detached via nohup with output files because ssh-run foreground invocations wedge when the kernel panics.", "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": 2400, "guest_dirty": 0, "build_cmd": "cc -O2 -pthread -Wall -o /tmp/accept_abort_race /tmp/accept_abort_race.c", "run_cmd": "/tmp/accept_abort_race 240 (unprivileged; panic expected <=2 min; see run.sh)", "code_hash": "1cff74cc2ec7f1724b825232c763e5f52cc202d3cd295047fc1d8e3216f2c438", "notes": "Fix keys below: baseline reproduced on stock #0 kernel (2/2 panics); patched kernel #1 (Wed Sep 2 10:31:50 UTC 2026, built with make nativekernel KERNCONF=X86_64_GENERIC after git apply fix.diff) survived 2x240s identical stress. Guest reset to clean with-src snapshot afterwards (guest_dirty=0). The reproduced INVARIANTS panic fires at the token-release that follows the wrong-ssb TAILQ_REMOVE; the non-INVARIANTS consequence (stale-pointer unlink + double reply) is established by code analysis of the same interleaving.", "recommended_fix": "Never mutate netmsg_so_notify.base.nm_so while the message is abortable: carry the accepted socket in a new nm_result output field (netmsg.h + soaccept_predicate + kern_accept) so the abort path's token/list identity is immutable.", "fix_status": "fixed", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Wed Sep 2 10:31:50 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "With fix.diff applied (nm_result output field; base.nm_so immutable for the message lifetime) the identical stress workload ran 2x240s with zero panics where the unpatched kernel panicked within 2 minutes; the abort path can no longer derive lock/list identity from a concurrently-mutated field.", "fix_evidence": [ "findings/poc/DF-2851/fixrun.log (2x240s clean runs with full counters)", "findings/poc/DF-2851/build.log (kernel build/install transcript, kernel #1)", "findings/poc/DF-2851/panic.txt, panic_run2.txt (baseline panics)" ] } |