https://sourceware.org/bugzilla/show_bug.cgi?id=34049

            Bug ID: 34049
           Summary: Heap-Buffer-Overflow WRITE in xcoff_link_add_symbols()
           Product: binutils
           Version: 2.46
            Status: UNCONFIRMED
          Severity: normal
          Priority: P2
         Component: ld
          Assignee: unassigned at sourceware dot org
          Reporter: takaosato1997 at gmail dot com
  Target Milestone: ---

Created attachment 16679
  --> https://sourceware.org/bugzilla/attachment.cgi?id=16679&action=edit
input payload file

## Summary

A heap-buffer-overflow (WRITE) vulnerability was found in `bfd/xcofflink.c` in
the
function `xcoff_link_add_symbols()`. The bug is triggered by a malformed XCOFF
32-bit
object file where a BFD section has a `target_index` value that exceeds
`abfd->section_count`. This causes an out-of-bounds write into the heap chunk
immediately following the `reloc_info` array, corrupting heap metadata. The
crash is
reproducible in an optimized release build (SIGABRT, "double free or corruption
(out)").

| Field         | Value                                          |
|---------------|------------------------------------------------|
| Component     | GNU Binutils — libbfd / XCOFF linker           |
| File          | bfd/xcofflink.c                                |
| Function      | xcoff_link_add_symbols()                       |
| Line          | 1416 (also 1420, 1440)                         |
| Bug type      | Heap-buffer-overflow — WRITE (CWE-122)         |
| Affected      | binutils 2.46, HEAD (confirmed unpatched)      |
| Trigger       | Any build of ld with XCOFF target support      |
| PoC           | 100-byte malformed XCOFF .o file               |
| Duplicate?    | No — confirmed not in known CVE list           |

---

## Root Cause

In `xcoff_link_add_symbols()`, `reloc_info` is allocated at **line 1401**:

```c
amt = abfd->section_count + 1;       // e.g. section_count=1 → 2 entries
amt *= sizeof (struct reloc_info_struct);
reloc_info = bfd_zmalloc (amt);      // 48 bytes (2 × 24)
```

`struct reloc_info_struct` has 3 pointer fields (24 bytes each entry):
```c
struct reloc_info_struct {
    struct internal_reloc *relocs;   // +0
    asection             **csects;   // +8
    bfd_byte              *linenos;  // +16
};
```

Later at **line 1410**, the code iterates over `abfd->sections` and writes:

```c
for (o = abfd->sections; o != NULL; o = o->next) {
    if ((o->flags & SEC_RELOC) != 0) {
        reloc_info[o->target_index].relocs = ...;  // LINE 1416 ↠OOB WRITE
        reloc_info[o->target_index].csects = ...;  // LINE 1420 ↠OOB WRITE
    }
    if (o->lineno_count > 0) {
        reloc_info[o->target_index].linenos = ...; // LINE 1440 ↠OOB WRITE
    }
}
```

**The mismatch:** When a malformed XCOFF file causes one section
(target_index=1) to
be removed from `abfd->sections` during parsing (decrementing `section_count`
from 2
to 1), while the surviving section retains its original `target_index=2`. The
allocation uses `section_count+1=2` entries (indices 0..1), but the surviving
section
accesses `reloc_info[2]` — **one entry past the end of the buffer**.

### GDB-confirmed values at crash point:
```
abfd->section_count   = 1        ↠allocates 2 entries (indices 0 and 1)
o->target_index       = 2        ↠WRITE at index 2 → OOB
reloc_info size       = 48 bytes ↠2 × struct reloc_info_struct
OOB write             = WRITE of 8 bytes at reloc_info + 48 (0 bytes past end)
```

---

## ASAN Report

```
ERROR: AddressSanitizer: heap-buffer-overflow on address 0x504000000780
WRITE of size 8 at 0x504000000780 thread T0
    #0 xcoff_link_add_symbols  bfd/xcofflink.c:1416
    #1 xcoff_link_add_object_symbols              :2381
    #2 _bfd_xcoff_bfd_link_add_symbols            :2600
    #3 load_symbols             ld/ldlang.c:3223
    ...

0x504000000780 is located 0 bytes after 48-byte region
[0x504000000750,0x504000000780)
allocated by thread T0:
    #0 bfd_zmalloc              bfd/libbfd.c:413
    #1 xcoff_link_add_symbols   bfd/xcofflink.c:1403
```

---

## Release Build Behavior

Without ASAN, the overflow writes a heap pointer value into the `prev_size`
field of
the adjacent heap chunk. The subsequent write (line 1420) overwrites that
chunk's
**size field** with a second heap pointer. When `free()` is later called on the
adjacent chunk, glibc's heap consistency check fails:

```
double free or corruption (out)
Aborted (core dumped)
[exit code 134]
```

---

## PoC

**Attached:** `crash_min.o` — 100-byte malformed XCOFF object. Run with: `ld
-o /dev/null crash_min.o`

### Reproduce (any ld built with --enable-targets=all):

```bash
# ASAN build — exact stack trace:
ASAN_OPTIONS="halt_on_error=0:print_stacktrace=1" \
  ld -o /dev/null crash_min.o

# Standard build — glibc detects heap corruption:
ld -o /dev/null crash_min.o
# → "double free or corruption (out)" + SIGABRT (exit 134)
```

### Expected ASAN output:

```
ERROR: AddressSanitizer: heap-buffer-overflow on address 0x504000000780
WRITE of size 8 at 0x504000000780 thread T0
    #0 xcoff_link_add_symbols  bfd/xcofflink.c:1416
    #1 xcoff_link_add_object_symbols              :2381
    #2 _bfd_xcoff_bfd_link_add_symbols            :2600
    #3 load_symbols             ld/ldlang.c:3223

0x504000000780 is located 0 bytes after 48-byte region
[0x504000000750,0x504000000780)
allocated by thread T0:
    #0 bfd_zmalloc  bfd/libbfd.c:413
    #1 xcoff_link_add_symbols  bfd/xcofflink.c:1403
```

### File structure of PoC (crash_min.o, 100 bytes):
```
XCOFF 32-bit (magic 0x01DF), f_nscns=2
  Section 1: size=0 → BFD removes it from section list (section_count: 2→1)
  Section 2: s_nreloc=0x3030 → SEC_RELOC set → target_index=2 → OOB write
```

---

## Exploitability Analysis

### Two-phase bug — more dangerous than a single write

The vulnerability has two distinct dangerous phases within the same function
call:

**Phase 1 — OOB WRITE (lines 1416, 1420, 1440):**
```
reloc_info[target_index].relocs  = xcoff_read_internal_relocs(...)  ↠WRITE
heap ptr A
reloc_info[target_index].csects  = bfd_zmalloc(...)                 ↠WRITE
heap ptr B
reloc_info[target_index].linenos = linenos                          ↠WRITE
heap ptr C
```
These three writes land at `reloc_info + target_index×24`, past the end of the
buffer.
With gap=1, this is `reloc_info+48` — exactly the `prev_size` and `size`
fields of the
adjacent glibc heap chunk, corrupting its metadata.

**Phase 2 — OOB READ + `free()` of OOB-written pointers (lines
2262–2363):**
```
rel      = reloc_info[target_index].relocs;            ↠OOB READ (ptr A)
rel_csect = reloc_info[target_index].csects;           ↠OOB READ (ptr B)
free(reloc_info[target_index].csects);                 ↠free(ptr B) — line
2328
free(reloc_info[target_index].linenos);                ↠free(ptr C) — line
2347
free(reloc_info[target_index].csects);                 ↠free(ptr B again)
— line 2362
free(reloc_info[target_index].linenos);                ↠free(ptr C again)
— line 2363
```

**Phase 2 is reached in the release build.** The SIGABRT triggered by glibc's
`"double free or corruption (out)"` happens at line 2328 when `free()`
validates the
adjacent chunk whose header was corrupted in Phase 1.

### Write primitive
- **What is written**: Heap pointers from `xcoff_read_internal_relocs()` and
  `bfd_zmalloc()`. Not arbitrary bytes — but valid heap addresses.
- **Where it is written**: `reloc_info + target_index×24` bytes from buffer
start.
  With gap=1: lands in `prev_size`/`size` of the adjacent chunk (0 bytes past
end).
- **Offset control**: Partially controllable via the gap between `target_index`
and
  `section_count`. A larger gap writes further OOB (deeper into adjacent
allocations).

### CWE classification
- **CWE-122**: Heap-Based Buffer Overflow (WRITE) — Phase 1, confirmed
- **CWE-416**: Use After Free (potential) — Phase 2 `free()` on OOB-stored
pointers
- **CWE-415**: Double Free (potential) — lines 2362/2363 re-free the same OOB
values

### Can an attacker reach code execution?

**With the current PoC**: No demonstrated code execution. The Phase 1 write
corrupts
glibc chunk metadata, which causes Phase 2's `free()` to abort. glibc's
safe-linking
and chunk consistency checks prevent straightforward exploitation.

**Theoretical path to code execution** (non-trivial, not demonstrated):
1. Craft XCOFF with `target_index=N` large enough that OOB lands in a live heap
   object (not a chunk header) — bypassing immediate glibc detection.
2. The OOB READ in Phase 2 then dereferences the corrupted value as a pointer,
   potentially reading attacker-influenced data.
3. Phase 2's `free()` on ptr_B/ptr_C could be leveraged for a tcache poisoning
   primitive if ptr_B/ptr_C overlap with a tcache chunk.

This requires glibc-version-specific heap layout knowledge and is **not
demonstrated**
here, but the two-phase nature (write + subsequent free-of-OOB-data) classifies
this
as a higher-risk primitive than a simple null dereference or OOB read.

**The heap corruption is reliable and consistent across all tested build
configs.**

---

## Security Impact

| Aspect               | Assessment                                         |
|----------------------|----------------------------------------------------|
| Attack vector        | Local — supply chain (malicious .o in dependency)  |
| Trigger              | User runs `ld` on attacker-controlled .o file      |
| No XCOFF dev needed  | Any build of ld with `--enable-targets=all`        |
| Minimum impact       | Reliable crash/DoS of linker process               |
| Maximum impact       | Heap corruption → potential code execution         |
| Privilege escalation | Possible if ld runs with elevated privileges (CI)  |
| Supply chain risk    | High — .o files from packages processed on build   |

**Real-world scenario**: A developer adds a dependency package to a project.
That
package contains a malicious `.a` archive with a crafted XCOFF object file.
When
the project is built, `ld` processes the file and crashes (DoS) or potentially
executes attacker code in the context of the build process.

---

## Affected Versions

- **binutils 2.46** — confirmed
- **binutils HEAD** (master, 2026-04-06) — confirmed unpatched (identical
code)
- **Earlier versions** — likely affected (code dates back years)
- **Ubuntu 2.42** — NOT affected (XCOFF target not included)
- **Any `--enable-targets=all` build** — affected

---

## Not a Duplicate

Existing XCOFF CVEs in binutils:
- Bug #33919 / CVE pending — `xcoff_ppc_relocate_section` r_type OOB **READ**
(different function, different mechanism)
- Bug #24055 — `_bfd_xcoff_swap_aux_in` stack smash (different function)

This is the first report of the `xcoff_link_add_symbols`
section_count/target_index
mismatch leading to an OOB **WRITE**.

---

## Proposed Fix

Add a bounds check before the loop body. Two options:

**Option A — Reject malformed files early** (preferred):
```c
/* xcofflink.c, before the for loop at line 1410 */
for (o = abfd->sections; o != NULL; o = o->next)
  {
    last_real = o;
+   if ((unsigned int) o->target_index > abfd->section_count)
+     {
+       bfd_set_error (bfd_error_bad_value);
+       goto error_return;
+     }

    if ((o->flags & SEC_RELOC) != 0)
      {
        reloc_info[o->target_index].relocs = ...
```

**Option B — Allocate using max target_index** (safer):
```c
/* Before line 1401, scan all sections to find max target_index */
unsigned int max_idx = 0;
for (o = abfd->sections; o != NULL; o = o->next)
  if ((unsigned int) o->target_index > max_idx)
    max_idx = o->target_index;
amt = max_idx + 1;          /* instead of section_count + 1 */
amt *= sizeof (struct reloc_info_struct);
reloc_info = bfd_zmalloc (amt);
```

---

## Files

| File | Description |
|------|-------------|
| `crash_min.o` | 100-byte minimized PoC (AFL++ output) |
| `crash_original.o` | Original AFL++ crash file |
| `CVE_REPORT.md` | This report |

---

## CVSS v3.1 Score

**Vector**: `CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H`  
**Base Score**: **7.8 (HIGH)**

| Metric | Value | Rationale |
|--------|-------|-----------|
| Attack Vector | Local | Attacker provides malicious `.o`/`.a` file; victim
runs `ld` |
| Attack Complexity | Low | No race condition, no ASLR bypass required for DoS
|
| Privileges Required | None | Attacker needs no account on victim system |
| User Interaction | Required | Developer/CI must invoke `ld` on the crafted
file |
| Scope | Unchanged | Crash is within the `ld` process only |
| Confidentiality | High | Heap state leaked; heap pointer values written OOB |
| Integrity | High | Heap metadata corrupted; possible code execution |
| Availability | High | Reliable crash of linker (DoS) in all build configs |

> The 7.8 score matches prior XCOFF binutils CVEs (e.g. CVE-2023-1579 scored 
> 7.8).
> If the reporter wants to be conservative, a 6.5 (Medium) is defensible by 
> dropping I/C to Low.

---

## Suggested Bugzilla Report

**Product**: binutils  
**Component**: ld  
**Version**: 2.46  
**Summary**: heap-buffer-overflow WRITE in xcoff_link_add_symbols() via
malformed XCOFF object  
**Severity**: Normal  
**Priority**: P2  
**Keywords**: security  

### Bugzilla Body (paste verbatim):

```
Summary:
  heap-buffer-overflow WRITE in xcoff_link_add_symbols() —
bfd/xcofflink.c:1416

Affected versions: binutils 2.46, confirmed unpatched in HEAD (2026-04-06).
Bug type: CWE-122 — Heap-Based Buffer Overflow (WRITE)

== Root Cause ==

In xcoff_link_add_symbols() (bfd/xcofflink.c:1401–1440):

  amt = abfd->section_count + 1;
  reloc_info = bfd_zmalloc (amt * sizeof(struct reloc_info_struct));  /* line
1403 */

  for (o = abfd->sections; o != NULL; o = o->next) {
    if ((o->flags & SEC_RELOC) != 0) {
      reloc_info[o->target_index].relocs = ...;  /* line 1416 — OOB WRITE */
      reloc_info[o->target_index].csects = ...;  /* line 1420 — OOB WRITE */
    }
    if (o->lineno_count > 0) {
      reloc_info[o->target_index].linenos = ...;  /* line 1440 — OOB WRITE */
    }
  }

When a malformed XCOFF file causes BFD to remove one section from
abfd->sections
during parsing (decrementing section_count from 2→1), while the surviving
section
retains its original target_index=2, the allocation covers only indices 0..1
but
the write accesses index 2 — one entry (24 bytes) past the end.

== ASAN Output ==

  ERROR: AddressSanitizer: heap-buffer-overflow on address 0x504000000780
  WRITE of size 8 at pc xcoff_link_add_symbols bfd/xcofflink.c:1416
  0x504000000780 is located 0 bytes after 48-byte region
[0x504000000750,0x504000000780)
  allocated by bfd_zmalloc bfd/libbfd.c:413 ↠xcoff_link_add_symbols:1403

== Release Build Behavior ==

Without ASAN, the OOB write corrupts the size/prev_size fields of the adjacent
heap
chunk. glibc detects this during subsequent free():

  malloc: corrupted top size
  double free or corruption (out)
  Aborted (core dumped)    [exit 134 / SIGABRT]

== Reproduce ==

PoC attached (100 bytes, XCOFF 32-bit).

  # ASAN build:
  ASAN_OPTIONS="halt_on_error=0:print_stacktrace=1" \
    ld-new -o /dev/null crash_min.o

  # Release build:
  ld-new -o /dev/null crash_min.o
  → "double free or corruption (out)" / SIGABRT

  # Also triggers WITHOUT --gc-sections (pure XCOFF path):
  ld-new -o /dev/null crash_min.o

== Fix ==

Option A — bounds check before loop (preferred):

  for (o = abfd->sections; o != NULL; o = o->next)
    {
+     if ((unsigned int) o->target_index > abfd->section_count)
+       {
+         bfd_set_error (bfd_error_bad_value);
+         goto error_return;
+       }
      if ((o->flags & SEC_RELOC) != 0) { ...

Option B — allocate using max(target_index) instead of section_count.

== Not a Duplicate ==

Existing XCOFF bug #33919 is xcoff_ppc_relocate_section OOB READ (different
function).
Bug #24055 is _bfd_xcoff_swap_aux_in stack smash (different function).
This is the first reported xcoff_link_add_symbols section_count/target_index
WRITE.
```

-- 
You are receiving this mail because:
You are on the CC list for the bug.

Reply via email to