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

            Bug ID: 34333
           Summary: readelf: unbounded recursion in ctf_decl_push
                    (libctf/ctf-decl.c) causes stack overflow
           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 16807
  --> https://sourceware.org/bugzilla/attachment.cgi?id=16807&action=edit
PoC ELF (16736 bytes) with self-referential CTF_K_FUNCTION type triggering
ctf_decl_push infinite recursion

Affected version: GNU Binutils 2.46.50.20260629 (git HEAD 0e73a13c)
Confirmed reproduced on latest upstream HEAD as of 2026-06-30.

------------------------------------------------------------------------
SUMMARY
------------------------------------------------------------------------

A crafted ELF file with a .ctf section that encodes a self-referential
CTF type chain (e.g. a function or pointer type whose ctt_type field
points back to itself) causes ctf_decl_push() (libctf/ctf-decl.c:75)
to recurse without bound, exhausting the call stack. readelf --ctf or
objdump --ctf crashes with SIGSEGV (stack overflow) regardless of file
size or any other flag. No recursion limit or cycle-detection is
implemented for CTF type traversal.

Unlike the Rust demangler's stack overflow (which is explicitly
documented, with --no-recurse-limit required to disable the 2048-frame
guard), this recursion is entirely inside libctf's internal type-name
resolution and has no user-controllable limit at all.

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

ctf_decl_push() builds the type-name declaration stack for CTF type
printing. For composite types it recurses into their ctt_type member:

    /* libctf/ctf-decl.c:75 */
    static void
    ctf_decl_push (ctf_decl_t *cd, ctf_dict_t *fp, ctf_id_t type)
    {
      ...
      switch (kind = LCTF_INFO_KIND (fp, tp->ctt_info))
        {
        case CTF_K_ARRAY:
          ctf_decl_push (cd, fp, ar.ctr_contents);   /* recurse */
          break;
        case CTF_K_TYPEDEF:
          ctf_decl_push (cd, fp, tp->ctt_type);      /* recurse */
          return;
        case CTF_K_FUNCTION:
          ctf_decl_push (cd, fp, tp->ctt_type);      /* recurse */
          break;
        case CTF_K_POINTER:
          ctf_decl_push (cd, fp, tp->ctt_type);      /* recurse */
          break;
        case CTF_K_SLICE:
          ctf_decl_push (cd, fp, ctf_type_reference (fp, type));
          return;
        case CTF_K_VOLATILE:
        case CTF_K_CONST:
        case CTF_K_RESTRICT:
          ctf_decl_push (cd, fp, tp->ctt_type);      /* recurse */
          break;
        ...
        }
    }

If a CTF type's ctt_type field points back to itself (or to any cycle
in the type graph), ctf_decl_push calls itself infinitely. There is no
visited-type set, depth counter, or any other cycle-detection mechanism.

------------------------------------------------------------------------
ASAN OUTPUT
------------------------------------------------------------------------

AddressSanitizer detects the crash as stack-overflow after ~250 frames:

    SUMMARY: AddressSanitizer: stack-overflow in ctf_decl_push
        #0  ctf_lookup_by_id  libctf/ctf-lookup.c:331
        #1  ctf_decl_push     libctf/ctf-decl.c:85
        #2  ctf_decl_push     libctf/ctf-decl.c:119
        ...  [~250 identical ctf_decl_push frames]

GDB on a plain build confirms ~74 829 frames before SIGSEGV:

    Program received signal SIGSEGV, Segmentation fault.
    #0  ctf_lookup_by_id (...)         libctf/ctf-lookup.c:331
    #1  ctf_decl_push (cd=..., type=8) libctf/ctf-decl.c:85
    #2  ctf_decl_push (cd=..., type=8) libctf/ctf-decl.c:119
    #3  ctf_decl_push (cd=..., type=8) libctf/ctf-decl.c:119
    ...  [74829 identical frames]

Call chain:

    readelf --ctf=.ctf ...
      dump_section_as_ctf        readelf.c:17641
      dump_ctf_archive_member
      ctf_variable_iter
      ctf_type_aname             libctf/ctf-type.c
      ctf_decl_push              libctf/ctf-decl.c:75  <- infinite recursion

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

Build from latest HEAD (plain build -- no sanitizers needed; stack
overflow crashes any build):

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

