β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5739

Expiry-callout teardown race: hci_acl_timeout() (softclock) frees refcnt-0 links concurrently with event-context hci_link_free()/hci_acl_recv() (netisr) - dead callout_active() deferral lets two hci_link_free() bodies interleave: double kfree demonstrated ('memchunk already freed' panic)

Summary

netbt has no serialization for the link lifecycle: the expiry callout (softclock, armed by an incoming connection request with refcnt 0 - attacker-seeded), the event handlers, and the rx path all reach hci_link_free/link fields concurrently; the only guard (callout_stop + callout_active) is DEAD CODE on DFly callouts (ACTIVE cleared unconditionally before the blocking wait). Guest-verified: three concurrent production loops -> 'panic: memchunk is already freed' with the trace through hci_link_free+0x2c6 (cross-thread double kfree) while rx-context prints prove concurrent liveness. Exactly-once atomic guard fix validated (double-kfree gone) but a sibling list-mutation path still crashed - partial fix, upstream lifecycle serialization required; the l2cap request purge carries the identical dead-deferral pattern.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5739 Β· 15 files
FileTypeDescriptionSize
harness.c β€” 24.2 KB view raw
hci_stub.c β€” 5.0 KB view raw
stub.h β€” 281 B view raw
Makefile β€” 530 B ↓ download
build.sh β€” 116 B view raw
run.sh β€” 215 B view raw
build.log β€” 1.0 KB view raw
run.log β€” 3.4 KB view raw
panic.txt β€” 2.2 KB view raw
fix_panic_patched.txt β€” 2.2 KB view raw
fix.diff β€” 3.0 KB view raw
fix_run.log β€” 1.9 KB view raw
env.txt β€” 732 B view raw
verdict.json β€” 6.2 KB view raw
manifest.json β€” 1.4 KB view raw

Fix verification

inconclusive
baseline reproduced→ patch + rebuild →patched reproduced

PARTIAL. fix.diff's 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_run.log; fix_panic_patched.txt; fix.diff
↓ fix.diffper-fix-DF-5739

Confirmed kernel references

Detail

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 (decisive lines)

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).

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.

Verified 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).

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).