mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: "Luca Rodenhäuser" <otzelot2021@outlook.de>
To: perex@perex.cz, tiwai@suse.com
Cc: linux-sound@vger.kernel.org, linux-kernel@vger.kernel.org,
	"Luca Rodenhäuser" <otzelot2021@outlook.de>
Subject: [RFC PATCH 4/8] ALSA: core: carry the untruncated form of the card names
Date: Tue, 15 Sep 2026 17:59:04 +0200	[thread overview]
Message-ID: <AMBP191MB2886BF75AD878F97C3CC8D3FCEBA2@AMBP191MB2886.EURP191.PROD.OUTLOOK.COM> (raw)
In-Reply-To: <AMBP191MB2886193B1F896974A7CE23E1CEBA2@AMBP191MB2886.EURP191.PROD.OUTLOOK.COM>

card->shortname, card->longname and card->mixername are fixed-size fields,
32 and 80 bytes, and a name that does not fit is cut.  The previous patch
stops that cut from landing inside a character, but it cannot make a name
fit: 32 bytes is eight CJK characters, so a cleanly cut name is still a cut
name, and user space has no way to recover what was dropped.

card->components already solves the same problem the other way round: the
card holds the whole string, and snd_ctl_card_info::components is a cut
copy of it, with the full one available through SNDRV_CTL_IOCTL_CARD_BYTES.
Do the same for the three names.

card->full_shortname and friends hold the whole name, allocated on demand,
and stay NULL when the name fit into its fixed field.  That is the point of
the design: "set" means "the fixed field is not the whole story", so there
is one name per card in two lengths, and never a pair that could disagree
or that a driver has to keep in sync.  Nothing user space sees today
changes, since the fixed fields keep exactly what they hold now.

Card names are UTF-8, which on USB they have been for decades, so the
kernel repairs what would break printing them: a malformed sequence becomes
U+FFFD, one per bad byte as recommended for a maximal subpart in Unicode
TR#36, and a code point that would break a line becomes a space.  The
second one matters because the kernel prints these names into
/proc/asound/cards, which is line based.  Rejecting a name instead would
achieve nothing: the fixed field would still hold it, and readers are told
to use that whenever the untruncated one is absent.

Nothing else is inspected.  Invisible and bidirectional format characters
are a real spoofing vector, but catching them needs the Unicode character
properties, which would mean a table in the sound core that goes stale with
every Unicode release - the same dependency avoided by leaving NFC to
drivers.  A partial range list is worse than none, because it invites user
space to skip its own filtering.

Assisted-by: LLM
Signed-off-by: Luca Rodenhäuser <otzelot2021@outlook.de>
---
 include/sound/core.h |  27 +++++
 sound/core/init.c    | 272 +++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 299 insertions(+)

diff --git a/include/sound/core.h b/include/sound/core.h
index 36bb95044a..c00f091ea1 100644
--- a/include/sound/core.h
+++ b/include/sound/core.h
@@ -96,6 +96,16 @@ static inline void snd_refcount_get(struct snd_refcount *ref)
 void snd_refcount_put(struct snd_refcount *ref);
 void snd_refcount_sync(struct snd_refcount *ref);
 
