DF-3558 / verdict.json
{ "finding_id": "DF-3558", "status": "reproduced", "reproduced": 1, "impact": "leak", "confidence": "certain", "verdict": "REPRODUCED (deterministic). ngh_rcvmsg (sys/netgraph7/hole/ng_hole.c:163-168) validates only header.arglen==NG_HOOKSIZ(32) and passes (char*)msg->data to ng_findhooks strcmp (ng_base.c:1095-1107); nothing on the path checks that the arglen claim fits what was actually sent (ngc_send kmallocs len+1: ng_socket.c:261-262; ng_package_msg validates nothing: ng_base.c:3007-3027), so an under-sent control message makes msg->data stale heap and the strcmp reads out of bounds of the kmallocd buffer. Demonstrated as a heap-residue match oracle: a getstats that supplies ZERO hook-name bytes returns the real hooks stats (frames=1 octets=5) in 256/256 rounds per run across 3 runs, because the allocator hands back the groomed 64-byte slab chunk (zoneindex() 8-byte granularity, kern_slaballoc.c). The honest-length unterminated case is provably safe (hook names are NUL-terminated within 31 chars, so strcmp stops by data[31]) -- carried as the PoCs negative control (ENOENT). Root-gated (uid 1001: EPERM at socket(), caps_priv_check); module buildable/loadable but not shipped in the default kernel or netgraph7 SUBDIR. Same consumer-side pattern as DF-0610/DF-2600/DF-3496 on the DF-0534 enabler. Fix validated: socket-side arglen-vs-len check + hole-side NUL check -> 0/256, all EINVAL, well-formed stats unregressed.", "exploit_chain": "", "evidence": "run.log: RESULT oob_matches=256/256 ... stats returned for a request that supplied ZERO hook-name bytes; negative control ENOENT on the same run; run.2.log/run.3.log determinism; unpriv EPERM in run.log; fix_run.log: oob_matches=0/256 rejected=256 (EINVAL), sanity stats still frames=1 octets=5; echo_neg.log: live negative proof that a closed echo<->echo ring is inert (separate ng_echo pass-2 lead).", "kernel_refs": [ "sys/netgraph7/hole/ng_hole.c:163", "sys/netgraph7/hole/ng_hole.c:168", "sys/netgraph7/netgraph/ng_base.c:1095", "sys/netgraph7/netgraph/ng_base.c:3007", "sys/netgraph7/socket/ng_socket.c:261" ], "poc_changes": "Seed harness was written fresh for this batch (DF-3557s ui_cycle.c pattern). Iterations: (1) initial version treated sendto failure as fatal -- ng7 returns the node handlers error synchronously via sendto errno (ENOENT = no hook match), so error accounting was reworked around that; (2) response wait switched from SO_RCVTIMEO to poll(); (3) fix-run accounting added (rejected/EINVAL).", "attempts": 3, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (stock INVARIANTS)", "runtime_sec": 96, "guest_dirty": 0, "build_cmd": "sh build.sh (in-guest root: make in netgraph7/{netgraph,socket,hole,echo}; cc -O2 triggers)", "run_cmd": "sh run.sh (kldload stack; /root/poc58/echo_neg; /root/poc58/hole_oob)", "code_hash": "cee8c37ac3553c142f64849d55572cde74cd501f1d017a1ae85a490da93b4b3d", "notes": "Impact classified leak as the closest enum: an OOB read of kernel heap residue whose observable is a match/no-match oracle (control-flow, not direct byte disclosure); root-gated, non-default module -> severity Low. The ng_echo negative evidence (echo_neg) and the ng_ip_input/ng_split pass-2 negative results are documented in VERDICT.md.", "recommended_fix": "ng_socket.c ngc_send: reject messages whose mbuf chain is shorter than sizeof(struct ng_mesg)+header.arglen (EINVAL); ng_hole.c: also require msg->data[NG_HOOKSIZ-1]==\\0.", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "fixed", "fix_evidence": "fix_run.log: after vm.sh reset with-src + patch(1) of fix.diff into /usr/src + -Werror-clean rebuild of ng_socket.ko/ng_hole.ko, the exact PoC shows oob_matches=0/256 with rejected=256 (EINVAL at the socket), sanity well-formed getstats still returns frames=1/octets=5 (no regression), and echo_neg still passes." } |