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

            Bug ID: 34342
           Summary: objdump: SIGSEGV (OOB read) in ctf_func_type_info
                    (libctf/ctf-types.c:1603)
           Product: binutils
           Version: 2.47 (HEAD)
            Status: UNCONFIRMED
          Severity: normal
          Priority: P2
         Component: binutils
          Assignee: unassigned at sourceware dot org
          Reporter: lswang1112 at gmail dot com
  Target Milestone: ---

Created attachment 16813
  --> https://sourceware.org/bugzilla/attachment.cgi?id=16813&action=edit
Minimal 360-byte hand-crafted ELF64 with a .ctf section containing a
CTF_K_FUNCTION type whose VLEN is set to 0x100000, triggering the OOB read.

Affected version: GNU Binutils 2.46.50.20260702
(upstream HEAD, git commit 7431e2b766ea7defd2db07c53a39fdd57606b691)

------------------------------------------------------------------------
ROOT CAUSE
------------------------------------------------------------------------

ctf_func_type_info() (libctf/ctf-types.c) fills in a ctf_funcinfo_t for a
CTF_K_FUNCTION type. It reads the function's argument count directly out
of the (attacker-controlled) CTF type's ctt_info field, then uses that
count to index into the argument-id array immediately following the type
record -- with no check that the array actually holds that many entries:

    /* libctf/ctf-types.c:1572-1610 */
    int
    ctf_func_type_info (ctf_dict_t *fp, ctf_id_t type, ctf_funcinfo_t *fip)
    {
      ctf_dict_t *ofp = fp;
      const ctf_type_t *tp;
      uint32_t kind;
      const uint32_t *args;
      const ctf_dtdef_t *dtd;
      ssize_t size, increment;
      ...
      (void) ctf_get_ctt_size (fp, tp, &size, &increment);
      kind = LCTF_INFO_KIND (fp, tp->ctt_info);

      if (kind != CTF_K_FUNCTION)
        return (ctf_set_errno (ofp, ECTF_NOTFUNC));

      fip->ctc_return = tp->ctt_type;
      fip->ctc_flags = 0;
      fip->ctc_argc = LCTF_INFO_VLEN (fp, tp->ctt_info);   /* fully
attacker-controlled */

      if ((dtd = ctf_dynamic_type (fp, type)) == NULL)
        args = (uint32_t *) ((uintptr_t) tp + increment);  /* points into raw
CTF section */
      else
        args = (uint32_t *) dtd->dtd_vlen;

      if (fip->ctc_argc != 0 && args[fip->ctc_argc - 1] == 0)   /* line 1603 --
OOB read */
        {
          fip->ctc_flags |= CTF_FUNC_VARARG;
          fip->ctc_argc--;
        }

      return 0;
    }

ctc_argc is the VLEN bitfield of the type's ctt_info (up to
CTF_MAX_VLEN = 0xffffff = 16,777,215 for a v2/v3 dict), taken verbatim
from the CTF type table with no validation against how much argument
data the type record actually carries or against the bounds of the CTF
section buffer. For a type that was loaded from the file (not built up
dynamically, i.e. ctf_dynamic_type() == NULL), args is simply
(uint32_t *) tp + increment -- a raw pointer into the mmap'd/malloc'd
CTF section data, with no length attached at this call site.

The single vararg-detection check at line 1603 -- args[ctc_argc - 1]
-- is the only place this unvalidated ctc_argc is used to compute an
array index. With a crafted CTF_K_FUNCTION type whose VLEN is set to a
huge value, that index reads far past the end of the CTF section (and
past the end of the file's mmap'd/loaded buffer entirely), landing on
unmapped memory and crashing with SIGSEGV. In the attached PoC,
VLEN = 0x100000 (1,048,576), so args[0xfffff] reads ~4 MB past the
start of args, far beyond the CTF section's actual size (28 bytes of
type data).

