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.