https://sourceware.org/bugzilla/show_bug.cgi?id=33878
Bug ID: 33878
Summary: # OOM in rust_demangle via unbounded lifetime count in
demangle_binder
Product: binutils
Version: 2.47 (HEAD)
Status: UNCONFIRMED
Severity: normal
Priority: P2
Component: binutils
Assignee: unassigned at sourceware dot org
Reporter: zesheng at tamu dot edu
Target Milestone: ---
## Summary
A crafted 61-byte Rust v0 mangled symbol causes `rust_demangle()` in
`libiberty/rust-demangle.c` to allocate >2GB of memory and crash. The
`demangle_binder()` function (line 920) parses `bound_lifetimes` from the input
as a base-62 integer with no upper bound, then loops that many times appending
lifetime strings to an output buffer that also has no size limit. This results
in ~35 million times memory amplification (61 bytes → 2GB+).
Any tool linked against libiberty that demangles untrusted symbols is affected:
c++filt, nm, objdump, addr2line, GDB, etc.
## Root Cause
### Primary: `demangle_binder()` uses an unchecked input-controlled loop count
```c
// libiberty/rust-demangle.c:920
static void
demangle_binder (struct rust_demangler *rdm)
{
uint64_t i, bound_lifetimes;
bound_lifetimes = parse_opt_integer_62 (rdm, 'G'); // ← attacker-controlled
if (bound_lifetimes > 0)
{
PRINT ("for<");
for (i = 0; i < bound_lifetimes; i++) // ← loops billions of times
{
if (i > 0)
PRINT (", ");
rdm->bound_lifetime_depth++;
print_lifetime_from_index (rdm, 1); // ← appends "'a", "'_123", etc.
}
PRINT ("> ");
}
}
```
`parse_opt_integer_62()` (line 176) decodes a base-62 integer from the mangled
symbol. 13 base-62 digits (only ~15 bytes of input) can represent values
exceeding 10^23. Even with `uint64_t` overflow, the resulting `bound_lifetimes`
is enormous, and the loop generates massive output.
### Secondary: `str_buf_reserve()` has no output size limit
```c
// libiberty/rust-demangle.c:1443
while (new_cap < min_new_cap)
{
new_cap *= 2; // doubles without upper bound
}
new_ptr = (char *)realloc (buf->ptr, new_cap); // OOM
```
The buffer keeps doubling until `realloc` fails at ~2GB, crashing the process.
### Why fixing only `str_buf_reserve()` is insufficient
Adding a size limit in `str_buf_reserve()` (e.g., setting `buf->errored = 1`
when exceeding a threshold) prevents OOM, **but the loop in `demangle_binder()`
continues running**. This is because:
1. `demangle_binder()` does not check `rdm->errored` inside the loop
2. `PRINT` calls `print_str()` which checks `rdm->errored`, not `buf->errored`
3. The callback `str_buf_demangle_callback()` calls `str_buf_append()`, which
returns early if `buf->errored`
Result: the buffer stops growing, but the process **hangs** in a loop that runs
for billions of iterations doing no-op writes. OOM is converted into an
infinite hang.
## Execution Trace
The PoC input (after stripping `_R` prefix):
```
INvC4te_C4tokpppppppppppFFFFFFGFpppppppppKj2_FFFFFFFFFFFFFE
```
Parsed as Rust v0 mangling:
```
I Generic instantiation <...>
NvC4te_C4tokp Nested path: te_C::tokp
p p p p p p p p p 9 placeholder type args (_)
F fn type #1 → demangle_binder checks for 'G': sees
'F', no binder
F fn type #2 → sees 'F', no binder
F fn type #3 → sees 'F', no binder
F fn type #4 → sees 'F', no binder
F fn type #5 → sees 'F', no binder
F fn type #6 → sees 'G': BINDER TRIGGERED
G FpppppppppKj2_ 'G' tag + base-62 integer = bound_lifetimes
F F F F F F F F F F F F F remaining function params
E terminator
```
The 6 nested `F` types reach a level where `demangle_binder()` encounters `G`.
It then decodes `FpppppppppKj2` (13 base-62 digits) as `bound_lifetimes` and
enters the loop. Each iteration appends lifetime text (`'a`, `'b`, ..., `'_26`,
`'_27`, ...) to the output buffer.
## PoC
```bash
# Generate crash input (61 bytes)
echo
"X1JJTnZDNHRlX0M0dG9rcHBwcHBwcHBwcHBGRkZGRkZHRnBwcHBwcHBwcEtqMl9GRkZGRkZGRkZGRkZGRQ=="
| base64 -d > crash_input.bin
# Reproduce with c++filt
echo '_RINvC4te_C4tokpppppppppppFFFFFFGFpppppppppKj2_FFFFFFFFFFFFFE' | c++filt
# Memory growth:
# 0s: ~10 MB
# 2s: ~180 MB
# 5s: ~393 MB
# 10s: ~823 MB → killed by OOM killer (exit 137)
```
### Stack trace (libFuzzer + ASAN)
```
==PID== ERROR: libFuzzer: out-of-memory (malloc(2147483648))
#0 in realloc
#1 in str_buf_reserve rust-demangle.c:1450
#2 in str_buf_append rust-demangle.c:1461
#3 in print_lifetime_from_index rust-demangle.c:880
#4 in demangle_binder rust-demangle.c:935
#5 in demangle_type rust-demangle.c (recursive)
```
## Suggested Fix
The fix should address the root cause (unbounded loop) rather than just the
symptom (buffer growth).
### Fix 1 (Primary): Limit `bound_lifetimes` in `demangle_binder()`
No valid Rust type has more than a handful of lifetime parameters. Adding a
sanity check eliminates the amplification at the source:
```c
static void
demangle_binder (struct rust_demangler *rdm)
{
uint64_t i, bound_lifetimes;
if (rdm->errored)
return;
bound_lifetimes = parse_opt_integer_62 (rdm, 'G');
/* No valid Rust type has this many lifetime parameters.
Reject malformed symbols to prevent output amplification. */
if (bound_lifetimes > 1024)
{
rdm->errored = 1;
return;
}
if (bound_lifetimes > 0)
{
PRINT ("for<");
for (i = 0; i < bound_lifetimes; i++)
{
if (i > 0)
PRINT (", ");
rdm->bound_lifetime_depth++;
print_lifetime_from_index (rdm, 1);
}
PRINT ("> ");
}
}
```
### Fix 2 (Defense-in-depth): Output size limit in `str_buf_reserve()`
As a secondary safeguard against similar amplification bugs in other code
paths, cap the output buffer and propagate the error to `rdm`:
```c
/* Add before the realloc call in str_buf_reserve(): */
if (new_cap > 16 * 1024 * 1024) /* 16 MB max output */
{
buf->errored = 1;
return;
}
```
Fix 2 alone is insufficient (see "Why fixing only str_buf_reserve() is
insufficient" above) but provides defense-in-depth when combined with Fix 1.
## Impact
| Aspect | Details |
|--------|---------|
| **Type** | Denial of Service (DoS) via OOM |
| **CWE** | CWE-400 (Uncontrolled Resource Consumption) |
| **CVSS 3.1** | 5.5 Medium (`CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H`) |
| **Input size** | 61 bytes |
| **Amplification** | ~35,000,000x (61 bytes → 2GB+) |
| **Attack vector** | Crafted ELF/object file with malicious Rust symbol names
|
| **Affected tools** | c++filt, nm, objdump, addr2line, GDB, Ghidra, etc. |
## Related
- Ghidra Security Advisory:
[GHSA-m94m-fqr3-x442](https://github.com/NationalSecurityAgency/ghidra/security/advisories/GHSA-m94m-fqr3-x442)
- Ghidra's GPL/DemanglerGnu v2.41 bundles a copy of this libiberty code
- `cplus_demangle()` dispatches `_R`-prefixed symbols to `rust_demangle()`, so
the default demangling mode is also affected
---
Contribution: This vulnerability was found by AI-Based Vuln Detection system
FuzzingBrain https://github.com/o2lab/afc-crs-all-you-need-is-a-fuzzing-brain.
--
You are receiving this mail because:
You are on the CC list for the bug.