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.