[PATCH v2] ALSA: pcm: Drain proc callbacks at device disconnect

From: Cen Zhang

Date: Thu Oct 22 2026 - 05:10:51 EST


Disconnecting an individual PCM leaves its proc files accessible until
snd_pcm_free_stream(). HDA codec-only sysfs unbind can take this path
while the controller and card remain registered.

With CONFIG_SND_VERBOSE_PROCFS, a prealloc write is buffered until file
release. Keep such a file open without an attached PCM runtime and unbind
the codec driver. The proc file does not hold a codec PCM reference, so
cleanup can free the preallocated buffer before the pending write runs:

HDA codec unbind prealloc file release
---------------- ---------------------
hda_codec_driver_remove()
snd_hda_codec_disconnect_pcms()
snd_device_disconnect()
snd_pcm_dev_disconnect()
/* proc nodes remain */
snd_hda_codec_cleanup_for_unbind()
codec_release_pcms()
snd_device_free()
snd_pcm_free()
free preallocated buffer
snd_info_text_entry_release()
snd_pcm_lib_preallocate_proc_write()
replace DMA buffer
remove stream proc nodes
free substream

Proc rundown itself can also run this buffered write after the final
buffer cleanup, leaving its replacement allocation behind.

Remove and drain each stream's proc subtree at PCM disconnect, before
taking open_mutex, which the proc callbacks need. Retain the info entries
for their existing final cleanup: freeing the tree at disconnect would
leave the OSS child's saved proc_entry pointer dangling.

Add snd_info_disconnect() to separate proc rundown from entry freeing.
Mark the subtree disconnected and clear its proc pointers under
info_mutex, then remove the nodes outside that mutex so pending opens
can finish. Check ancestors under the same mutex when registering an
entry, preventing a later card registration or direct child registration
from republishing a disconnected subtree.

Suggested-by: Takashi Iwai <tiwai@xxxxxxx>
Assisted-by: LLM
Signed-off-by: Cen Zhang <zzzccc427@xxxxxxxxx>
---
Changes in v2:
- Follow Takashi's suggestion to remove and drain stream proc nodes at PCM
disconnect rather than only moving final buffer cleanup.
- Run proc teardown before taking open_mutex and retain info entries for
final cleanup, including the saved OSS child entries.
- Prevent later recursive or direct descendant registration from
republishing a disconnected subtree.

Link to v1: https://lore.kernel.org/r/pm-pmb-2439c851f82c443c3600-v4-9e7df0ea42c36afe6171@xxxxxxxxx

include/sound/info.h | 3 +++
sound/core/info.c | 31 ++++++++++++++++++++++++++++++-
sound/core/pcm.c | 9 +++++++++
3 files changed, 42 insertions(+), 1 deletion(-)