Call chain (confirmed via gdb on upstream HEAD 2.46.50.20260702):

    objdump --ctf=.ctf poc.elf
      display_file                objdump.c:6071
      display_any_bfd             objdump.c:6050
      display_object_bfd          objdump.c:5971
      dump_bfd                    objdump.c:5897
      dump_ctf                    objdump.c:5016
      dump_ctf_archive_member     objdump.c:4916
      ctf_dump                    ctf-dump.c:752
      ctf_type_iter_all           ctf-types.c:412
      ctf_dump_type               ctf-dump.c:577
      ctf_dump_format_type        ctf-dump.c:115
      ctf_type_aname              ctf-types.c:743
      ctf_func_type_info          ctf-types.c:1603   <-- OOB read, SIGSEGV

------------------------------------------------------------------------
CRASH OUTPUT
------------------------------------------------------------------------

gdb backtrace at the fault, plain (non-instrumented) build:

    Program received signal SIGSEGV, Segmentation fault.
    0x000055555563c676 in ctf_func_type_info (fp=0x5555558c9360, type=2,
        fip=0x7fffffffd500) at ../../libctf/ctf-types.c:1603
    1603      if (fip->ctc_argc != 0 && args[fip->ctc_argc - 1] == 0)
    #0  ctf_func_type_info    ../../libctf/ctf-types.c:1603
    #1  ctf_type_aname        ../../libctf/ctf-types.c:743
    #2  ctf_dump_format_type  ../../libctf/ctf-dump.c:115
    #3  ctf_dump_type         ../../libctf/ctf-dump.c:577
    #4  ctf_type_iter_all     ../../libctf/ctf-types.c:412
    #5  ctf_dump              ../../libctf/ctf-dump.c:752
    #6  dump_ctf_archive_member  ../../binutils/objdump.c:4916
    #7  dump_ctf              ../../binutils/objdump.c:5016
    #8  dump_bfd              ../../binutils/objdump.c:5897
    #9  display_object_bfd    ../../binutils/objdump.c:5971
    #10 display_any_bfd       ../../binutils/objdump.c:6050
    #11 display_file          ../../binutils/objdump.c:6071
    #12 main                  ../../binutils/objdump.c:6494

    (gdb) print/x fip->ctc_argc
    $1 = 0x100000                   <-- the crafted VLEN, used as index

------------------------------------------------------------------------
HOW TO REPRODUCE
------------------------------------------------------------------------

Build from latest HEAD:

    git clone --depth 1 https://sourceware.org/git/binutils-gdb.git
    cd binutils-gdb
    mkdir build && cd build
    ../configure --disable-gdb --disable-gdbserver --disable-gdbsupport \
      --disable-sim --disable-gprofng CFLAGS="-g -O0"
    make -j$(nproc) all-binutils

The attached poc.elf is a hand-crafted, 360-byte minimal ELF64. It
contains nothing but a .ctf section holding a minimal CTF v3 dict with
exactly two types:

    type 1: CTF_K_INTEGER "int"                  (valid return-type anchor)
    type 2: CTF_K_FUNCTION, VLEN = 0x100000      (the crafted trigger)

The CTF type section is 28 bytes total. Type 2's VLEN field in ctt_info
claims 1,048,576 function arguments follow the type record, but only
5 bytes (the string table) actually remain in the buffer.
ctf_func_type_info() trusts this and reads args[0xfffff] -- ~4 MB past
the end of the mapped buffer -- landing on unmapped memory and crashing.

Run with:

    ./binutils/objdump --ctf=.ctf poc.elf

Under gdb:

    gdb -q -batch -ex run -ex bt \
      --args ./binutils/objdump --ctf=.ctf poc.elf

Plain (no-sanitizer) build crashes with SIGSEGV, as shown above.

------------------------------------------------------------------------
SUGGESTED FIX
------------------------------------------------------------------------

