sound: hda: reconfig reuses card->shutdown as a soft lock, so a concurrent controller remove skips the card disconnect (persistent dangling card / concurrent UAF; reproducible system freeze)
From: 徐晗翔
Date: Fri Oct 30 2026 - 00:43:05 EST
The HDA reconfig path (writing /sys/class/sound/hwCxDy/reconfig,
CONFIG_SND_HDA_RECONFIG) has no dedicated lock for its critical
section (codec driver release + reprobe): it temporarily sets
card->shutdown = 1 as a "soft lock" (snd_hda_lock_devices(),
sound/hda/common/codec.c:1746-1779) and clears it afterwards. But
snd_card_disconnect() interprets card->shutdown == 1 as "disconnect
already done" and returns immediately (sound/core/init.c:502-505) --
the same flag carrying two meanings. If the controller is removed
(PCI unbind/remove) while a reconfig is inside its soft-lock window,
the card teardown skips its entire disconnect sequence: snd_cards[]
is never cleared, the card device is never device_del'ed, the card
number bitmap bit leaks -- and the card memory is then kfree()d with
those references still dangling, a persistent use-after-free surface
that later ordinary, unprivileged accesses hit (reading
/proc/asound/cards, snd_card_ref() from control/LED paths, sysfs
id_store which copies attacker-controlled bytes into the freed card).
I reproduced a system-level consequence on a vanilla Linux 7.3 KASAN
kernel with an emulated Intel HDA controller, using only root sysfs
writes: the reconfig x PCI-unbind hammer froze the whole guest solid
(two independent runs, ~33 s and ~39 s in, serial console silent, no
hung-task or panic output until the host killed the VM), while two
control hammers without the reconfig path -- including a much harder
codec/PCI/PCM churn that produced ~1900 KASAN splats -- never froze.
The freeze shape matches a permanent spin on a lock embedded in freed
memory (the same freed-lock family that dominates the churn run's
reports); I did not kprobe-pin the exact spin point, so I report the
deadlock/reproducible-DoS part as confirmed and the precise UAF
localization as source-verified.
The detailed description:
* The semantic conflict (Linux 7.3)
snd_hda_lock_devices() (codec.c:1746-1779), under
card->files_lock momentarily: if (!card->shutdown)
card->shutdown = 1; ... return 0; -- and the flag stays set for
the whole reconfig critical section (device_release_driver(codec),
milliseconds) AFTER the spinlock is released.
snd_card_disconnect() (init.c:495-561):
scoped_guard(spinlock, &card->files_lock) {
if (card->shutdown)
return; /* skips EVERYTHING below */
card->shutdown = 1;
... replace fops ...
}
... snd_device_disconnect_all(); device_del(&card->card_dev);
... snd_cards[card->number] = NULL; clear_bit(...);
Both check-and-set pairs are atomic (same files_lock), so there is
no torn state -- the problem is purely that T2 cannot distinguish
"already disconnected" (should proceed with teardown) from
"reconfig freeze in progress" (must not proceed), and the early
return means the teardown continues with the disconnect steps
skipped.
* Interleaving I (T2's snd_card_disconnect lands inside the reconfig
soft-lock window) -- the main damage
T1 (reconfig): lock_devices (shutdown=1) ->
device_release_driver(codec) [codec_dev->mutex held] ...
T2 (PCI remove): azx_remove -> snd_card_free -> snd_card_disconnect
sees shutdown==1 -> returns (skips fops replacement,
snd_device_disconnect_all, card_dev device_del, snd_cards[]
clearing) -> put_device -> snd_card_do_free ->
snd_device_free_all: frees PCM etc., then the CODEC node ->
snd_hda_codec_unregister -> device_del(codec_dev) blocks on
codec_dev->mutex ... T1 releases the codec mutex,
unlock_devices (shutdown=0; card not yet kfreed -- T2 is blocked
in the CODEC round), device_reprobe(codec) and
snd_card_register(card) run against the card that T2 is tearing
down ... T2 proceeds, azx_free, kfree(card), returns.
Net result regardless of the second-order ordering: snd_cards[idx]
still points at the kfree()d card; card_dev was never
device_del'ed (still on the sound class / sysfs); the
snd_cards_lock bit leaked. Persistent, no further race needed:
- snd_card_ref() (init.c:389-399) does get_device() on the
dangling card_dev (kref write on freed memory); reachable from
module autoload on open (sound.c:127), the control LED code
(control_led.c:250,310,604,768), control.c:2358;
- /proc/asound/cards (init.c:943-965) prints id/shortname/
longname from the freed card (UAF read / info leak);
- /sys/class/sound/cardN/id store (init.c:788-828) strscpy's
attacker-controlled bytes into the freed card's memory.
(New /dev/snd opens stay blocked: the compensating disconnect in
do_free unregisters the minors -- pcm.c:1118-1120,
control.c:2438-2451.)
Second-order branches: if the reprobe wins the codec lock before
T2 kills the device, hda_codec_driver_probe() re-inserts PCM
snd_device nodes into card->devices and snd_card_register()
re-traverses it -- unlocked list operations against T2's
concurrent free_all (same unlocked card->devices family as my
separate snd_device report); if T2 wins, snd_card_register()
still runs on the possibly-already-freed card (UAF read +
traversal).
* Interleaving II (T2's disconnect arrives after T1's unlock but
before reconfig finishes): the disconnect runs fully, and the
reconfig tail (snd_pcm_new/snd_device_new insertion,
snd_device_register_all traversal, snd_card_register) races the
card teardown and kfree -- a large window (the whole first
free_all round), no soft-lock timing needed. Interleaving III
(disconnect already done before reconfig locks): reconfig
correctly fails with -EINVAL. Safe.
* Evidence (vanilla 7.3 KASAN + CONFIG_SND_HDA_RECONFIG=y,
QEMU intel-hda + hda-duplex; reproducer attached)
Hammer (attached snd-hammer-c.sh): T1 reconfig write loop x T2
PCI unbind/bind rounds with a live window -- pure root sysfs
writes.
- Two independent runs: after 6 and 12 real reconfig executions
respectively, the guest froze completely at ~33.5 s and ~38.9 s
(serial console dead silence, no hung-task warning, no panic,
unresponsive until the host timeout killed the VM at 500 s).
- Control 1 (same PCI churn, no reconfig, 40 rounds): clean
completion, zero splats, dangling=0.
- Control 2 (a much harder churn -- codec unbind + PCI unbind +
PCM open/close, no reconfig): ~1900 KASAN splats but the system
never froze in 500 s. The only differing variable in the
freeze runs is the reconfig path (snd_hda_codec_reset's
lock/unlock_devices soft lock + device_reprobe +
snd_card_register).
- The no-output freeze (not even the hung-task detector) is
consistent with permanent spinning on a lock inside freed
memory -- the churn run's ~1890 queued_spin_lock_slowpath
slab-out-of-bounds reports are the same family caught mid-spin
by KASAN; both lock_devices() and snd_card_disconnect() take
card->files_lock. I did not kprobe-pin the exact spin point in
the frozen state, hence: reproducible DoS = confirmed; exact
UAF locus = source analysis (interleavings above) with the
freeze as its observable.
Regards,
Hanxiang Xu
xuhx@xxxxxxxxxxx
Attachment:
snd-hammer-c.sh
Description: Bourne shell script