+/*
+ * Maximum size in bytes, excluding the terminating NUL, of the untruncated
+ * card name fields.  Each limit is four times the size of the corresponding
+ * fixed field, so the same number of characters always fits, whatever they
+ * encode to: card names are UTF-8, and a character takes up to four bytes.
+ */
+#define SNDRV_CARD_FULL_SHORTNAME_MAX	128
+#define SNDRV_CARD_FULL_LONGNAME_MAX	320
+#define SNDRV_CARD_FULL_MIXERNAME_MAX	320
+
 /* main structure for soundcard */
 
 struct snd_card {
@@ -108,6 +118,20 @@ struct snd_card {
 	char longname[80];		/* name of this soundcard */
 	char irq_descr[32];		/* Interrupt description */
 	char mixername[80];		/* mixer name */
+	/*
+	 * The untruncated form of the three name fields above, like
+	 * card->components is the untruncated form of what
+	 * snd_ctl_card_info::components holds.  Each is NULL unless the name
+	 * did not fit into its fixed-size field, so a reader that finds NULL
+	 * already has the whole name in the field above.
+	 *
+	 * Set them only via snd_card_set_full_*(), and only before
+	 * snd_card_register().  After that they are read under
+	 * snd_ioctl_rwsem.
+	 */
+	char *full_shortname;		/* UTF-8 short name, may be NULL */
+	char *full_longname;		/* UTF-8 long name, may be NULL */
+	char *full_mixername;		/* UTF-8 mixer name, may be NULL */
 	char *components;		/* card components, space-delimited */
 	unsigned int components_alloc_size;	/* current allocation size of components */
 	struct module *module;		/* top-level module */
@@ -317,6 +341,9 @@ void snd_card_free_when_closed(struct snd_card *card);
 int snd_card_free_on_error(struct device *dev, int ret);
 void snd_card_set_id(struct snd_card *card, const char *id);
 size_t snd_utf8_strscpy(char *dst, const char *src, size_t size);
+int snd_card_set_full_shortname(struct snd_card *card, const char *name);
+int snd_card_set_full_longname(struct snd_card *card, const char *name);
+int snd_card_set_full_mixername(struct snd_card *card, const char *name);
 int snd_card_register(struct snd_card *card);
 int snd_card_info_init(void);
 int snd_card_add_dev_attr(struct snd_card *card,
diff --git a/sound/core/init.c b/sound/core/init.c
index 67b79b1826..cabe4da212 100644
--- a/sound/core/init.c
+++ b/sound/core/init.c
@@ -16,6 +16,7 @@
 #include <linux/debugfs.h>
 #include <linux/completion.h>
 #include <linux/interrupt.h>
+#include <linux/err.h>
 
 #include <sound/core.h>
 #include <sound/control.h>
@@ -146,6 +147,7 @@ static int snd_card_init(struct snd_card *card, struct device *parent,
 			 int idx, const char *xid, struct module *module,
 			 size_t extra_size);
 static int snd_card_do_free(struct snd_card *card);
+static void snd_card_free_full_names(struct snd_card *card);
 static const struct attribute_group card_dev_attr_group;
 
 static void release_card_device(struct device *dev)
@@ -594,6 +596,7 @@ static int snd_card_do_free(struct snd_card *card)
 	kfree(card->components);
 	card->components = NULL;
 	card->components_alloc_size = 0;
+	snd_card_free_full_names(card);
 	if (card->private_free)
 		card->private_free(card);
 #ifdef CONFIG_SND_CTL_DEBUG
@@ -841,6 +844,275 @@ size_t snd_utf8_strscpy(char *dst, const char *src, size_t size)
 }
 EXPORT_SYMBOL_GPL(snd_utf8_strscpy);
 
+/*
+ * Untruncated card names
+ *
+ * card->shortname, card->longname and card->mixername are fixed-size fields,
+ * and a name that does not fit is cut.  card->full_shortname and friends hold
+ * the whole name in that case, exactly as card->components holds the whole
+ * components string of which snd_ctl_card_info::components is a cut copy.
+ * User space reads them through SNDRV_CTL_IOCTL_CARD_BYTES.
+ *
+ * A field stays NULL when the name fits, so "set" means "the fixed field is
+ * not the whole story".  Nothing has to be kept in sync: there is one name,
+ * in two lengths.
+ *
+ * Card names are UTF-8.  The kernel prints them into procfs and hands them to
+ * user space, so it repairs what would break that: a malformed sequence
+ * becomes U+FFFD, and a code point that would break a line becomes a space.
+ * Rejecting a name instead would achieve nothing, since the fixed field still
+ * holds it and readers are told to use that when the full one is absent.
+ *
+ * Beyond that the kernel does not inspect names.  NFC, homoglyphs and the
+ * invisible and bidirectional format characters need the Unicode character
+ * properties, which would go stale in the sound core; user space has them.
+ * A name is a display string and never an identifier: card->id stays the
+ * unique handle.
+ */
+
+/* ASCII whitespace only; isspace() also matches 0xa0, which would eat UTF-8
+ * continuation bytes
+ */
+static bool utf8_is_ascii_space(unsigned char c)
+{
+	return c == ' ' || (c >= '\t' && c <= '\r');
+}
+
+/* Decode the UTF-8 sequence at @s, which holds @len readable bytes.
+ * Stores the code point in @cp and returns its length in bytes, or 0 if @s
+ * doesn't start a well-formed sequence.
+ */
+static size_t utf8_decode(const unsigned char *s, size_t len, u32 *cp)
+{
+	static const u32 min_cp[5] = { 0, 0, 0x80, 0x800, 0x10000 };
+	size_t i, n;
+	u32 val;
+
+	if (s[0] < 0x80) {
+		*cp = s[0];
+		return 1;
+	} else if ((s[0] & 0xe0) == 0xc0) {
+		n = 2;
+		val = s[0] & 0x1f;
+	} else if ((s[0] & 0xf0) == 0xe0) {
+		n = 3;
+		val = s[0] & 0x0f;
+	} else if ((s[0] & 0xf8) == 0xf0) {
+		n = 4;
+		val = s[0] & 0x07;
+	} else {
+		return 0;	/* stray continuation byte or 5/6-byte form */
+	}
+
+	if (len < n)
+		return 0;
+	for (i = 1; i < n; i++) {
+		if ((s[i] & 0xc0) != 0x80)
+			return 0;
+		val = (val << 6) | (s[i] & 0x3f);
+	}
+
+	/* overlong encoding, surrogate half or beyond the Unicode range */
+	if (val < min_cp[n] || val > 0x10ffff || (val >= 0xd800 && val <= 0xdfff))
+		return 0;
+
+	*cp = val;
+	return n;
+}
+
+/* U+FFFD REPLACEMENT CHARACTER */
+#define UTF8_REPLACEMENT	"\xef\xbf\xbd"
+
+/* Code points that would break the structure the name is printed into: sysfs
+ * attributes and /proc/asound/cards are line based, and a terminal acts on
+ * control characters.  They are replaced by a space rather than rejected.
+ *
+ * Nothing else is inspected.  Invisible, bidirectional and deprecated format
+ * characters are a real spoofing vector, but catching them needs the Unicode
+ * character properties, which would mean a table in the sound core that goes
+ * stale with every Unicode release.  A short list of ranges only looks like a
+ * defence; user space has the tables and has to do that part.
+ */
+static bool utf8_needs_space(u32 cp)
+{
+	if (cp < 0x20 || cp == 0x7f)		/* C0 controls and DEL */
+		return true;
+	if (cp >= 0x80 && cp <= 0x9f)		/* C1 controls */
+		return true;
+	if (cp == 0x2028 || cp == 0x2029)	/* line/paragraph separator */
+		return true;
+	return false;
+}
+
+/* Sanitise @src into a UTF-8 string of at most @max bytes.
+ *
+ * A malformed sequence becomes U+FFFD, one per bad byte, as recommended for
+ * the maximal subpart of an ill-formed subsequence in Unicode TR#36.  Code
+ * points that would break a line become a space.  The result is therefore
+ * always well-formed and always printable on one line, which means a caller
+ * never has to fall back to the ASCII field because a name was hostile.
+ *
+ * Surrounding whitespace is dropped and the result is cut at a character
+ * boundary, never inside a sequence.  Returns NULL when nothing usable is
+ * left, which the caller treats as "unset".
+ */
+static char *snd_card_name_dup(const char *src, size_t max)
+{
+	const unsigned char *s = (const unsigned char *)src;
+	size_t avail = strlen(src);
+	size_t in = 0, out = 0, start = 0;
+	char *buf, *shrunk;
+	size_t n;
+	u32 cp;
+
+	buf = kmalloc(max + 1, GFP_KERNEL);
+	if (!buf)
+		return ERR_PTR(-ENOMEM);
+
+	while (in < avail) {
+		n = utf8_decode(s + in, avail - in, &cp);
+		if (!n) {
+			if (out + sizeof(UTF8_REPLACEMENT) - 1 > max)
+				break;
+			memcpy(buf + out, UTF8_REPLACEMENT,
+			       sizeof(UTF8_REPLACEMENT) - 1);
+			out += sizeof(UTF8_REPLACEMENT) - 1;
+			in++;
+			continue;
+		}
+		if (utf8_needs_space(cp)) {
+			if (out + 1 > max)
+				break;
+			buf[out++] = ' ';
+			in += n;
+			continue;
+		}
+		if (out + n > max)
+			break;		/* cut at a character boundary */
+		memcpy(buf + out, s + in, n);
+		out += n;
+		in += n;
+	}
+
+	/* drop surrounding whitespace, including what the replacing above may
+	 * have produced at either end
+	 */
+	while (start < out && utf8_is_ascii_space(buf[start]))
+		start++;
+	while (out > start && utf8_is_ascii_space(buf[out - 1]))
+		out--;
+	if (out == start) {
+		kfree(buf);
+		return NULL;
+	}
+	out -= start;
+	memmove(buf, buf + start, out);
+	buf[out] = '\0';
+
+	shrunk = krealloc(buf, out + 1, GFP_KERNEL);
+	return shrunk ? : buf;	/* shrinking does not fail, but do not rely on it */
+}
+
+static int snd_card_set_full_name(struct snd_card *card, char **dst,
+				  const char *ascii, const char *name,
+				  size_t max)
+{
+	char *copy = NULL, *old;
+
+	if (name) {
+		copy = snd_card_name_dup(name, max);
+		if (IS_ERR(copy))
+			return PTR_ERR(copy);
+		/* Nothing gained over the ASCII field: leave the field unset so
+		 * the documented fallback does the same job without a second
+		 * allocation.  This keeps "set" meaning "carries more".
+		 */
+		if (copy && !strcmp(copy, ascii)) {
+			kfree(copy);
+			copy = NULL;
+		}
+	}
+
+	scoped_guard(rwsem_write, &snd_ioctl_rwsem) {
+		old = *dst;
+		*dst = copy;
+	}
+	kfree(old);
+	return 0;
+}
+
+static void snd_card_free_full_names(struct snd_card *card)
+{
+	kfree(card->full_shortname);
+	card->full_shortname = NULL;
+	kfree(card->full_longname);
+	card->full_longname = NULL;
+	kfree(card->full_mixername);
+	card->full_mixername = NULL;
+}
+
+/**
+ *  snd_card_set_full_shortname - set the UTF-8 short name of the card
+ *  @card: soundcard structure
+ *  @name: UTF-8 name, or NULL to clear the field
+ *
+ *  Offers the untruncated form of card->shortname.  Set card->shortname first,
+ *  with snd_utf8_strscpy(): if the sanitised name comes out identical to it,
+ *  the name fit and the field is left unset, so "set" always means "the fixed
+ *  field is cut".
+ *
+ *  @name is sanitised, never rejected: malformed sequences become U+FFFD and
+ *  line breaking code points become a space.  Surrounding whitespace is
+ *  stripped and a name longer than SNDRV_CARD_FULL_SHORTNAME_MAX bytes is cut
+ *  at a character boundary.  The caller should normalise @name to NFC
+ *  beforehand; the kernel cannot.  Call this before snd_card_register().
+ *
+ *  Return: Zero on success, -ENOMEM on allocation failure.
+ */
+int snd_card_set_full_shortname(struct snd_card *card, const char *name)
+{
+	return snd_card_set_full_name(card, &card->full_shortname,
+				      card->shortname, name,
+				      SNDRV_CARD_FULL_SHORTNAME_MAX);
+}
+EXPORT_SYMBOL_GPL(snd_card_set_full_shortname);
+
+/**
+ *  snd_card_set_full_longname - set the UTF-8 long name of the card
+ *  @card: soundcard structure
+ *  @name: UTF-8 name, or NULL to clear the field
+ *
+ *  Offers the untruncated form of card->longname; see
+ *  snd_card_set_full_shortname() for the rules that apply.
+ *
+ *  Return: Zero on success, -ENOMEM on allocation failure.
+ */
+int snd_card_set_full_longname(struct snd_card *card, const char *name)
+{
+	return snd_card_set_full_name(card, &card->full_longname,
+				      card->longname, name,
+				      SNDRV_CARD_FULL_LONGNAME_MAX);
+}
+EXPORT_SYMBOL_GPL(snd_card_set_full_longname);
+
+/**
+ *  snd_card_set_full_mixername - set the UTF-8 mixer name of the card
+ *  @card: soundcard structure
+ *  @name: UTF-8 name, or NULL to clear the field
+ *
+ *  Offers the untruncated form of card->mixername; see
+ *  snd_card_set_full_shortname() for the rules that apply.
+ *
+ *  Return: Zero on success, -ENOMEM on allocation failure.
+ */
+int snd_card_set_full_mixername(struct snd_card *card, const char *name)
+{
+	return snd_card_set_full_name(card, &card->full_mixername,
+				      card->mixername, name,
+				      SNDRV_CARD_FULL_MIXERNAME_MAX);
+}
+EXPORT_SYMBOL_GPL(snd_card_set_full_mixername);
+
 static ssize_t id_show(struct device *dev,
 		       struct device_attribute *attr, char *buf)
 {
-- 
2.43.0


  parent reply	other threads:[~2026-09-15 15:59 UTC|newest]

Thread overview: 9+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-15 15:59 [RFC PATCH 0/8] ALSA: untruncated " Luca Rodenhäuser
2026-09-15 15:59 ` [RFC PATCH 1/8] ALSA: core: keep non-ASCII bytes out of the card id Luca Rodenhäuser
2026-09-15 15:59 ` [RFC PATCH 2/8] ALSA: control: copy the card bytes outside snd_ioctl_rwsem Luca Rodenhäuser
2026-09-15 15:59 ` [RFC PATCH 3/8] ALSA: usb-audio: don't cut the card name inside a UTF-8 character Luca Rodenhäuser
2026-09-15 15:59 ` Luca Rodenhäuser [this message]
2026-09-15 15:59 ` [RFC PATCH 5/8] ALSA: control: return the untruncated card names via CARD_BYTES Luca Rodenhäuser
2026-09-15 15:59 ` [RFC PATCH 6/8] ALSA: core: add KUnit coverage for the card names Luca Rodenhäuser
2026-09-15 15:59 ` [RFC PATCH 7/8] ALSA: usb-audio: offer the untruncated " Luca Rodenhäuser
2026-09-15 15:59 ` [RFC PATCH 8/8] ALSA: Documentation: describe the card name fields Luca Rodenhäuser

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=AMBP191MB2886BF75AD878F97C3CC8D3FCEBA2@AMBP191MB2886.EURP191.PROD.OUTLOOK.COM \
    --to=otzelot2021@outlook.de \
    --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®