hdaa_sysctl_reconfig: device-tree mutation inside a sysctl handler self-deadlocks the global sysctllock β permanent kernel freeze from the documented reconfiguration interface
Summary
Writing dev.hdaa.N.reconfig=1 (root-only; the standard workflow for applying new pin configurations without rebooting) never returns and freezes the entire kernel. hdaa_sysctl_reconfig calls device_delete_children(dev)/bus_generic_attach(dev) from inside the sysctl handler, but userland_sysctl() holds the caller CPU's per-CPU global sysctllock SHARED across handler execution, while pcm child (un)registration calls sysctl_unregister_oid/sysctl_add_oid -> _sysctl_xlock(), which takes EVERY CPU's gd_sysctllock EXCLUSIVE. The handler's own thread blocks waiting for its own shared hold: permanent sleep, no panic, and sysctl service poisons system-wide (ps/login/new ssh sessions hang in sequence). REPRODUCED 2/2 deterministically on a QEMU-attached intel-hda+hda-duplex (hardware-independent; any real HDA machine affected identically): write syscall never returns, by t+~120s new ssh hangs, serial console silent (deadlock, not panic); tree-neutral negative control returns instantly. Fix (taskqueue deferral + drain in detach) built as a module on-guest, swapped, and validated: trigger returns rc=0 in <1s, pcm0 detaches/re-attaches, /dev/dsp0 recreated, two consecutive reconfigs and full system health verified. Also killed: channels_handler NULL-deref, SETFRAGMENT div0 (live-probed), reconfig UAF windows, conns/ctl/as parse bounds, recursions depth-capped.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3601 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.7 KB | β raw | |
| VERDICT.md | β | 3.8 KB | β raw | |
| run.log | β | 1.2 KB | view raw | |
| fix_validation.log | β | 1.4 KB | view raw | |
| env.txt | β | 257 B | view raw | |
| boot.log.firstwedge | β | 13.3 KB | β download | |
| trigger_reconfig.sh | β | 460 B | view raw | |
| fix.diff | β | 2.2 KB | view raw | |
| verdict.json | β | 3.6 KB | view raw | |
| manifest.json | β | 1.0 KB | view raw |
DF-3601 β hdaa_sysctl_reconfig global-sysctllock self-deadlock (kernel freeze)
What
sysctl dev.hdaa.N.reconfig=1 β the documented, root-only HD Audio
reconfiguration interface β permanently deadlocks the DragonFly kernel.
No panic, no console output: the triggering thread sleeps forever holding
the per-CPU global sysctllock SHARED (plus some other CPUs' locks
EXCLUSIVE), and the system progressively wedges (sysctl, ps, login, and
finally all new ssh sessions hang).
Root cause (source)
userland_sysctl()holdsSYSCTL_SLOCK()(the caller CPU'sgd_sysctllock, SHARED) across the whole handler invocation: sys/kern/kern_sysctl.c:1572-1577.hdaa_sysctl_reconfig()(sys/dev/sound/pci/hda/hdaa.c:6474-6503) callsdevice_delete_children(dev)(hdaa.c:6492) and laterbus_generic_attach(dev)(hdaa.c:6498) from inside that handler.- pcm child detach (
hdaa_pcm_detachβpcm_unregister, sys/dev/sound/pcm/sound.c:1233-1237) and re-attach runsysctl_ctx_free()/SYSCTL_ADD_*(), i.e.sysctl_unregister_oid()/sysctl_add_oid()βSYSCTL_XLOCK()(sys/kern/kern_sysctl.c:171-175, 446). - DragonFly's
_sysctl_xlock()takes every CPU'sgd_sysctllockEXCLUSIVE (sys/kern/kern_sysctl.c:1642-1653). The handler's own thread therefore waits for its own SHARED hold β permanent self-deadlock. Other CPUs' locks already taken EXCLUSIVE poison sysctl system-wide.
Hardware-independent: reproduced on QEMU intel-hda + hda-duplex
(benign codec); the deadlock is in the sysctl/device-tree interaction,
not in codec data.
Reproduce (needs an HDA controller + snd_hda)
Guest booted with -device intel-hda -device hda-duplex:
# ./up.sh # boots the DFly guest with the HDA device vm.sh run_root 'kldload snd_hda; sleep 2; dmesg | tail -3' vm.sh run_root "sysctl 'dev.hdaa.0.config=softpcmvol'" # control: OK vm.sh run_root 'sysctl dev.hdaa.0.reconfig=1' # TRIGGER
Expected (vulnerable kernel): the sysctl write never returns; within
seconds-to-minutes sysctl/ps/new ssh sessions hang system-wide;
QEMU process stays alive; serial console silent (no panic).
Expected (patched kernel): write returns immediately (rc=0), dmesg shows
pcm0: detached then pcm0: <...> at nid 3 and 5 on hdaa0, /dev/dsp0
re-created, system fully responsive (verified: 2 consecutive reconfigs).
Files
run.logβ controlled repro #2 (timing, lock-poisoning probe)boot.log.firstwedge,boot.log.repro2β serial console of both wedgesfix.diffβ deferred-taskqueue fix (applies -p1, zero fuzz)fix_validation.logβ patched module: trigger returns, pcm re-attach, health OKverdict.json,manifest.json,VERDICT.md
DF-3601 VERDICT β hdaa_sysctl_reconfig kernel self-deadlock
Status: reproduced (deterministic, 2/2 runs) β kernel deadlock / system freeze (dos). Fix: validated on guest (module rebuild + swap + re-trigger).
Setup
Guest: DragonFly 6.5-DEVELOPMENT #0 Thu Jul 2 2026, X86_64_GENERIC,
QEMU -device intel-hda -device hda-duplex (QEMU's own benign codec,
Generic (0x1af40022)), snd_hda loaded as KLD.
Reproduction timeline (run 2, controlled β see run.log)
kldload snd_hdaβ hdac0/hdacc0/hdaa0/pcm0 attach cleanly (boot.log.repro2 tail).- Negative control:
sysctl dev.hdaa.0.config=softpcmvol(a tree-neutral hdaa sysctl write) returns instantly β proves ordinary hdaa sysctl writes are fine. - t=0 session A:
sysctl dev.hdaa.0.reconfig=1- sysctl(8) printsdev.hdaa.0.reconfig: 0(the pre-write read-back) and then the write syscall NEVER returns; session A killed by an externaltimeout 30βecho RC=$?/ trailingdatenever execute. - t+7s session B:
sysctl kern.osreldatestill works (rc=0),psworks β consistent with the per-CPU lock analysis: the wedged thread holds its own CPU'sgd_sysctllockSHARED and sleeps requesting EXCLUSIVE; CPUs whose lock it already acquired EXCLUSIVE (it iterates cpu 0..n-1) are poisoned immediately, others poison as soon as lockmgr queues the EXCLUSIVE request. - t+~120s: even new ssh connections hang (login path needs sysctl) β
vm.sh statusprobe itself timed out. QEMU process alive, serial console silent (no panic β sleeping thread, not a crash). - Run 1 (accidental, same trigger): identical outcome (boot.log.firstwedge: console output simply stops after the attach messages; ssh dead; qemu alive). 2/2 deterministic.
Why (line-level)
- kern_sysctl.c:1572-1577 β
SYSCTL_SLOCK(); error = sysctl_root(...); SYSCTL_SUNLOCK();(SHARED per-CPU global sysctllock held across the handler). - hdaa.c:6492 β handler calls
device_delete_children(dev); hdaa.c:6498bus_generic_attach(dev). - sound.c:1233-1237 β pcm child detach frees its sysctl contexts
(
sysctl_ctx_free(&d->play_sysctl_ctx)/ rec). - kern_sysctl.c:171-175 β
sysctl_unregister_oid()βSYSCTL_XLOCK(); kern_sysctl.c:1642-1653 β_sysctl_xlock()lockmgr's every CPU'sgd_sysctllockLK_EXCLUSIVE. - The same thread therefore blocks acquiring its own CPU's lock exclusively β sleeps forever; system-wide sysctl poisoning follows.
- Trigger privilege: root only (
caps_priv_check_td(SYSCAP_NOSYSCTL_WR), kern_sysctl.c:1447). Not reachable by unprivileged users.
Fix
fix.diff: defer device_delete_children + unconfigure + configure +
bus_generic_attach into hdaa_reconfig_task() on
taskqueue_thread[mycpuid]; the sysctl handler only enqueues and returns.
Drain the task in hdaa_detach before teardown. Zero-fuzz patch -p1.
Fix validation (fix_validation.log)
cd /usr/src/sys/dev/sound/driver/hda && makeβ snd_hda.ko builds clean with -Werror.- Swapped module, re-attached (hdaa0/pcm0 up).
sysctl dev.hdaa.0.reconfig=1β returns RC=0 in <1s; dmesg:pcm0: detachedthenpcm0: <Generic (0x1af40022) (Analog)> at nid 3 and 5 on hdaa0.- Second reconfig: RC=0,
/dev/dsp0present,sysctl kern.osreldateresponsive β HEALTH_OK.
Impact ceiling
Whole-kernel freeze from a single root sysctl write on any machine with an HDA codec (the standard "apply new pin config" workflow). Not a privilege escalation; no memory corruption. Bucket: base:dos.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedDeferred-taskqueue fix (hdaa.c: sysctl handler enqueues reconfig_task; hdaa.h: struct task member; attach TASK_INIT; detach taskqueue_drain) built as a module (-Werror clean) and swapped into the guest: the previously-hanging sysctl now returns rc=0 in <1s, dmesg shows pcm0 detach/re-attach cycle, /dev/dsp0 recreated, two consecutive reconfigs and full sysctl/ssh health verified.
findings/poc/DF-3601/fix_validation.log; findings/poc/DF-3601/fix.diff
Confirmed kernel references
Detail
Exploit chain
root: sysctl dev.hdaa.0.reconfig=1 -> hdaa_sysctl_reconfig (hdaa.c:6474) -> device_delete_children (hdaa.c:6492) -> hdaa_pcm_detach -> pcm_unregister -> sysctl_ctx_free (sound.c:1233) -> sysctl_unregister_oid -> SYSCTL_XLOCK (kern_sysctl.c:171) -> _sysctl_xlock takes every CPU's gd_sysctllock EXCLUSIVE (kern_sysctl.c:1642) while the same thread holds its own CPU's lock SHARED (kern_sysctl.c:1572) -> permanent sleep; wedge spreads system-wide.
Evidence (decisive lines)
run.log (write syscall never returns; t+120s even new ssh hangs; qemu alive, no panic); boot.log.firstwedge / boot.log.repro2 (console silent after attach); fix_validation.log (patched module: RC=0, 'pcm0: detached' then re-attach, HEALTH_OK, 2 consecutive reconfigs); fix.diff (taskqueue deferral, patch -p1 zero fuzz).
PoC changes
No code PoC needed beyond sysctl dev.hdaa.0.reconfig=1; guest needed booting with -device intel-hda -device hda-duplex (vm.sh temporarily extended via HDA_ARGS env var, reverted after).
Verified recommended fix
Defer device_delete_children/unconfigure/configure/bus_generic_attach out of the sysctl handler into a taskqueue (hdaa_reconfig_task on taskqueue_thread[mycpuid]); drain it in hdaa_detach.
Verdict
Deterministic kernel self-deadlock: writing dev.hdaa.N.reconfig=1 (root-only, the documented audio reconfiguration interface) never returns and freezes the whole system. The sysctl handler mutates the newbus device tree (device_delete_children/bus_generic_attach) from inside sysctl-handler context where userland_sysctl() holds the per-CPU global sysctllock SHARED, while pcm child (un)registration takes _sysctl_xlock() = every CPU's gd_sysctllock EXCLUSIVE β the thread waits for its own shared hold forever and poisons sysctl system-wide. Reproduced 2/2 on QEMU intel-hda+hda-duplex (benign codec β hardware-independent). Fixed by deferring the reconfiguration to a taskqueue; fix validated on-guest (trigger returns rc=0, pcm0 detaches/re-attaches, system healthy, second reconfig clean).
No comments yet.