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

            Bug ID: 34372
           Summary: objdump: CPU-exhaustion hang in dump_section() when
                    --section bypasses SEC_HAS_CONTENTS guard
                    (binutils/objdump.c:5089)
           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 16827
  --> https://sourceware.org/bugzilla/attachment.cgi?id=16827&action=edit
Minimal 1024-byte PE32 with .text VirtualSize=0x52000000 and no
SEC_HAS_CONTENTS; triggers CPU-exhaustion hang in dump_section

Affected version: GNU Binutils 2.46.1 (also confirmed on 2.46.50.20260707 git
HEAD)
Confirmed reproduced on 2.46.1 release.

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

dump_section() (binutils/objdump.c) dumps the raw hex content of an ELF
or PE section. It contains an early-exit guard that skips sections which
have no real on-disk content:

    /* objdump.c:5086 – dump_section (simplified) */
    static void
    dump_section (bfd *abfd, asection *section, void *dummy)
    {
      ...
      if (only_list == NULL)
        {
          /* No --section filter: skip sections without content. */
          if ((section->flags & SEC_HAS_CONTENTS) == 0)
            return;
        }
      else if (!process_section_p (section))
        return;   /* --section filter: process_section_p decides; no
                     SEC_HAS_CONTENTS check here! */
      ...
    }

When the user does NOT pass --section (only_list == NULL), the
SEC_HAS_CONTENTS flag is checked and sections with only a virtual size
(e.g. BSS or PE SectionVirtualSize without content) are skipped.

When --section=NAME IS passed (only_list != NULL), the code takes the
else branch. process_section_p() checks only whether the section name
matches — it does NOT check SEC_HAS_CONTENTS. A PE32 section that has
a large VirtualSize but carries no raw data (characteristics without the
IMAGE_SCN_CNT_INITIALIZED_DATA or IMAGE_SCN_CNT_CODE bits mapped by BFD
to SEC_HAS_CONTENTS) passes process_section_p() and execution falls
through to:

    bfd_get_full_section_contents (abfd, section, &data)

BFD honours the VirtualSize for content allocation even when the section
has no file data. With a crafted PE32 whose .text VirtualSize is
0x52000000 (1.28 GB), bfd_malloc allocates 1.28 GB, then the hex-dump
loop at the bottom of dump_section() iterates over ~85 million 16-byte
lines, consuming enormous CPU and memory before outputting the entire
1.28 GB of zeroes.

A companion bug: bfd_section_size_insane() (bfd.h) is also supposed to
detect unreasonably large sections, but its check requires SEC_HAS_CONTENTS
to be set, so it also returns false (safe) for this section — adding a
second layer of bypass.

Trigger combination:
  --section=.text   (only_list != NULL, causes bypass)
  --full-contents   (activates dump_section via dump_bfd)
  PE32 input where .text VirtualSize >> FileSize

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

There is no crash; the process hangs printing hex output until killed.

Reproduction evidence:

    $ timeout 3 objdump --disassemble=main -D --endian=big -F -M intel \
        -d -j .text -m i386:x86-64 -s poc_dump_section_hang.exe 2>&1 | tail -3
    Terminated
    $ echo $?
    143    (SIGTERM from timeout)

    $ ulimit -t 3; objdump ... poc_dump_section_hang.exe >/dev/null
    Killed
    $ echo $?
    137    (SIGKILL from CPU time limit — confirms CPU-bound loop)

In /proc/<PID>/status while running:
    State: R (running)   ← confirms purely CPU-bound

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

Download the source:

    # Release tarball (recommended):
    wget https://ftp.gnu.org/gnu/binutils/binutils-2.46.1.tar.xz

    # Or from git:
    git clone git://sourceware.org/git/binutils-gdb.git
    cd binutils-gdb && git checkout binutils-2_46_1

Build from the tarball:

    tar xf binutils-2.46.1.tar.xz && cd binutils-2.46.1
    mkdir build && cd build
    ../configure --disable-gdb --disable-gdbserver \
      --disable-gdbsupport --disable-sim CFLAGS="-g -O0"
    make -j$(nproc) all-binutils

The crafted PE32 input must:
  1. Be a valid minimal PE32 file (MZ + PE header + optional header +
     section table).
  2. Contain a section named ".text" where:
       VirtualSize (SizeOfRawData in the optional header) = 0x52000000
       SizeOfRawData (actual on-disk bytes)               = 0x00 or very small
       Characteristics                                     = 0x00020000
         (IMAGE_SCN_MEM_EXECUTE without CNT_CODE/CNT_INIT_DATA, so BFD
         does not set SEC_HAS_CONTENTS)

Reproduce:

    ./binutils/objdump \
      --disassemble=main --insn-width=8 -D --endian=big -F \
      -M intel -d -j .text -m i386:x86-64 -s poc_dump_section_hang.exe

The key flags are:
  -j .text   (equivalent to --section=.text — enables only_list path)
  -s         (equivalent to --full-contents — calls dump_section)

Without -j (i.e., without specifying --section), the SEC_HAS_CONTENTS
guard fires and dump_section returns immediately (no hang).

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

The SEC_HAS_CONTENTS check must also apply when the --section filter is
active. Change the guard in dump_section() from an if/else into a
combined check:

    --- a/binutils/objdump.c
    +++ b/binutils/objdump.c
    @@ -5084,10 +5084,9 @@ dump_section (bfd *abfd, asection *section, void
*dummy)
     {
       ...
    -  if (only_list == NULL)
    -    {
    -      if ((section->flags & SEC_HAS_CONTENTS) == 0)
    -        return;
    -    }
    -  else if (!process_section_p (section))
    +  if ((section->flags & SEC_HAS_CONTENTS) == 0)
         return;
    +  if (only_list != NULL && !process_section_p (section))
    +    return;
       ...
     }

This ensures that sections without on-disk content are always skipped,
regardless of whether a --section filter is specified.

Additionally, the bfd_section_size_insane() helper should be fixed to
not unconditionally return false when SEC_HAS_CONTENTS is absent, or a
redundant size cap should be added in dump_section() before calling
bfd_get_full_section_contents().

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

Denial of service (CPU and memory exhaustion) via crafted PE32 or
other BFD-handled binary. Any local or remote use of objdump --section
--full-contents on untrusted files (e.g. in automated analysis pipelines,
CI/CD build systems, or package-manager scripts) is affected. With
VirtualSize ≤ available RAM, the process also allocates 1+ GB of memory.

CWE-835 (Loop with Unreachable Exit Condition) / CWE-400 (Uncontrolled
Resource Consumption). No privileges required; a single crafted file
triggers the hang.

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

Reply via email to