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.

Reply via email to