DF-5739 / verdict.json
{ "finding_id": "DF-5739", "status": "reproduced", "reproduced": true, "impact": "panic", "confidence": "likely", "verdict": "Link teardown is unsynchronized across execution contexts: hci_acl_timeout() (softclock thread, hci_link.c:199-243) frees refcnt-0 links, event handlers (netisr) free the same links by handle, and hci_acl_recv() (netisr) holds uncounted references across reassembly writes. hci_link_free()s deferral guard (callout_stop + callout_active, hci_link.c:1005-1007) is dead code on DragonFly callouts - callout_stop() clears CALLOUT_ACTIVE unconditionally before blocking (kern_timeout.c:861-862) - so two hci_link_free() bodies interleave freely on one link. Demonstrated on the guest with the verbatim-production KLD rig: incoming-connection links (hci_acl_newconn, refcnt 0, expiry callout armed - attacker-seeded via Connection_Request) raced against Disconnect_Complete floods produced panic: memchunk 0xfffff8011780adc0 is already freed! with the trace through kfree() <- hci_link_free+0x2c6 (the kfree(link) at hci_link.c:1024) - a cross-thread double free. The concurrent hcih0: dropped incomplete ACL packet prints prove hci_acl_recv() was live on the same link population. Race is probabilistic (first hit on the first full ~90 s pass; 3 later passes survived). fix.diff (atomic exactly-once teardown guard + callout_drain/terminate) prevented the double-kfree recurrence in the patched runs, but the same stress still crashed via a sibling unsynchronized path (QUEUEDEBUG Bad link elm from hci_link_alloc insert vs hci_link_free remove) - fix is partial; full remediation requires serializing the netbt link lifecycle (subsystem lock/serializer + reference counting for handle-lookup users).", "exploit_chain": "hostile controller sends Connection_Request (creates refcnt-0 WAIT_CONNECT link with 10 s expiry callout, hci_link.c:191-193) then supplies Disconnect_Complete for handle 0 timed against the expiry (hostile transport: at will; real world: probabilistic) -> softclock hci_acl_timeout and netisr hci_event_discon_compl both enter hci_link_free() on the same link -> double m_freem(hl_rxp/txq), double TAILQ_REMOVE, double kfree(link) (demonstrated allocator panic); alternatively the timer frees the link while netisr hci_acl_recv() is between lookup_handle (:451) and its hl_rxp writes (:483, :495-498) -> UAF write of an mbuf pointer into freed/reclaimed 168-byte M_BLUETOOTH memory. Credible UAF-write platform with bluetooth-slab grooming; not pushed to uid=0 (no Bluetooth hardware; KLD rig demonstrates the primitive).", "evidence": "panic.txt: raw serial console with the interleaved panic printer - de-interleaved: panic: memchunk 0xfffff8011780adc0 is already freed! + trace chunk_mark_free+0xfd <- chunk_free <- kfree() at kmalloc+0x97 <- hci_link_free+0x2c6 while hcih0: dropped incomplete ACL packet (production hci_acl_recv) prints concurrently. run.log: de-interleaved transcript. fix_panic_patched.txt + fix_run.log: patched-build pass-1 clean (0 already freed), pass-2 QUEUEDEBUG list-corruption panic (sibling race).", "kernel_refs": [ "sys/netbt/hci_link.c:199", "sys/netbt/hci_link.c:213", "sys/netbt/hci_link.c:191", "sys/netbt/hci_link.c:451", "sys/netbt/hci_link.c:483", "sys/netbt/hci_link.c:930", "sys/netbt/hci_link.c:1005", "sys/netbt/hci_link.c:1024", "sys/netbt/hci_event.c:629", "sys/kern/kern_timeout.c:861" ], "poc_changes": "Reused the DF-5733/5734 rig; added three race sysctls driving production functions only: hlh_race_arm (hci_acl_newconn + production callout_reset re-arm to 1 tick + one queued PDU), hlh_race_discon (discon_compl(handle 0) flood via hci_input_event), hlh_race_frag (ACL START/FRAGMENT flood on handle 0 via hci_input_acl). Fake device_t is a mapped blob with a valid nameunit string because the production error kprintfs (hci_acl_recv:473) dereference unit->hci_dev during the flood.", "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": 2100, "guest_dirty": 0, "build_cmd": "cd /root/hcih && sh build.sh (verbatim production netbt sources, md5s recorded)", "run_cmd": "kldload /root/hcih/hci_link_harness.ko; sysctl hw.hlh_setup=1; (sysctl hw.hlh_race_discon=100000 &); (sysctl hw.hlh_race_frag=50000 &); sleep 1; sysctl hw.hlh_race_arm=100000", "code_hash": "4080aafc01c62ecac3a037fa10265fafa88d1bfaef8f19b8f1a2b11eca666754", "notes": "Probabilistic race: 1 hit on the first full trio pass (~90 s), 3 subsequent passes survived - expected for a whole-body interleaving window. Sanity arm=5 without the flood is clean (timers free links correctly when uncontended). Distinct from DF-5737 (syscall-tx vs event; this is softclock-timer vs event/rx with double-hci_link_free sink). fix_status inconclusive: the specific double-kfree did not recur patched, but the same stress still crashes through the equally-unlocked link-list mutation (hci_link_alloc INSERT vs hci_link_free REMOVE) - documented in fix_run.log; upstream should serialize the whole link lifecycle, not just the free.", "recommended_fix": "Serialize netbt link teardown (subsystem lock or lwkt serializer around link alloc/free/lookup/use) + reference counting for handle-based users; interim local hardening in fix.diff (exactly-once atomic guard in hci_link_free/hci_acl_timeout, callout_drain+callout_terminate replacing the dead callout_active deferral).", "fix_status": "inconclusive", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 1, "fix_verdict": "PARTIAL. fix.diffs exactly-once atomic teardown guard eliminated the demonstrated double-kfree (patched pass 1: full race stress, 0 already freed panics, guest alive). Pass 2 (larger counts) still crashed with panic: Bad link elm ... next->prev != elm (QUEUEDEBUG) - a sibling manifestation of the same missing serialization (hci_link_alloc TAILQ_INSERT_HEAD vs hci_link_free TAILQ_REMOVE on unit->hci_links), which a local exactly-once guard cannot fix. Full remediation requires the subsystem-level lock/refcount redesign; fix_evidence: fix_run.log, fix_panic_patched.txt.", "fix_evidence": "fix_run.log; fix_panic_patched.txt; fix.diff" } |