From: 徐晗翔 <xuhx@hust.edu.cn>
To: perex@perex.cz, tiwai@suse.com
Cc: linux-sound@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: sound: core: card->devices is unlocked across the codec-side targeted free and the card-side bulk free (double free / UAF; KASAN confirmed on 7.3)
Date: Fri, 30 Oct 2026 12:43:23 +0800 (GMT+08:00) [thread overview]
Message-ID: <18ba4e54.1b567.1a18b1ea506.Coremail.xuhx@hust.edu.cn> (raw)
[-- Attachment #1: Type: text/plain, Size: 6762 bytes --]
The per-card device list (card->devices) has two independent free
paths that share no lock: the HDA codec driver's targeted release
(unbinding the codec driver walks snd_hda_codec_cleanup_for_unbind()
and frees the codec's PCM/JACK snd_device nodes) and the card-level
bulk release (snd_card_free() -> snd_device_free_all(), entered from
PCI remove or, when files were open, from the last file close).
sound/core/device.c maintains that list -- lookup, traversal, unlink,
free -- entirely without locks; the two paths run under different
driver-core locks (codec_dev->mutex vs pci_dev->mutex vs none), so
the interleavings are real concurrency. I reproduced it on a vanilla
Linux 7.3 KASAN kernel with an emulated Intel HDA controller using
only root sysfs writes: the first concurrent round already produced
slab-use-after-free reports, and a sustained hammer accumulated
~1900 KASAN reports, including a three-stack closure on the JACK
objects (allocated by the codec probe, freed by the codec-side
targeted cleanup, written by the card-side bulk free).
Where the locks are, and where they are not: the PCM disconnect
callbacks do take the global register_mutex, so the *disconnect*
stages of both paths are serialized -- the gap is the *free* stage
and the list itself (__snd_device_free(), look_for_dev(),
snd_device_free_all() in sound/core/device.c are all unlocked). A
fourth, wider form extends to card->controls: when the codec-side PCM
free reaches free_chmap() -> snd_ctl_remove() for the PCM's channel
map kcontrol, the card-side CONTROL teardown
(snd_ctl_dev_free()) may already have deleted and freed that same
kcontrol -- controls_rwsem serializes the list operations but cannot
protect the dangling chmap_kctl pointer, so snd_ctl_remove()
performs a UAF write (list_del) and a double free. This form does
not depend on winning the narrow instruction-level windows of the
list races: its window is the whole second half of the targeted
snd_pcm_free() versus the card-side CONTROL-round completion, i.e.
tens of microseconds, and I expect it to be the first form hit under
sustained pressure on real hardware.
The detailed description:
* The two unlocked paths (Linux 7.3 line numbers)
T1 (codec side): /sys/bus/hdaudio/drivers/<codec-drv>/unbind ->
device_release_driver(codec_dev) [codec_dev->mutex] ->
hda_codec_driver_remove() (sound/hda/common/bind.c:152-174) ->
snd_hda_codec_cleanup_for_unbind() (codec.c:758-791) ->
codec_release_pcms() -> snd_device_free(card, pcm)
(codec.c:747); along the way snd_hda_jack_tbl_clear() ->
snd_device_free(card, jack->jack) (jack.c:188).
T2 (card side, no open files): PCI unbind/remove ->
device_release_driver(pci_dev) [pci_dev->mutex] -> azx_remove()
-> snd_card_free() -> snd_card_disconnect() ->
snd_device_disconnect_all(); same-thread put_device ->
snd_card_do_free() -> snd_device_free_all().
T3 (card side, files were open): the last close's
snd_card_file_remove() -> put_device() runs snd_card_do_free()
-> snd_device_free_all() in the closing task.
All snd_device list operations in sound/core/device.c (82-91
look_for_dev; 69-80 __snd_device_free's list_del; 208-216
disconnect_all; 222-239 free_all) are unlocked; T1/T2/T3's lock
sets are pairwise disjoint. The best window is the "same close
event, dual wakeup": with a PCM stream open, T1 blocks in
snd_refcount_sync(&codec->pcm_ref) and T2 blocks in
wait_for_completion(&released); the stream's close wakes T1
(snd_hda_codec_pcm_put) and simultaneously makes the closer itself
run the card bulk free -- one event releases two unlocked paths.
* Failure forms
1. free vs traversal: T1's look_for_dev() picks a node, T2/T3
frees it, T1's list_del/dev_free/kfree hit freed memory (UAF
write + indirect call); the reverse tears T2/T3's
list_for_each_entry_safe traversal.
2. free vs free: both obtain the node before either unlinks --
two snd_pcm_free() executions, double kfree of the snd_device,
the snd_pcm and the azx_pcm private data, plus a double
put_device on the stream devices (kref underflow).
3. chmap vs CONTROL teardown (the wide one, see above):
snd_pcm_free() -> snd_pcm_free_stream() -> free_chmap()
(pcm.c:802-810,836) -> snd_ctl_remove(card, chmap_kctl) races
snd_ctl_dev_free() (control.c:2457-2477): the rwsem orders the
list ops but T1's chmap_kctl is a bare pointer; if T2 freed the
kcontrol first, T1's __snd_ctl_remove() does remove_hash_entries
(UAF read/write) + list_del (UAF write) and snd_ctl_free_one
runs private_free + kfree again (double free).
* Evidence (vanilla 7.3 KASAN + CONFIG_SND_HDA_RECONFIG/GENERIC,
QEMU intel-hda + hda-duplex; reproducer attached)
Hammer (attached snd-hammer-b.sh): T1 codec-driver unbind/bind
loop x T2 PCI unbind/bind rounds x T3 PCM open/close churn -- pure
root sysfs writes, no kernel modification.
- First concurrent round: 4 slab-use-after-free reports; sustained
run: ~1900 reports. Dominant forms:
* hda_free_jack_priv+0x23/+0x31 slab-use-after-free (write),
three stacks closed:
use: snd_device_free_all -> snd_jack_dev_free ->
hda_free_jack_priv (card-side bulk free)
free: snd_hda_codec_cleanup_for_unbind -> snd_array_free
(codec-side targeted cleanup of the jack table)
alloc: hda_codec_driver_probe -> snd_hda_gen_build_controls
-> snd_hda_jack_tbl_new -> snd_array_new (codec probe)
-- i.e. the audited two-path interleaving, materialized on
the JACK objects (jack->jack is a snd_device on the same
card->devices list).
* snd_pcm_open slab-use-after-free + snd_card_file_add
out-of-bounds/NULL (opens racing the teardown).
* snd_device_register_all wild-memory-access reading list
poison (dead000000000118): the delayed probe work's
snd_card_register traversal racing the concurrent teardown --
the register_all side of the same unlocked list.
* ~1890 reports of queued_spin_lock_slowpath
slab-out-of-bounds: spinning on locks embedded in freed
memory (the card/files lock family) -- the freed-lock spin
family that also explains the hang shape in my separate
reconfig report.
- Baseline (sequential reconfig, sequential PCI unbind/bind, 40
churn rounds without concurrency): zero splats.
Honest scope note: the natural hammer hit the widest-window forms
first (JACK UAF, freed-lock spins); the PCM/chmap-specific forms
1/2/3 above were source-verified and are the same unlocked-list
root cause, but were not individually isolated in this run.
Regards,
Hanxiang Xu
xuhx@hust.edu.cn
[-- Attachment #2: snd-hammer-b.sh --]
[-- Type: application/x-sh, Size: 1821 bytes --]
reply other threads:[~2026-10-30 4:43 UTC|newest]
Thread overview: [no followups] expand[flat|nested] mbox.gz Atom feed
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=18ba4e54.1b567.1a18b1ea506.Coremail.xuhx@hust.edu.cn \
--to=xuhx@hust.edu.cn \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-sound@vger.kernel.org \
--cc=perex@perex.cz \
--cc=tiwai@suse.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®