mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* 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)
@ 2026-10-30  4:43 徐晗翔
  0 siblings, 0 replies; only message in thread
From: 徐晗翔 @ 2026-10-30  4:43 UTC (permalink / raw)
  To: perex, tiwai; +Cc: linux-sound, linux-kernel

[-- 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 --]

^ permalink raw reply	[flat|nested] only message in thread

only message in thread, other threads:[~2026-10-30  4:43 UTC | newest]

Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-30  4:43 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) 徐晗翔

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®