diff --git a/include/sound/info.h b/include/sound/info.h
index adbc506860d6..3775aed2d8ab 100644
--- a/include/sound/info.h
+++ b/include/sound/info.h
@@ -71,6 +71,7 @@ struct snd_info_entry {
void *private_data;
void (*private_free)(struct snd_info_entry *entry);
struct proc_dir_entry *p;
+ bool disconnected;
struct mutex access;
struct list_head children;
struct list_head list;
@@ -118,6 +119,7 @@ struct snd_info_entry *snd_info_create_card_entry(struct snd_card *card,
const char *name,
struct snd_info_entry *parent);
void snd_info_free_entry(struct snd_info_entry *entry);
+void snd_info_disconnect(struct snd_info_entry *entry);

int snd_info_card_create(struct snd_card *card);
int snd_info_card_register(struct snd_card *card);
@@ -165,6 +167,7 @@ static inline char *snd_info_get_str(char *dest, char *src, int len) { return NU
static inline struct snd_info_entry *snd_info_create_module_entry(struct module *module, const char *name, struct snd_info_entry *parent) { return NULL; }
static inline struct snd_info_entry *snd_info_create_card_entry(struct snd_card *card, const char *name, struct snd_info_entry *parent) { return NULL; }
static inline void snd_info_free_entry(struct snd_info_entry *entry) { ; }
+static inline void snd_info_disconnect(struct snd_info_entry *entry) { }

static inline int snd_info_card_create(struct snd_card *card) { return 0; }
static inline int snd_info_card_register(struct snd_card *card) { return 0; }
diff --git a/sound/core/info.c b/sound/core/info.c
index c4d57d0809ad..c74dd779d361 100644
--- a/sound/core/info.c
+++ b/sound/core/info.c
@@ -737,6 +737,30 @@ static void snd_info_clear_entries(struct snd_info_entry *entry)
entry->p = NULL;
}

+/**
+ * snd_info_disconnect - remove proc nodes without freeing their info entries
+ * @entry: root of the info entry subtree
+ *
+ * Prevents registration of this subtree and drains its proc callbacks.
+ * The caller must serialize this with entry destruction and must not hold
+ * locks needed by the callbacks. Entries remain for snd_info_free_entry().
+ */
+void snd_info_disconnect(struct snd_info_entry *entry)
+{
+ struct proc_dir_entry *p;
+
+ if (!entry)
+ return;
+
+ scoped_guard(mutex, &info_mutex) {
+ entry->disconnected = true;
+ p = entry->p;
+ snd_info_clear_entries(entry);
+ }
+ proc_remove(p);
+}
+EXPORT_SYMBOL(snd_info_disconnect);
+
/**
* snd_info_free_entry - release the info entry
* @entry: the info entry
@@ -774,11 +798,16 @@ EXPORT_SYMBOL(snd_info_free_entry);
static int __snd_info_register(struct snd_info_entry *entry)
{
struct proc_dir_entry *root, *p = NULL;
+ struct snd_info_entry *parent;

if (snd_BUG_ON(!entry))
return -ENXIO;
- root = entry->parent == NULL ? snd_proc_root->p : entry->parent->p;
guard(mutex)(&info_mutex);
+ for (parent = entry; parent; parent = parent->parent) {
+ if (parent->disconnected)
+ return 0;
+ }
+ root = !entry->parent ? snd_proc_root->p : entry->parent->p;
if (entry->p || !root)
return 0;
if (S_ISDIR(entry->mode)) {
diff --git a/sound/core/pcm.c b/sound/core/pcm.c
index 1e4e0ecb3dbd..ca1b72ba252a 100644
--- a/sound/core/pcm.c
+++ b/sound/core/pcm.c
@@ -520,6 +520,11 @@ static int snd_pcm_stream_proc_init(struct snd_pcm_str *pstr)
return 0;
}

+static void snd_pcm_stream_proc_disconnect(struct snd_pcm_str *pstr)
+{
+ snd_info_disconnect(pstr->proc_root);
+}
+
static int snd_pcm_stream_proc_done(struct snd_pcm_str *pstr)
{
snd_info_free_entry(pstr->proc_root);
@@ -580,6 +585,7 @@ static int snd_pcm_substream_proc_init(struct snd_pcm_substream *substream)

#else /* !CONFIG_SND_VERBOSE_PROCFS */
static inline int snd_pcm_stream_proc_init(struct snd_pcm_str *pstr) { return 0; }
+static inline void snd_pcm_stream_proc_disconnect(struct snd_pcm_str *pstr) { }
static inline int snd_pcm_stream_proc_done(struct snd_pcm_str *pstr) { return 0; }
static inline int snd_pcm_substream_proc_init(struct snd_pcm_substream *substream) { return 0; }
#endif /* CONFIG_SND_VERBOSE_PROCFS */
@@ -1094,6 +1100,9 @@ static int snd_pcm_dev_disconnect(struct snd_device *device)
int cidx;

guard(mutex)(&register_mutex);
+ /* Proc callbacks may take open_mutex, including at file release. */
+ for (cidx = 0; cidx < 2; cidx++)
+ snd_pcm_stream_proc_disconnect(&pcm->streams[cidx]);
guard(mutex)(&pcm->open_mutex);
wake_up(&pcm->open_wait);
list_del_init(&pcm->list);
--
2.43.0