DF-3539 / verdict.json
{ "finding_id": "DF-3539", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "verdict": "Unpatched: the SET_CONFIG realloc (cpu0 netisr, ngc_send->ng_send_msg direct dispatch) frees sc->sbuf/sc->abuf while nga_rcv_async/nga_rcv_sync run unlocked in ksocket-upcall ip-input context on other CPUs; the corrupted state then defeats the SYNC_BUF_SIZE(cfg.amru) guard and the decode loop writes attacker bytes without bound past the 8KB sbuf (QEMU-monitor proof: CPU0 spinning in nga_rcv_async with sc->slen=0x5655=22101 vs allocation <=8202; two additional silent hard lockups <20s each). Patched with fix.diff (per-byte spinlock consistency for (cfg,buf,slen), serialized SET_CONFIG, retired frees after unlock): 10-minute soak of the identical trigger plus cycle-mode storm survived with the guest alive and responsive. Root-gated trigger (v1 control socket requires SYSCAP_RESTRICTEDROOT) - Medium severity, memcorrupt bucket.", "exploit_chain": "root -> ng control socket -> graph [S]-sync-[async]-async-[ksocket udp] -> 3x UDP flooders drive the decode loop in ip-input context on CPUs 1-5 -> concurrent SET_CONFIG amru 8191<->8192 storm executes kfree(sc->sbuf) mid-decode -> write-after-free corrupts ng_async_private (cfg.amru/slen) -> guard passes with bogus limit -> sc->sbuf[sc->slen++] walks 14KB+ past the heap chunk writing 0x41/A pattern -> kernel heap flattened -> silent hard lockup (no panic banner, no dump; vm.sh DDB-heuristic kill). Escalation to uid0 not pursued: trigger itself requires root (SYSCAP_RESTRICTEDROOT on v1 ng control sockets), so there is no unprivileged entry point on a stock system.", "evidence": "df3539_regs.txt + df3539_cpu0.txt (frozen-vCPU registers: RIP in ng_async.ko, RDX=sc->slen=22101); VERDICT.md embeds the disassembly match of the spin site to nga_rcv_async (movq 0x28(%rbx)=sc->sbuf, movl 0x30(%rbx)=sc->slen, movzwl 0x1a(%rbx)=sc->fcs/PPP_FCS); console_run2.log (silent lockup #2); fix.diff + 10-min patched soak", "kernel_refs": [ "sys/netgraph/async/ng_async.c:317", "sys/netgraph/async/ng_async.c:325", "sys/netgraph/async/ng_async.c:339", "sys/netgraph/async/ng_async.c:595", "sys/netgraph/async/ng_async.c:600", "sys/netgraph/netgraph/ng_base.c:1226", "sys/netgraph/netgraph/ng_base.c:1687", "sys/netgraph/socket/ng_socket.c:257", "sys/netgraph/socket/ng_socket.c:400", "sys/netgraph/ksocket/ng_ksocket.c:605" ], "poc_changes": "Rewrote the seed as a fork()-based harness (race2.c): control driver in one process at 70k msg/s + separate UDP flood processes (the pthread prototype crawled at ~45 msg/s due to a userspace pathology); per-run unique node names and UDP ports (stale graphs from earlier runs otherwise collide); v1 quirks handled: NG_CONTROL protocol number is 2 (inverted vs ng7), absolute node paths need a trailing colon, sockaddr sa_len must be set, ksocket address is encoded in the hook name (inet/dgram/udp), ng_ksocket delivers via socket upcall so rcvdata runs in ip-input context rather than cpu0 netisr.", "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": 1450, "guest_dirty": 0, "build_cmd": "cc -O2 -Wall -o /root/race2 /root/race2.c", "run_cmd": "/root/race2 all config (kldload netgraph ng_socket ng_async ng_ksocket first)", "code_hash": "bd82ca472a1ba4a0869bc27b159e2388babe096c40aa10ce91678d501329fd19", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "Baseline unpatched: 2x silent hard lockup <20s + 1x captured wedge with register proof of the unbounded overflow. Patched (module rebuilt from fix.diff, /boot/kernel/ng_async.ko swapped): identical trigger survived a 10-minute soak (alive at 2/4/6/8/10 min checks, ssh responsive, mbufs stable) plus a create/destroy cycle-mode storm; bad behavior gone.", "fix_evidence": "fix.diff (git-apply-clean against sys/netgraph/async/ng_async.c); patched module build log in session; soak timings in VERDICT.md", "notes": "Impact classified panic (machine death without banner); the underlying primitive is unbounded kernel heap corruption - memcorrupt bucket. Residual: nga_shutdown frees sc/node while in-flight rcvdata may hold it - v1-core-wide lifetime issue, documented in VERDICT.md, out of scope for a per-node lock. vm.sh gained a QEMU monitor unix socket (boot-mon.sock) this session for wedged-machine forensics.", "recommended_fix": "Serialize ng_async control-path mutation against the data path: per-node spinlock making the decode guard+write and every abuf write atomic vs SET_CONFIGs realloc/cfg swap, with retired buffers freed only after unlock (see fix.diff)." } |