The real gap is not in ctf_func_type_info() itself but in the CTF dict
loader: init_static_types_internal() (libctf/ctf-open.c) already walks
every type record once at dict-open time to compute its vbytes (the
byte length of the record's variable-length payload -- for
CTF_K_FUNCTION, sizeof(uint32_t) * (vlen + (vlen & 1)), i.e. exactly the
argument-id array that ctf_func_type_info() later reads):

    /* libctf/ctf-open.c, init_static_types_internal(), first pass */
    tbuf = (ctf_type_t *) (fp->ctf_buf + cth->cth_typeoff);
    tend = (ctf_type_t *) (fp->ctf_buf + cth->cth_stroff);

    for (tp = tbuf; tp < tend; typemax++)
      {
        ...
        (void) ctf_get_ctt_size (fp, tp, &size, &increment);
        vbytes = LCTF_VBYTES (fp, kind, size, vlen);

        if (vbytes < 0)
          return ECTF_CORRUPT;

        tp = (ctf_type_t *) ((uintptr_t) tp + increment + vbytes);
        ...
      }

This loop already rejects a record when vbytes itself is negative
(overflow of the vlen*sizeof computation), but never checks that
increment + vbytes actually fits before tend -- it just advances tp past
tend and lets the tp < tend loop condition quietly stop. A record whose
declared VLEN makes it claim far more payload than the type section
actually has is accepted as the (silently truncated) last type in the
dict, and any later consumer (ctf_func_type_info() included) that walks
its payload has no way to know it is reading past the end of the buffer.

Suggested patch, closing the whole class of "VLEN inflated past the
type section" bugs at load time, not just this one call site:

    --- a/libctf/ctf-open.c
    +++ b/libctf/ctf-open.c
    @@ -748,6 +748,13 @@ init_static_types_internal (ctf_dict_t *fp,
ctf_header_t *cth,
           if (vbytes < 0)
             return ECTF_CORRUPT;

    +      /* Reject types whose variable-length data would run past the end
    +         of the type section: an attacker-controlled VLEN (e.g. a
    +         CTF_K_FUNCTION argument count) must not be allowed to make
    +         later readers walk off the end of the mapped CTF buffer.  */
    +      if ((uintptr_t) tend - (uintptr_t) tp < (uintptr_t) increment +
vbytes)
    +        return ECTF_CORRUPT;
    +
           /* For forward declarations, ctt_type is the CTF_K_* kind for the
tag,
              so bump that population count too.  */
           if (kind == CTF_K_FORWARD)

Verified: applied this patch to the 2026-07-02 upstream HEAD checkout,
rebuilt (make all-binutils), and re-ran:

  - poc.elf: patched objdump no longer crashes (exit code 1, no
    SIGSEGV). It now prints
      "error: ctf_arc_bufopen(): cannot open CTF"
      "CTF open failure: File data structure corruption detected."
    and exits cleanly, matching the outcome of any other malformed CTF
    section objdump already knows how to reject.

  - Regression check: built a small real object file with a genuine
    CTF_K_FUNCTION type (gcc -gctf -c t.c -o t.o, with a one-argument
    function) and ran objdump --ctf=.ctf t.o on both the patched and
    an unpatched build. Output is byte-for-byte identical on both
    (including the correctly decoded function signature), confirming the
    added bounds check does not affect well-formed CTF data.

------------------------------------------------------------------------
IMPACT
------------------------------------------------------------------------

Out-of-bounds read (CWE-125) in libctf, reachable from objdump's
--ctf=<section> option when the section it names holds a crafted CTF
type section containing a CTF_K_FUNCTION type with an inflated VLEN.
Reproducible impact is denial of service (SIGSEGV) on any file passed to
objdump --ctf=.ctf (or any other objdump flag combination that triggers
a CTF dump); a sufficiently large but still-mapped read offset could also
disclose adjacent heap/mmap memory before crashing.

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

Reply via email to