DF-3203 / verdict.json
{ "finding_id": "DF-3203", "status": "reproduced", "reproduced": 1, "impact": "leak", "confidence": "certain", "verdict": "The pre-pullup accept-status read at krpc_subr.c:391 was reproduced on the guest through the verbatim production code: a 16-byte accepted RPC reply (bytes 16..23 do not exist on the wire) passes the dead residual-length gate at :366-370 (sbinit zeroes sb_cc and soreceive counts delivered bytes into it, so the old math yields 65536-minus-received, never below 16 for UDP) and the m_len>=16 check at :372, after which the code reads rp_status at struct offset 20..23 before the m_pullup(24) at :417 -- 4 bytes of stale kernel-heap mbuf content. The verbatim kprintf at :397 prints that stale value to console and dmesg, and the value steers control flow. Attribution is deterministic: a userland-only primer planting 0x5a5a5a5a at mbuf offset 20..23 produced status=1515870810 on 9/9 attempts and 0x3cc33cc3 produced status=1019428035 on 9/9; a kernel-side 0xc3 groom hit once with exactly (int)0xc3c3c3c3; unplanted runs disclosed varying residue (943337777 = ASCII 8:11, 153789694, 357452694); stale==0 attempts aborted with ENOBUFS at :417-419; a primer of 0x00000002 made 3/3 calls take the RPC_PROGMISMATCH exit returning EBADRPC with exactly +3 leaked mbufs (the DF-3204 path driven purely by the uninitialized value). No panic (the read stays inside the mbuf allocation), so the disclosed artifact is canary_dump.txt. Not a local unpriv->root vector: krpc has no runtime callers on a disk-root guest; reachability needs the DF-3205 boot-network attacker or a misbehaving server. The validated fix (use the received sb_cc length, require the full 24-byte header, m_pullup before any field read) silenced all short-reply prints (0 verbatim rpc denied lines on the fixed boot) with no regression.", "exploit_chain": "boot-network attacker (DF-3205 delivery) sends a 16-byte accepted reply with the predicted xid; verbatim krpc_call reads 4 stale heap bytes past the datagram as rp_status; the value is printed to the console and serial boot log (kernel heap disclosure) and selects the control-flow branch (print+continue, ENOBUFS boot-RPC abort, or the DF-3204 mbuf-leak exit when the stale value is 2); with the primer spray the attacker chooses the exact printed value", "evidence": "run.attrib.log + primer_leak_sample.txt: rpc denied, status=1515870810 x9 and status=1019428035 x9, each exactly when its primer pattern was sprayed; run.couple.log: 3/3 err=EBADRPC with mbufs 7->10 for primer 0x00000002; run.2.log: -1010580541 (0xc3c3c3c3 groom hit) and ENOBUFS attempts; canary_dump.txt: all distinct stale values across runs; run.fixed.log + fixed-boot dmesg: strict grep ^rpc denied, status= returns 0", "kernel_refs": ["sys/vfs/nfs/krpc_subr.c:366", "sys/vfs/nfs/krpc_subr.c:370-373", "sys/vfs/nfs/krpc_subr.c:391", "sys/vfs/nfs/krpc_subr.c:397", "sys/vfs/nfs/krpc_subr.c:417-419", "sys/sys/sockbuf.h:115-126", "sys/kern/uipc_socket.c:1277"], "poc_changes": "seed sketch assumed a root-on-NFS boot lab; replaced with the established verbatim-interposition kernel-compiled rig driving the real UDP stack with a userland attacker; attribution required usched :1 CPU pinning (per-CPU objcache magazines) and, for the deterministic 18/18 result, a userland primer spray (24-byte datagrams to a dead port carrying the pattern at offset 20..23) so the attacker alone plants the bytes the kernel later reads out of bounds; harness marker text prints the expected value while the authoritative observed value is the column-0 dmesg line captured by the scripts", "attempts": 9, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #1: Tue Oct 20 17:57:48 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (baseline decisive boot; fixed decisive boot #2: Tue Oct 20 18:25:39 UTC 2026)", "runtime_sec": 210, "guest_dirty": 0, "build_cmd": "sh build.sh (fixed variant: sh build.sh af834f6e30225b1af68cc83cbcdaf25e29e0e39ca6deaec6f3c13663a661b886)", "run_cmd": "sh attrib.sh ; sh run.sh ; sh run.couple sequence in attrib/couple logs ; fix A/B: sh run_fixed.sh", "code_hash": "7e1e3ce06a6846a1b942d9d9f0098be5685fe36980db2620828f691ffb429520 (dfrig13_krpc.c) ; d7337b8717b8d4edb990f51ff612ea099085970756a8314fbbe9ab328c0507f1 (attacker.c)", "notes": "leak magnitude is 4 stale kernel-heap bytes per triggering reply, disclosed as a decimal integer on console/dmesg/serial boot log; residue included readable ASCII fragments (8:11, 8:13). Guest reset with-src performed after the run. Fixed source sha256 af834f6e30225b1af68cc83cbcdaf25e29e0e39ca6deaec6f3c13663a661b886 (fix_all.diff covers all three krpc findings; the DF-3203 hunks are line-disjoint from the other two).", "recommended_fix": "use the received length (len = (int)sio.sb_cc), require the full sizeof(struct rpc_reply) before any field access, and m_pullup the header before parsing (validated in fix.diff)" } |