September 17, 2026
I Found a Stack Buffer Overflow in ALSA-lib, The Audio Library Running on Every Linux Machine |โฆ
CVE-2026โ90781 | An incomplete fix, two missed loops, and a same-day patch from the man who wrote it

By Harsh Raj Singhania
7 min read
CVE-2026โ90781 | An incomplete fix, two missed loops, and a same-day patch from the man who wrote it
Across eight posts now, I've written about bugs in Perl, Rust, C++, PHP, and Python. This one is C. And if you're reading this on Linux, you've already used the library it's in today โ probably within the last five minutes.
What ALSA-lib Actually Is
ALSA stands for Advanced Linux Sound Architecture. The kernel half of it is the audio subsystem sitting inside the Linux kernel itself. ALSA-lib is the userspace half โ the library that applications call to talk to that kernel subsystem.
PipeWire uses it. PulseAudio uses it. JACK uses it. alsamixer, the terminal tool you use to adjust volume, is built on it. The alsactl restore command that loads your saved audio settings calls it. Every time you plug headphones into a Linux laptop, some part of this library is involved.
If you look at the copyright header at the top of the vulnerable file, it reads:
Copyright (c) 2010 by Jaroslav Kysela <perex@perex.cz>Copyright (c) 2010 by Jaroslav Kysela <perex@perex.cz>Jaroslav Kysela wrote ALSA. He's been maintaining it for over two decades. He's also the person who fixed this bug, the same day I reported it.
That's the scale of what we're talking about.
What This Parser Does
Inside ALSA, every controllable parameter of an audio device โ volume, mute switch, EQ band, microphone gain โ is called a control element. Each one has a text identifier, something like:
numid=1,iface=MIXER,name='Master Playback Volume'numid=1,iface=MIXER,name='Master Playback Volume'The function snd_ctl_ascii_elem_id_parse() converts that string into an internal struct the library can work with. It's a public API, called from alsamixer, from alsactl, from ALSA's Use Case Manager (UCM), from control remapping โ basically anything that needs to translate a human-readable description of an audio control into something the kernel understands.
The internal implementation that does the actual parsing is __snd_ctl_ascii_elem_id_parse() in src/control/ctlparse.c. That's where the bug is.
PR #509: "Fix Theoretical One-byte Buffer Overrun"
Before getting to the bug itself, you need to know that the maintainer already found it.
Someone opened PR #509 with exactly that title. They identified that the numid= parsing loop inside __snd_ctl_ascii_elem_id_parse() had an off-by-one condition. The fix was a one-character change:
// Before PR #509
if (size < (int)sizeof(buf))
// After PR #509
if (size < (int)sizeof(buf) - 1)// Before PR #509
if (size < (int)sizeof(buf))
// After PR #509
if (size < (int)sizeof(buf) - 1)They called it "theoretical." They filed it. They closed it. The numid= loop was fixed.
And right below it, the two name= loops had the exact same condition, untouched.
The Vulnerable Code
__snd_ctl_ascii_elem_id_parse() declares a 64-byte stack buffer at the top:
char buf[64];char buf[64];Valid indices: buf[0] through buf[63]. Then it maintains a pointer ptr and a counter size, and parses each field into that buffer character by character. After the loop, it always writes a terminating NUL:
*ptr = '\0';*ptr = '\0';Here are the two name= parsing loops as they existed in the vulnerable source:
Quoted string variant (when the name is wrapped in quotes, like name='Master Volume'):
while (*str && *str != c) {
if (size < (int)sizeof(buf)) { // BUG: should be sizeof(buf) - 1
*ptr++ = *str;
size++;
}
str++;
}
// ...
*ptr = '\0';while (*str && *str != c) {
if (size < (int)sizeof(buf)) { // BUG: should be sizeof(buf) - 1
*ptr++ = *str;
size++;
}
str++;
}
// ...
*ptr = '\0';Unquoted string variant (bare name without quotes):
while (*str && *str != ',') {
if (size < (int)sizeof(buf)) { // BUG: should be sizeof(buf) - 1
*ptr++ = *str;
size++;
}
str++;
}
// ...
*ptr = '\0';while (*str && *str != ',') {
if (size < (int)sizeof(buf)) { // BUG: should be sizeof(buf) - 1
*ptr++ = *str;
size++;
}
str++;
}
// ...
*ptr = '\0';Now compare that to the already-fixed numid= loop in the development source at the time of my report:
while (*str && *str != ',') {
if (size < (int)sizeof(buf) - 1) { // CORRECT
*ptr++ = *str;
size++;
}
str++;
}
*ptr = '\0';while (*str && *str != ',') {
if (size < (int)sizeof(buf) - 1) { // CORRECT
*ptr++ = *str;
size++;
}
str++;
}
*ptr = '\0';One character difference. - 1. That's it. Applied to the numid= loop, not to the two name= loops sitting in the same function.
Why the Condition Is Wrong
Walk through the arithmetic once and it's obvious.
sizeof(buf) is 64. The loop permits writing while size < 64, so when size equals 63, the character goes in, size becomes 64, ptr advances to buf + 64. The loop then exits โ either the string ended or the delimiter was hit.
Then the code executes *ptr = '\0'. With ptr == buf + 64, that's writing to buf[64]. But buf only owns indices 0 through 63. buf[64] is one byte past the end of the buffer.
With sizeof(buf) - 1, the condition becomes size < 63. The 64th character is rejected. ptr never reaches buf + 64. The NUL writes to buf[63] โ the last valid byte. Safe.
That's the whole bug. One missing - 1, in two places.
Proving It With ASan
I built a standalone harness using the exact same parsing logic from ctlparse.c, unchanged, and ran it with AddressSanitizer. The trigger for the unquoted variant is simple:
// 64 A's โ fills the buffer, then the NUL goes one past the end
snd_ctl_ascii_elem_id_parse(&id,
"name=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
);// 64 A's โ fills the buffer, then the NUL goes one past the end
snd_ctl_ascii_elem_id_parse(&id,
"name=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
);ASan output:
ERROR: AddressSanitizer: stack-buffer-overflow
WRITE of size 1 at 0x7f1d540000e0 thread T0
#0 parse_name_unquoted_vulnerable /tmp/alsa_poc.c:34
#1 main /tmp/alsa_poc.c:97
[32, 96) 'buf' (line 22) <== Memory access at offset 96 overflows this variableERROR: AddressSanitizer: stack-buffer-overflow
WRITE of size 1 at 0x7f1d540000e0 thread T0
#0 parse_name_unquoted_vulnerable /tmp/alsa_poc.c:34
#1 main /tmp/alsa_poc.c:97
[32, 96) 'buf' (line 22) <== Memory access at offset 96 overflows this variableThe [32, 96) range tells you buf occupies 64 bytes of stack space starting at offset 32. The access is at offset 96, which is offset 32 + 64 โ the first byte past the end of buf. ASan calls this the "Stack right redzone," the poison zone it places immediately after each stack object. Hitting it confirms exactly where the write lands.
I tested the quoted variant separately with 64 B's wrapped in single quotes:
ERROR: AddressSanitizer: stack-buffer-overflow
WRITE of size 1
[32, 96) 'buf' <== Memory access at offset 96 overflows this variableERROR: AddressSanitizer: stack-buffer-overflow
WRITE of size 1
[32, 96) 'buf' <== Memory access at offset 96 overflows this variableSame report. Same offset. Same one-byte OOB write. Two independent code paths, both confirmed.
For the fixed version, I ran the same 64-character payload through the corrected condition:
Fixed path: size capped at 63, NUL writes to buf[63] โ no ASan report.Fixed path: size capped at 63, NUL writes to buf[63] โ no ASan report.Where This Is Actually Reachable
The public API wrapper, snd_ctl_ascii_elem_id_parse(), is called all over the ALSA userspace stack. But the most interesting reachability path is UCM โ ALSA's Use Case Manager.
UCM configuration files ship with hardware support for audio devices. If you have a laptop on Linux, your distro's alsa-ucm-conf package contains a UCM config for your specific hardware model. Those configs contain cset directives that look like this:
cset "name='Headphone Jack Switch' on"cset "name='Headphone Jack Switch' on"That string goes through __snd_ctl_ascii_elem_id_parse(). PipeWire and PulseAudio load UCM configs when audio devices are connected. A malformed UCM configuration with a name= value 64 characters long reaches the vulnerable path directly.
This is also reachable through alsactl restore, which reads saved audio state from a file. And through alsamixer, command line arguments. The parser is not buried in a corner of the library that nothing calls โ it's used in the normal audio device configuration workflow.
What This Is And What It Isn't
I want to be precise here, because a "stack buffer overflow in ALSA-lib" sounds scarier than what the evidence actually shows.
The write is one byte. The written value is always '\0'. It always lands at the same address relative to buf. This is not an arbitrary write โ you can't choose what goes there or exactly where relative to other stack objects it lands.
With stack canaries enabled, which is the default for every major Linux distribution, a function return after this write triggers the canary check, which aborts the process. The realistic demonstrated outcome is a crash โ denial of service.
Without stack canaries, or in a configuration where the overwritten byte happens to land in a non-critical neighboring stack slot, the behavior could be silent memory corruption. But I didn't demonstrate code execution and I'm not going to claim it.
CVSS 4.8 Medium is the right score for what this actually is. I recommended 5.5 in the original submission, VulnCheck published it at 4.8. That's a defensible call.
The reason this still deserves a CVE โ and the reason it's worth writing about โ isn't the raw severity number. It's the provenance. This is a confirmed stack buffer overflow in a library that every Linux audio stack depends on. The class of bug had already been identified and fixed in the same function. Two instances of it were missed. That's a clean, well-evidenced finding even at a Medium score.
The Maintainer
I emailed Jaroslav Kysela on August 29. He committed the fix the same day:
f84cd4ced7b36fddb8e4ee24404cf7c091d27020
The reply was one sentence. "Thanks. Fixed."
The fix is exactly four characters added in two places, - 1, to both name= loops, mirroring the correction PR #509 had already made to the numid= loop. That's it. The man who wrote this library in 2010, who's been maintaining it since, looked at the report and had it done in hours.
CVE-2026โ56109 Is Not This
VulnCheck published another ALSA-lib CVE in 2026 โ CVE-2026โ56109, a double-free in parse_def() inside src/conf.c. Completely different file. Completely different function. Completely different bug class. If you've read about ALSA-lib security issues this year, that's the other one. This is CVE-2026-90781, src/control/ctlparse.c, __snd_ctl_ascii_elem_id_parse(), off-by-one in two name= loops.
The Habit
Nine posts now, seven languages and runtimes, a different project each time. They all started the same way. Something in the existing code or an existing fix implied that the problem was handled. I checked whether that was actually true.
PR #509 said "fix theoretical one-byte buffer overrun." The word "theoretical" stuck, but more than that, the fix was right. It was just applied to one out of three places where the same condition existed. Checking the neighbors takes maybe ten minutes once you understand what you're looking at. It found two more.
The overflow PR #509 fixed was called theoretical. It wasn't. The two they missed alongside it weren't either.
Nine posts, seven confirmed CVEs. The next one is coming. If something here is wrong, the comments are open.
References
- VulnCheck advisory for CVE-2026โ90781: https://www.vulncheck.com/advisories/alsa-lib-through-1.2.16.1-off-by-one-stack-buffer-overflow-in-snd-ctl-ascii-elem-id-parse
- Vulnerable source at v1.2.16.1 (lines 216โ241): https://github.com/alsa-project/alsa-lib/blob/v1.2.16.1/src/control/ctlparse.c#L216-L241
- Fix commit
f84cd4ce: https://github.com/alsa-project/alsa-lib/commit/f84cd4ced7b36fddb8e4ee24404cf7c091d27020 - alsa-devel mailing list โ original report thread: https://lore.kernel.org/alsa-devel/CACBQ=P2FhO3M6dkv3cWuKb6Qhs92ouV+FJ3SJZ_PVBSSdJWRAQ@mail.gmail.com/
- ALSA-lib GitHub repository: https://github.com/alsa-project/alsa-lib
- CVE-2026โ56109 (different bug, different file โ not this one): https://nvd.nist.gov/vuln/detail/CVE-2026-56109
- My previous post, CVE-2026โ89267 in starlette-admin: https://medium.com/@harshrajsinghania/the-fix-introduced-an-asymmetry-and-the-asymmetry-was-the-bug
- My post on CVE-2026โ90781 sibling, mrubyc: https://medium.com/@harshrajsinghania/i-found-a-dos-vulnerability-by-asking-what-about-the-next-function-mruby-c-cve-2026-85647
- My first post, CVE-2026โ73194: https://medium.com/@harshrajsinghania/i-found-my-first-cve-by-trying-to-understand-someone-elses-bug-the-story-of-cve-2026-73194-1702f8bde5c1