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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3601 Β· 10 files
FileTypeDescriptionSize
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() holds SYSCTL_SLOCK() (the caller CPU's gd_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) calls device_delete_children(dev) (hdaa.c:6492) and later bus_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 run sysctl_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's gd_sysctllock EXCLUSIVE (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 wedges
  • fix.diff β€” deferred-taskqueue fix (applies -p1, zero fuzz)
  • fix_validation.log β€” patched module: trigger returns, pcm re-attach, health OK
  • verdict.json, manifest.json, VERDICT.md
VERDICT.md
↓ download raw

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)

  1. kldload snd_hda β†’ hdac0/hdacc0/hdaa0/pcm0 attach cleanly (boot.log.repro2 tail).
  2. Negative control: sysctl dev.hdaa.0.config=softpcmvol (a tree-neutral hdaa sysctl write) returns instantly β€” proves ordinary hdaa sysctl writes are fine.
  3. t=0 session A: sysctl dev.hdaa.0.reconfig=1 - sysctl(8) prints dev.hdaa.0.reconfig: 0 (the pre-write read-back) and then the write syscall NEVER returns; session A killed by an external timeout 30 β€” echo RC=$? / trailing date never execute.
  4. t+7s session B: sysctl kern.osreldate still works (rc=0), ps works β€” consistent with the per-CPU lock analysis: the wedged thread holds its own CPU's gd_sysctllock SHARED 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.
  5. t+~120s: even new ssh connections hang (login path needs sysctl) β€” vm.sh status probe itself timed out. QEMU process alive, serial console silent (no panic β€” sleeping thread, not a crash).
  6. 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:6498 bus_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's gd_sysctllock LK_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: detached then pcm0: <Generic (0x1af40022) (Analog)> at nid 3 and 5 on hdaa0.
  • Second reconfig: RC=0, /dev/dsp0 present, sysctl kern.osreldate responsive β€” 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

fixed
baseline reproduced→ patch + rebuild →patched clean

Deferred-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
↓ fix.diffper-fix-DF-3601

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