The attached poc.elf is 16 736 bytes. Run with:

    ./binutils/readelf --ctf=.ctf --ctf-parent=.ctf \
        --ctf-strings=.strtab --ctf-symbols=.symtab poc.elf
    # Segmentation fault  (~74 000 recursive ctf_decl_push frames)

The PoC contains a .ctf section with a CTF archive whose type dictionary
encodes a self-referential CTF_K_FUNCTION type with ctt_type pointing
back to itself. When ctf_type_aname resolves this type for printing,
ctf_decl_push recurses indefinitely.

Note: objdump --ctf=.ctf poc.elf triggers the same crash via an
identical call chain.

------------------------------------------------------------------------
Build & Platform
------------------------------------------------------------------------

binutils version: GNU Binutils 2.46.50.20260629 (git HEAD 0e73a13c)
component: readelf / libctf
OS: Ubuntu 24.04.4 LTS
arch: x86_64

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

Add a depth counter to ctf_decl_t (libctf/ctf-impl.h) and a guard at
the top of ctf_decl_push. The counter must be decremented at every
return path to track actual recursion depth, not total calls.

Step 1: add cd_depth to struct ctf_decl in libctf/ctf-impl.h:

    typedef struct ctf_decl
    {
      ...
      int cd_err;       /* Saved error value.  */
      int cd_enomem;    /* Nonzero if OOM during printing.  */
    + int cd_depth;     /* Current recursion depth.  */
    } ctf_decl_t;

    /* cd_depth is zero-initialised by ctf_decl_init() via memset.  */

Step 2: add depth guard in ctf_decl_push (libctf/ctf-decl.c):

    static void
    ctf_decl_push (ctf_decl_t *cd, ctf_dict_t *fp, ctf_id_t type)
    {
    + #define CTF_DECL_PUSH_MAX_DEPTH 512
    + if (cd->cd_depth >= CTF_DECL_PUSH_MAX_DEPTH)
    +   { cd->cd_err = ECTF_CORRUPT; return; }
    + cd->cd_depth++;

      if ((tp = ctf_lookup_by_id (&fp, type)) == NULL)
        {
          cd->cd_err = fp->ctf_errno;
    +     cd->cd_depth--;
          return;
        }

      switch (kind = LCTF_INFO_KIND (fp, tp->ctt_info))
        {
        case CTF_K_TYPEDEF:       /* early return */
          ctf_decl_push (cd, fp, tp->ctt_type);
    +     cd->cd_depth--;
          return;

        case CTF_K_SLICE:         /* early return */
          ctf_decl_push (cd, fp, ctf_type_reference (fp, type));
    +     cd->cd_depth--;
          return;

        ...
        }

      if ((cdp = malloc (sizeof (ctf_decl_node_t))) == NULL)
        {
          cd->cd_err = EAGAIN;
    +     cd->cd_depth--;
          return;
        }

      /* ... append cdp to cd_nodes ... */
    + cd->cd_depth--;
    }

With this patch, the PoC is handled gracefully: self-referential types
are reported as "cannot format name dumping type 0xN" and readelf exits
cleanly with code 0 instead of crashing.

Alternatively, maintain a ctf_id_t visited-set within ctf_decl_t and
return ECTF_CORRUPT when a type ID is seen for the second time (proper
cycle detection without a fixed depth limit).

------------------------------------------------------------------------
DIFFERENCE FROM RUST DEMANGLER STACK OVERFLOW
------------------------------------------------------------------------

The Rust demangler in libiberty also has a stack overflow reachable from
readelf, but it is explicitly documented: --no-recurse-limit must be
passed to disable the 2048-frame default limit, and binutils SECURITY.txt
states that reports with that flag disabled are rejected.

ctf_decl_push has no equivalent limit. There is no option to enable
depth-limiting in libctf's type declaration printer, so the crash is
triggered with default flags and a plain --ctf=.ctf invocation.

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

Unbounded recursive call (CWE-674) in libctf triggered by a
self-referential CTF type in any ELF file. Stack exhaustion causes
SIGSEGV. Because libctf is also used internally by the linker (ld)
during CTF type merging, a crafted object file could trigger this crash
during a link operation, which may qualify as a security-relevant denial
of service under the Binutils security policy.

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

Reply via email to