https://sourceware.org/bugzilla/show_bug.cgi?id=34373
Bug ID: 34373
Summary: objdump: infinite loop in debug_type_samep() due to
missing cycle detection for INDIRECT type chains
(binutils/debug.c:3016)
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 16828
--> https://sourceware.org/bugzilla/attachment.cgi?id=16828&action=edit
Minimal 504-byte ELF with a crafted .stab section that creates a
self-referential INDIRECT type cycle; triggers infinite loop in
debug_type_samep() at debug.c:3016
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
------------------------------------------------------------------------
debug_type_samep() (binutils/debug.c) compares two debug type objects
for structural equality. It begins by "unwinding" any INDIRECT types —
forwarding pointers used by the stab parser to handle forward type
references — via two while loops:
/* debug.c:3005 – debug_type_samep (simplified) */
static bool
debug_type_samep (struct debug_handle *info,
struct debug_type_s *t1,
struct debug_type_s *t2)
{
...
while (t1->kind == DEBUG_KIND_INDIRECT)
{
t1 = *t1->u.kindirect->slot; /* follow the indirection */
if (t1 == NULL)
return false;
/* ← NO cycle detection: if slot points back to t1, loops forever */
}
while (t2->kind == DEBUG_KIND_INDIRECT)
{
t2 = *t2->u.kindirect->slot;
if (t2 == NULL)
return false;
}
...
}
A crafted stab .stab section can create a circular INDIRECT chain:
INDIRECT_A → slot = &slot_cell
slot_cell = INDIRECT_A ← slot_cell points back to INDIRECT_A
In this case `*t1->u.kindirect->slot` always returns `INDIRECT_A` again,
so `t1` never changes, the NULL check never fires, and the loop runs
without bound.
The contrast with the rest of debug.c is instructive: debug_write_type()
uses a `type->mark` field specifically to detect and skip already-visited
types:
/* debug.c:2428 */
type->mark = info->mark; /* stamp; re-entering with same mark → bail */
debug_type_samep() has no equivalent visited-set or mark check; it
silently relies on the assumption that INDIRECT chains are acyclic.
Trigger: objdump -g on a crafted ELF whose .stab section encodes a type
number that resolves through an INDIRECT slot that points to itself.
------------------------------------------------------------------------
HANG OUTPUT
------------------------------------------------------------------------
There is no crash; the process loops forever.
Reproduction evidence:
$ timeout 3 objdump -g poc_debug_type_samep_infinite_loop.elf 2>&1 | tail
-3
Terminated
$ echo $?
124 (SIGTERM from timeout)
$ objdump -g poc_debug_type_samep_infinite_loop.elf &
PID=$!; sleep 1
$ cat /proc/$PID/status | grep State
State: R (running) ← purely CPU-bound, never progresses
$ kill $PID
CPU ticks consumed per second: ~100 (one full core), confirmed
by reading /proc/<PID>/stat with a 1-second sampling interval.
The hang is inside debug_type_samep(), which is called from
debug_set_class_id() when two struct types with the same tag name
are compared for structural equality during stab debug-info output.
------------------------------------------------------------------------
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 ELF (poc_debug_type_samep_infinite_loop.elf, 504 bytes)
requires a .stab section with five entries that create a self-
referential INDIRECT type cycle in three steps:
Step 1 — plant the forward-reference slot:
N_LSYM int:f(0,1)=9;(0,1)
The 'f' case calls parse_stab_type for the function's return type.
Parsing "(0,1)=9" triggers a look-up of type number 9 via
stab_find_type([0,9]): slot[9] is empty, so an INDIRECT object
(INDIRECT_9) is created with slot = &slot[9]. The result is
stored: slot[1] = INDIRECT_9. Crucially, slot[9] itself remains
NULL at this point; INDIRECT_9 does not yet point to itself.
Step 2 — create two struct definitions with the same tag:
N_LSYM A:T(0,2)=s4x:(0,1),0,32;; (first definition)
N_LSYM A:T(0,2)=s4x:(0,1),0,32;; (second definition)
Each struct field `x` looks up type (0,1) via stab_find_type,
which now returns INDIRECT_9 (slot[1] is non-NULL). Both field
objects therefore store INDIRECT_9 as their type.
Step 3 — close the cycle (nameless typedef):
N_LSYM :t(0,9)=1(0,1)
The empty name prevents debug_name_type() from wrapping the type
in a NAMED node and from overwriting the slot. parse_stab_type
resolves "1" as type-number [0,1], looks up slot[1] = INDIRECT_9,
and calls stab_record_type([0,9], INDIRECT_9). This stores
INDIRECT_9 into slot[9]. Now:
INDIRECT_9->slot = &slot[9]
*INDIRECT_9->slot = slot[9] = INDIRECT_9 ← cycle!
Trigger:
After all stabs are parsed, debug_write() outputs the debug info.
When it processes the second "A" struct type, debug_set_class_id()
finds the first "A" struct in id_list and calls:
debug_type_samep(info, struct1, struct2)
→ debug_class_type_samep() compares field types:
debug_get_real_type(INDIRECT_9)
→ *slot == INDIRECT_9 == type → cycle detected → returns INDIRECT_9
debug_type_samep(info, INDIRECT_9, INDIRECT_9)
while (t1->kind == INDIRECT):
t1 = *t1->u.kindirect->slot = slot[9] = INDIRECT_9
(never NULL, loop never exits) ← INFINITE LOOP
Reproduce:
./binutils/objdump -g poc_debug_type_samep_infinite_loop.elf
The -g flag activates the stab debug-info printer, which internally
calls debug_type_samep() during the type-equality checks in
debug_set_class_id() when two struct types share a tag name.
------------------------------------------------------------------------
SUGGESTED FIX
------------------------------------------------------------------------
Limit the unwinding depth for each INDIRECT chain independently.
Each loop gets its own counter so calls do not share state:
--- a/binutils/debug.c
+++ b/binutils/debug.c
@@ -3016,11 +3016,21 @@ debug_type_samep (struct debug_handle *info,
- while (t1->kind == DEBUG_KIND_INDIRECT)
- {
- t1 = *t1->u.kindirect->slot;
- if (t1 == NULL)
- return false;
- }
- while (t2->kind == DEBUG_KIND_INDIRECT)
- {
- t2 = *t2->u.kindirect->slot;
- if (t2 == NULL)
- return false;
- }
+ {
+ unsigned int depth = 0;
+ while (t1->kind == DEBUG_KIND_INDIRECT)
+ {
+ if (++depth > 1000)
+ return false;
+ t1 = *t1->u.kindirect->slot;
+ if (t1 == NULL)
+ return false;
+ }
+ }
+ {
+ unsigned int depth = 0;
+ while (t2->kind == DEBUG_KIND_INDIRECT)
+ {
+ if (++depth > 1000)
+ return false;
+ t2 = *t2->u.kindirect->slot;
+ if (t2 == NULL)
+ return false;
+ }
+ }
Note: the info->mark field (used by debug_write_type() for its own
cycle detection) must NOT be reused here. debug_write_type() stamps
every visited type — including INDIRECT nodes — with info->mark before
processing them (debug.c:2428). Since debug_set_class_id() is called
from within debug_write_type(), INDIRECT nodes in field-type chains may
already carry info->mark, causing a spurious "cycle detected" return
even for non-cyclic chains. The depth limit avoids this: each
debug_type_samep() call maintains its own independent counter, and
INDIRECT chains deeper than 1000 cannot occur in valid debug info.
------------------------------------------------------------------------
IMPACT
------------------------------------------------------------------------
Denial of service (CPU exhaustion / infinite loop) via a crafted ELF
file with a malformed .stab section. Any invocation of objdump -g on an
untrusted file is affected. The process consumes 100% of one CPU core
and never terminates without an external kill signal.
CWE-835 (Loop with Unreachable Exit Condition). No privileges required;
a single crafted file triggers the hang. Automated analysis pipelines
that call objdump -g on submitted binaries are especially exposed.
--
You are receiving this mail because:
You are on the CC list for the bug.