DF-3311 / verdict.json
{ "finding_id": "DF-3311", "status": "inconclusive", "reproduced": 0, "impact": "dos", "confidence": "likely", "verdict": "Code defect certain: wg_down() cancels/drains the handshake and encrypt/decrypt tasks and purges their queues (if_wg.c:2763-2786) while the UDP sockets are still open (closed only at :2795), so an unprivileged peers packet driving wg_upcall()->wg_input() can enqueue a handshake packet and re-enqueue sc_handshake_task (if_wg.c:2166-2170; WG_PKT_IS_* checks need no valid crypto) AFTER the cancel; wg_clone_destroy() then kfrees the task arrays (:2883-2884), lockuninits the queues (:2885-2887) and kfrees the softc (:2917) that the late tasks callback argument points to. Dynamically: the FIRST race run (root create/up/destroy churn + nobody-spammed 148/92/64-byte packets on lo0) hard-wedged the guest within 45 s (ssh dead, vm down, QEMU alive, no panic text, no /var/crash dump - silent hang needing force-kill); 3 clean controls and 4 subsequent race reruns (~8000 iterations incl. a 12-peer widened variant) stayed up, so the wedge could not be repeated and full reproduced is not honestly claimable. Realistic ceiling: fatal local DoS raced between unprivileged packet input and a privileged config op; no path to uid=0 pursued (write content not attacker-chosen, window is teardown-only).", "exploit_chain": "", "evidence": [ "findings/poc/DF-3311/VERDICT.md (interleaving diagram + 8-run campaign table)", "findings/poc/DF-3311/run.log (the wedge run #1: status down, boot.log silent, /var/crash empty)", "findings/poc/DF-3311/run.2.log run.3.log (controls clean; reruns 1 wedge / 5 races)", "findings/poc/DF-3311/fix.diff (close sockets before cancelling tasks in wg_down)" ], "kernel_refs": [ "sys/net/wg/if_wg.c:2163", "sys/net/wg/if_wg.c:2166", "sys/net/wg/if_wg.c:2170", "sys/net/wg/if_wg.c:2189", "sys/net/wg/if_wg.c:2763", "sys/net/wg/if_wg.c:2795", "sys/net/wg/if_wg.c:2883", "sys/net/wg/if_wg.c:2885", "sys/net/wg/if_wg.c:2917" ], "poc_changes": "No wg(8) on guest: wgtool.c issues SIOCSWG directly (byte-exact struct layout from if_wg.h); spam.c sends type-1/2/3 packets whose type word + exact length (148/92/64B) pass WG_PKT_IS_* with zero valid crypto - the enqueue path never validates MACs before queueing, which is what makes the unprivileged side of this race possible.", "attempts": 5, "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 -o wgtool wgtool.c && cc -O2 -o spam spam.c", "run_cmd": "su -m nobody -c ./spam 127.0.0.1 51820 0 tag & ; sh raceA.sh 3000", "code_hash": "5d0bd6ccf1bd41c2cc575ff036deb71aabb2bac3c0c04565acdd9f14d96c7905", "notes": "One silent hard wedge in 5 race runs vs 3 clean controls; no crash dump available (wedge, not panic). Peer-lifetime half of teardown is known DF-0315; this finding covers the softc/task-queue half. Guest left clean (vm.sh reset with-src).", "recommended_fix": "In wg_down(), call wg_socket_uninit() FIRST (before any taskqueue_cancel/purge) so no new wg_input() can re-enqueue tasks/queues being torn down; a complete fix additionally needs a softc reference across wg_input() (see fix.diff).", "fix_status": "not_testable", "fix_kernel_uname": "", "fix_baseline_reproduced": 0, "fix_patched_reproduced": 0, "fix_verdict": "Wedge occurred only in run #1 and never recurred on the unpatched kernel afterwards, so a baseline/patched A/B could not be driven; fix is by-inspection (reorders wg_socket_uninit() ahead of all cancels/purges).", "fix_evidence": "findings/poc/DF-3311/fix.diff" } |