Hello Binutils maintainers,
I am reporting a public robustness bug in libctf archive parsing. GNU
Binutils 2.47 accepts archive-controlled count and offset fields
without validating them against the supplied CTF section size.
ctf_dict_open() subsequently reaches search_modent_by_name(), which
calls strcmp() through an out-of-bounds name pointer.
I reproduced two local crashes against an official 2.47 build on x86-64 Ubuntu:
- A 56-byte archive placed immediately before a PROT_NONE guard page
crashes through ctf_arc_bufopen() -> ctf_dict_open(), exit 139.
- A 760-byte ELF object containing a malformed .ctf section crashes
readelf --ctf, exit 139.
Current main at revision 8818c47980038800239da822cfef2aa63e0fac1f
still contains the same unchecked path. I am reporting this as an
ordinary public robustness issue consistent with
binutils/SECURITY.txt, not as a CVE or code-execution claim.
Demonstrated impact is an out-of-bounds read and process termination
only.
Please credit: monsterd0n
Contact: [email protected]
The full report and both source reproducers are attached and reproduced below.
**libctf archive name lookup reads beyond malformed input in
`search_modent_by_name()`**
Researcher credit: **monsterd0n**
## Executive Summary
A user can pass a malformed CTF archive to the public libctf buffer
API, or ask GNU `readelf --ctf` to inspect an ELF object containing
that archive, and terminate the parsing process. The archive declares
one member but places its name table at or far beyond the end of the
supplied CTF section. libctf accepts the archive based only on its
header size and magic, trusts its offsets and count during dictionary
lookup, and calls `strcmp()` on an out-of-bounds pointer. GNU Binutils
2.47 reproducibly exited with signal 11/status 139 in both the direct
API and `readelf` tests. The demonstrated problem is an out-of-bounds
read and process denial of service, not a write, data disclosure, code
execution, or privilege escalation.
This report is an ordinary upstream robustness report, not a request
for a private security process, CVE, or embargo.
`binutils/SECURITY.txt` says that inspection-tool crashes on crafted
input are treated as non-security issues unless a stronger
trust-boundary breach is demonstrated. No such breach was found here.
The same source defect affects the public libctf buffer API, but no
privileged or network-facing consumer was demonstrated.
I inspected the exact GNU Binutils 2.47 release source, the two
portable PoCs, and current main at revision `8818c47` dated 15 August
2026. The 2.47 run records came from an x86-64 Ubuntu build produced
with GCC 13.3; the C PoC was compiled with `-Wall -Wextra -Werror`
before execution. The earliest release I could verify is 2.47. Current
main remains affected, but the introduction point, other released
versions, and a fixed release were not established.
## Background
libctf supports archives containing multiple CTF dictionaries. The
on-disk archive begins with a 40-byte `struct ctf_archive`. Its
attacker-controlled fields include the number of dictionaries
(`ctfa_ndicts`), the offset of the archive name table (`ctfa_names`),
and the offset of the CTF data table (`ctfa_ctfs`). An array of
16-byte `ctf_archive_modent` records follows the header; each record
contains a name-table-relative `name_offset` and a CTF-table-relative
`ctf_offset`.
The direct test provides libctf with a `ctf_sect_t` whose `cts_data`
points to 56 readable bytes: exactly one 40-byte header plus one
16-byte member. That buffer ends at a page boundary, and the following
page has `PROT_NONE`. The archive declares one member, sets
`ctfa_names` and `ctfa_ctfs` to 56, and gives the member a zero name
offset. The logical name table therefore starts exactly one byte past
the supplied buffer. No special runtime configuration or race is
needed.
The file-interface test uses the same 56-byte CTF archive inside a
760-byte ELF relocatable object. It deliberately sets `ctfa_names` to
a very large value so that the invalid pointer used by `readelf` is
deterministic on the tested platform. The command explicitly selects
`.ctf`, `.symtab`, and `.strtab`; no malformed executable is run.
## Vulnerability Details
In GNU Binutils 2.47, `libctf/ctf-archive.c:ctf_arc_bufopen()` decides
that a supplied buffer is an archive when it is at least the size of
the fixed header and has the expected magic:
```c
if (ctfsect->cts_data != NULL
&& ctfsect->cts_size >= sizeof (struct ctf_archive)
&& (le64toh ((*(uint64_t *) ctfsect->cts_data)) == CTFA_MAGIC))
{
is_archive = 1;
arc = (struct ctf_archive *) ctfsect->cts_data;
}
```
This entry point has the true `cts_size`, but it does not validate
that the member array, name table, CTF table, or their contents fit
within that size. It returns an archive wrapper, so the direct PoC's
56-byte header-and-member buffer is accepted successfully.
`ctf_dict_open()` reaches
`libctf/ctf-archive.c:ctf_dict_open_internal()`. A null requested name
becomes the default `.ctf` name. The function derives an unchecked
member-array pointer immediately after the header, derives the
name-table base from `ctfa_names`, and gives the attacker-controlled
count to `bsearch_r()`:
```c
modent = (ctf_archive_modent_t *) ((char *) arc
+ sizeof (struct ctf_archive));
search_nametbl = (const char *) arc + le64toh (arc->ctfa_names);
modent = bsearch_r (name, modent, le64toh (arc->ctfa_ndicts),
sizeof (struct ctf_archive_modent),
search_modent_by_name, (void *) search_nametbl);
```
There are three related missing checks before the search: `ctfa_ndicts
* sizeof(struct ctf_archive_modent)` is not checked for integer
overflow or containment; `ctfa_names` is not checked against the
archive length; and the chosen member has not yet been validated.
For the one-member test, `bsearch_r()` calls
`libctf/ctf-archive.c:search_modent_by_name()`. The comparison adds
the member's unvalidated `name_offset` to the already unvalidated
name-table base, then performs an unbounded C-string comparison:
```c
const struct ctf_archive_modent *v = ent;
const char *search_nametbl = arg;
return strcmp (k, &search_nametbl[le64toh (v->name_offset)]);
```
In the direct PoC, `search_nametbl` points to the first byte of the
inaccessible guard page and `name_offset` is zero. `strcmp()` reads
that page while comparing against `.ctf`, producing the observed
segmentation fault. Even when a name offset initially falls within the
buffer, `strcmp()` remains unsafe unless libctf has established that a
terminating NUL exists before the name table or archive ends.
The shipped-tool path is direct. In
`binutils/readelf.c:dump_section_as_ctf()`, `readelf` obtains the
selected sections, passes the `.ctf` bytes to `ctf_arc_bufopen()`, and
then calls `ctf_dict_open()` for the parent dictionary:
```c
if ((ctfa = ctf_arc_bufopen (&ctfsect, symsectp, strsectp, &err)) == NULL)
goto fail;
if ((parent = ctf_dict_open (ctfa, dump_ctf_parent_name, &err)) == NULL)
goto fail;
```
The 760-byte ELF PoC therefore reaches the same lookup through an
ordinary `readelf --ctf` interface rather than a private helper.
Current main contains the same `ctf_arc_bufopen()`,
`ctf_dict_open_internal()`, and `search_modent_by_name()` logic. Only
release 2.47 and current main were verified for this report. I did not
establish the introducing commit, inspect earlier releases, or
identify a fix, so no broader affected-version range is claimed.
## Exploitability Analysis
The established primitive is an out-of-bounds read in the process
parsing the crafted archive. The guarded public-API test makes the
boundary crossing precise: all 56 archive bytes are readable, the name
table begins on the next inaccessible page, `ctf_arc_bufopen()`
returns a non-null handle, and the subsequent dictionary lookup
terminates the process. The ELF test establishes that the same missing
validation is reachable through GNU `readelf`.
The observed impact is limited to process availability. The failing
operation is `strcmp()`, so it reads until it finds a difference, a
NUL, or inaccessible memory. The experiments did not return bytes to
the user, alter memory, influence control flow beyond the crash, or
run in a process with elevated privileges. Address placement also
differs between the guarded API PoC and normal file mappings, which is
why the assembly fixture uses a deliberately huge `ctfa_names` value
for deterministic failure rather than claiming every malformed offset
will crash identically.
The direct PoC provides a useful positive and sequencing control:
`ctf_arc_bufopen()` accepts the minimum-sized archive, prints
`archive_size=56; calling ctf_dict_open`, and only then faults during
lookup. This rules out failure in `mmap()`, `mprotect()`, or the
initial magic/header check as the explanation. The `readelf` object
supplies a separate real-interface control because the assembler
produces a structurally valid ELF file with ordinary `.symtab` and
`.strtab` sections; the malformed data is confined to `.ctf`.
No patched-build or valid-archive negative-control run was supplied
because no fix was identified. A normal archive with in-bounds tables
is expected to parse, but this report does not present that unrecorded
expectation as an executed result.
The Binutils security policy is decisive for classification. It states
that inspection tools such as `readelf`, `nm`, and `objdump` are not
treated as security boundaries and recommends sandboxing them for
hostile input. It also requires a stronger result, such as code
execution as another user or sandbox escape, before a crafted-input
tool bug is considered a security issue. Since this report
demonstrates neither, the appropriate outcome is a public robustness
fix.
## Proof of Concept
Two source PoCs are provided in `poc/`:
- `search_modent_oob_read.c` exercises the public libctf buffer API
with a guarded 56-byte archive.
- `malformed_ctf.S` assembles to an ELF object with a 56-byte
malformed `.ctf` section for `readelf`.
The direct PoC is intended for Linux because it uses anonymous
`mmap()` and `mprotect()`. Download the official
`binutils-2.47.tar.xz` into the report directory, then build, compile,
and run as follows:
```sh
tar -xf binutils-2.47.tar.xz
mkdir binutils-build
cd binutils-build
../binutils-2.47/configure \
--disable-gdb --disable-gdbserver --disable-gprofng \
--disable-gold --disable-sim --disable-werror
make -j2 all-libctf all-binutils
cd ..
binutils-build/libctf/libtool --mode=link \
cc -Wall -Wextra -Werror \
-Ibinutils-2.47/include \
poc/search_modent_oob_read.c \
binutils-build/libctf/libctf-nobfd.la \
-o poc/search_modent_oob_read
./poc/search_modent_oob_read
printf 'exit=%d\n' "$?"
```
The recorded GNU Binutils 2.47 run on x86-64 Ubuntu reached the
diagnostic below and then exited with status 139 after `SIGSEGV`:
```text
archive_size=56; calling ctf_dict_open
```
The C source was compiled with GCC 13.3 and `-Wall -Wextra -Werror`
before this run. The process faulted when `strcmp()` entered the
`PROT_NONE` page; it did not write files or change privileges.
Build and exercise the real-interface fixture with:
```sh
cc -c poc/malformed_ctf.S -o poc/malformed_ctf.o
binutils-build/binutils/readelf \
--ctf=.ctf \
--ctf-symbols=.symtab \
--ctf-strings=.strtab \
poc/malformed_ctf.o
printf 'exit=%d\n' "$?"
```
The assembled object was 760 bytes. The official GNU Binutils 2.47
`readelf` build terminated with signal 11/status 139. This run used
the supplied `malformed_ctf.S`; it does not rely on the original
notes' earlier, unavailable fixture or result.
Run both PoCs only against a disposable local build because they
intentionally crash the tested process. Cleanup consists of removing
the two generated files `poc/search_modent_oob_read` and
`poc/malformed_ctf.o`; neither PoC modifies the source checkout or
external state.
## Remediation
Validate the entire archive layout while `ctf_arc_bufopen()` still has
both the buffer base and `cts_size`, and preserve the validated
archive length in the internal wrapper for later checks. A safe
validator should use overflow-resistant subtraction or checked
arithmetic and reject the archive unless all of the following hold:
- the fixed archive header fits;
- `ctfa_ndicts * sizeof(struct ctf_archive_modent)` does not overflow
and the complete member array fits after the header;
- `ctfa_names` and `ctfa_ctfs` are within the supplied section and
describe non-overlapping, structurally valid regions as required by
the archive format;
- every member's `name_offset` resolves within the name table and
reaches a terminating NUL before the table or archive boundary;
- every member's `ctf_offset`, length prefix, and dictionary body fit
within the CTF table and archive; and
- alignment requirements are checked before typed loads where the
format requires them.
Do not try to repair only `search_modent_by_name()` with a pointer
comparison: `strcmp()` needs an explicit remaining length, and other
archive consumers also use `ctfa_ndicts`, `ctfa_names`, `name_offset`,
`ctfa_ctfs`, and `ctf_offset`. After validation, the comparator can
use a bounded comparison or validated strings, but all archive
iteration and open-by-offset paths should share the same trusted
layout metadata.
Malformed archives should be rejected through the existing libctf
error mechanism rather than reaching `bsearch_r()`. No upstream patch
was available to inspect, so this is proposed remediation rather than
a description of shipped code.
Regression tests should cover both public and shipped-tool paths: the
guarded 56-byte archive; a member-array count whose multiplication
overflows; member arrays truncated by one byte; name-table offsets at
and beyond the end; in-range names without NUL terminators;
overflowing `name_offset` and `ctf_offset`; truncated CTF length
prefixes and bodies; a valid one-member archive; and the ELF `readelf
--ctf` fixture. The malformed cases should return a format error
without a signal under ordinary, ASan, and UBSan builds.
## Summary
GNU Binutils 2.47 libctf accepts a header-sized CTF archive without
validating its count or table offsets. Dictionary lookup then passes
attacker-controlled array metadata to `bsearch_r()` and calls
`strcmp()` using an unchecked name-table base and member name offset.
A 56-byte guarded archive crashes the public API path, and a 760-byte
ELF object crashes the shipped `readelf --ctf` path; both recorded
runs exit 139. The demonstrated result is an out-of-bounds read and
process denial of service only.
Release 2.47 is the earliest verified affected release, and current
main still contains the defect. Introduction, other affected releases,
and a fixed release are unknown. Under Binutils' documented policy,
this crafted-input inspection-tool crash should be handled as a public
robustness bug unless future work demonstrates a stronger
trust-boundary breach.
Regards,
monsterd0n
.text
.globl ctf_dummy_symbol
.type ctf_dummy_symbol,@function
ctf_dummy_symbol:
ret
.size ctf_dummy_symbol, .-ctf_dummy_symbol
.section .ctf,"",@progbits
.balign 8
.quad 0x8b47f2a4d7623eeb
.quad 0
.quad 1
.quad 0x7fffffffffffffff
.quad 56
.quad 0
.quad 0
**libctf archive name lookup reads beyond malformed input in `search_modent_by_name()`**
Researcher credit: **monsterd0n**
## Executive Summary
A user can pass a malformed CTF archive to the public libctf buffer API, or ask GNU `readelf --ctf` to inspect an ELF object containing that archive, and terminate the parsing process. The archive declares one member but places its name table at or far beyond the end of the supplied CTF section. libctf accepts the archive based only on its header size and magic, trusts its offsets and count during dictionary lookup, and calls `strcmp()` on an out-of-bounds pointer. GNU Binutils 2.47 reproducibly exited with signal 11/status 139 in both the direct API and `readelf` tests. The demonstrated problem is an out-of-bounds read and process denial of service, not a write, data disclosure, code execution, or privilege escalation.
This report is an ordinary upstream robustness report, not a request for a private security process, CVE, or embargo. `binutils/SECURITY.txt` says that inspection-tool crashes on crafted input are treated as non-security issues unless a stronger trust-boundary breach is demonstrated. No such breach was found here. The same source defect affects the public libctf buffer API, but no privileged or network-facing consumer was demonstrated.
I inspected the exact GNU Binutils 2.47 release source, the two portable PoCs, and current main at revision `8818c47` dated 15 August 2026. The 2.47 run records came from an x86-64 Ubuntu build produced with GCC 13.3; the C PoC was compiled with `-Wall -Wextra -Werror` before execution. The earliest release I could verify is 2.47. Current main remains affected, but the introduction point, other released versions, and a fixed release were not established.
## Background
libctf supports archives containing multiple CTF dictionaries. The on-disk archive begins with a 40-byte `struct ctf_archive`. Its attacker-controlled fields include the number of dictionaries (`ctfa_ndicts`), the offset of the archive name table (`ctfa_names`), and the offset of the CTF data table (`ctfa_ctfs`). An array of 16-byte `ctf_archive_modent` records follows the header; each record contains a name-table-relative `name_offset` and a CTF-table-relative `ctf_offset`.
The direct test provides libctf with a `ctf_sect_t` whose `cts_data` points to 56 readable bytes: exactly one 40-byte header plus one 16-byte member. That buffer ends at a page boundary, and the following page has `PROT_NONE`. The archive declares one member, sets `ctfa_names` and `ctfa_ctfs` to 56, and gives the member a zero name offset. The logical name table therefore starts exactly one byte past the supplied buffer. No special runtime configuration or race is needed.
The file-interface test uses the same 56-byte CTF archive inside a 760-byte ELF relocatable object. It deliberately sets `ctfa_names` to a very large value so that the invalid pointer used by `readelf` is deterministic on the tested platform. The command explicitly selects `.ctf`, `.symtab`, and `.strtab`; no malformed executable is run.
## Vulnerability Details
In GNU Binutils 2.47, `libctf/ctf-archive.c:ctf_arc_bufopen()` decides that a supplied buffer is an archive when it is at least the size of the fixed header and has the expected magic:
```c
if (ctfsect->cts_data != NULL
&& ctfsect->cts_size >= sizeof (struct ctf_archive)
&& (le64toh ((*(uint64_t *) ctfsect->cts_data)) == CTFA_MAGIC))
{
is_archive = 1;
arc = (struct ctf_archive *) ctfsect->cts_data;
}
```
This entry point has the true `cts_size`, but it does not validate that the member array, name table, CTF table, or their contents fit within that size. It returns an archive wrapper, so the direct PoC's 56-byte header-and-member buffer is accepted successfully.
`ctf_dict_open()` reaches `libctf/ctf-archive.c:ctf_dict_open_internal()`. A null requested name becomes the default `.ctf` name. The function derives an unchecked member-array pointer immediately after the header, derives the name-table base from `ctfa_names`, and gives the attacker-controlled count to `bsearch_r()`:
```c
modent = (ctf_archive_modent_t *) ((char *) arc
+ sizeof (struct ctf_archive));
search_nametbl = (const char *) arc + le64toh (arc->ctfa_names);
modent = bsearch_r (name, modent, le64toh (arc->ctfa_ndicts),
sizeof (struct ctf_archive_modent),
search_modent_by_name, (void *) search_nametbl);
```
There are three related missing checks before the search: `ctfa_ndicts * sizeof(struct ctf_archive_modent)` is not checked for integer overflow or containment; `ctfa_names` is not checked against the archive length; and the chosen member has not yet been validated.
For the one-member test, `bsearch_r()` calls `libctf/ctf-archive.c:search_modent_by_name()`. The comparison adds the member's unvalidated `name_offset` to the already unvalidated name-table base, then performs an unbounded C-string comparison:
```c
const struct ctf_archive_modent *v = ent;
const char *search_nametbl = arg;
return strcmp (k, &search_nametbl[le64toh (v->name_offset)]);
```
In the direct PoC, `search_nametbl` points to the first byte of the inaccessible guard page and `name_offset` is zero. `strcmp()` reads that page while comparing against `.ctf`, producing the observed segmentation fault. Even when a name offset initially falls within the buffer, `strcmp()` remains unsafe unless libctf has established that a terminating NUL exists before the name table or archive ends.
The shipped-tool path is direct. In `binutils/readelf.c:dump_section_as_ctf()`, `readelf` obtains the selected sections, passes the `.ctf` bytes to `ctf_arc_bufopen()`, and then calls `ctf_dict_open()` for the parent dictionary:
```c
if ((ctfa = ctf_arc_bufopen (&ctfsect, symsectp, strsectp, &err)) == NULL)
goto fail;
if ((parent = ctf_dict_open (ctfa, dump_ctf_parent_name, &err)) == NULL)
goto fail;
```
The 760-byte ELF PoC therefore reaches the same lookup through an ordinary `readelf --ctf` interface rather than a private helper.
Current main contains the same `ctf_arc_bufopen()`, `ctf_dict_open_internal()`, and `search_modent_by_name()` logic. Only release 2.47 and current main were verified for this report. I did not establish the introducing commit, inspect earlier releases, or identify a fix, so no broader affected-version range is claimed.
## Exploitability Analysis
The established primitive is an out-of-bounds read in the process parsing the crafted archive. The guarded public-API test makes the boundary crossing precise: all 56 archive bytes are readable, the name table begins on the next inaccessible page, `ctf_arc_bufopen()` returns a non-null handle, and the subsequent dictionary lookup terminates the process. The ELF test establishes that the same missing validation is reachable through GNU `readelf`.
The observed impact is limited to process availability. The failing operation is `strcmp()`, so it reads until it finds a difference, a NUL, or inaccessible memory. The experiments did not return bytes to the user, alter memory, influence control flow beyond the crash, or run in a process with elevated privileges. Address placement also differs between the guarded API PoC and normal file mappings, which is why the assembly fixture uses a deliberately huge `ctfa_names` value for deterministic failure rather than claiming every malformed offset will crash identically.
The direct PoC provides a useful positive and sequencing control: `ctf_arc_bufopen()` accepts the minimum-sized archive, prints `archive_size=56; calling ctf_dict_open`, and only then faults during lookup. This rules out failure in `mmap()`, `mprotect()`, or the initial magic/header check as the explanation. The `readelf` object supplies a separate real-interface control because the assembler produces a structurally valid ELF file with ordinary `.symtab` and `.strtab` sections; the malformed data is confined to `.ctf`.
No patched-build or valid-archive negative-control run was supplied because no fix was identified. A normal archive with in-bounds tables is expected to parse, but this report does not present that unrecorded expectation as an executed result.
The Binutils security policy is decisive for classification. It states that inspection tools such as `readelf`, `nm`, and `objdump` are not treated as security boundaries and recommends sandboxing them for hostile input. It also requires a stronger result, such as code execution as another user or sandbox escape, before a crafted-input tool bug is considered a security issue. Since this report demonstrates neither, the appropriate outcome is a public robustness fix.
## Proof of Concept
Two source PoCs are provided in `poc/`:
- `search_modent_oob_read.c` exercises the public libctf buffer API with a guarded 56-byte archive.
- `malformed_ctf.S` assembles to an ELF object with a 56-byte malformed `.ctf` section for `readelf`.
The direct PoC is intended for Linux because it uses anonymous `mmap()` and `mprotect()`. Download the official `binutils-2.47.tar.xz` into the report directory, then build, compile, and run as follows:
```sh
tar -xf binutils-2.47.tar.xz
mkdir binutils-build
cd binutils-build
../binutils-2.47/configure \
--disable-gdb --disable-gdbserver --disable-gprofng \
--disable-gold --disable-sim --disable-werror
make -j2 all-libctf all-binutils
cd ..
binutils-build/libctf/libtool --mode=link \
cc -Wall -Wextra -Werror \
-Ibinutils-2.47/include \
poc/search_modent_oob_read.c \
binutils-build/libctf/libctf-nobfd.la \
-o poc/search_modent_oob_read
./poc/search_modent_oob_read
printf 'exit=%d\n' "$?"
```
The recorded GNU Binutils 2.47 run on x86-64 Ubuntu reached the diagnostic below and then exited with status 139 after `SIGSEGV`:
```text
archive_size=56; calling ctf_dict_open
```
The C source was compiled with GCC 13.3 and `-Wall -Wextra -Werror` before this run. The process faulted when `strcmp()` entered the `PROT_NONE` page; it did not write files or change privileges.
Build and exercise the real-interface fixture with:
```sh
cc -c poc/malformed_ctf.S -o poc/malformed_ctf.o
binutils-build/binutils/readelf \
--ctf=.ctf \
--ctf-symbols=.symtab \
--ctf-strings=.strtab \
poc/malformed_ctf.o
printf 'exit=%d\n' "$?"
```
The assembled object was 760 bytes. The official GNU Binutils 2.47 `readelf` build terminated with signal 11/status 139. This run used the supplied `malformed_ctf.S`; it does not rely on the original notes' earlier, unavailable fixture or result.
Run both PoCs only against a disposable local build because they intentionally crash the tested process. Cleanup consists of removing the two generated files `poc/search_modent_oob_read` and `poc/malformed_ctf.o`; neither PoC modifies the source checkout or external state.
## Remediation
Validate the entire archive layout while `ctf_arc_bufopen()` still has both the buffer base and `cts_size`, and preserve the validated archive length in the internal wrapper for later checks. A safe validator should use overflow-resistant subtraction or checked arithmetic and reject the archive unless all of the following hold:
- the fixed archive header fits;
- `ctfa_ndicts * sizeof(struct ctf_archive_modent)` does not overflow and the complete member array fits after the header;
- `ctfa_names` and `ctfa_ctfs` are within the supplied section and describe non-overlapping, structurally valid regions as required by the archive format;
- every member's `name_offset` resolves within the name table and reaches a terminating NUL before the table or archive boundary;
- every member's `ctf_offset`, length prefix, and dictionary body fit within the CTF table and archive; and
- alignment requirements are checked before typed loads where the format requires them.
Do not try to repair only `search_modent_by_name()` with a pointer comparison: `strcmp()` needs an explicit remaining length, and other archive consumers also use `ctfa_ndicts`, `ctfa_names`, `name_offset`, `ctfa_ctfs`, and `ctf_offset`. After validation, the comparator can use a bounded comparison or validated strings, but all archive iteration and open-by-offset paths should share the same trusted layout metadata.
Malformed archives should be rejected through the existing libctf error mechanism rather than reaching `bsearch_r()`. No upstream patch was available to inspect, so this is proposed remediation rather than a description of shipped code.
Regression tests should cover both public and shipped-tool paths: the guarded 56-byte archive; a member-array count whose multiplication overflows; member arrays truncated by one byte; name-table offsets at and beyond the end; in-range names without NUL terminators; overflowing `name_offset` and `ctf_offset`; truncated CTF length prefixes and bodies; a valid one-member archive; and the ELF `readelf --ctf` fixture. The malformed cases should return a format error without a signal under ordinary, ASan, and UBSan builds.
## Summary
GNU Binutils 2.47 libctf accepts a header-sized CTF archive without validating its count or table offsets. Dictionary lookup then passes attacker-controlled array metadata to `bsearch_r()` and calls `strcmp()` using an unchecked name-table base and member name offset. A 56-byte guarded archive crashes the public API path, and a 760-byte ELF object crashes the shipped `readelf --ctf` path; both recorded runs exit 139. The demonstrated result is an out-of-bounds read and process denial of service only.
Release 2.47 is the earliest verified affected release, and current main still contains the defect. Introduction, other affected releases, and a fixed release are unknown. Under Binutils' documented policy, this crafted-input inspection-tool crash should be handled as a public robustness bug unless future work demonstrates a stronger trust-boundary breach.
#define _GNU_SOURCE
#include <ctf-api.h>
#include <endian.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
struct archive_header {
uint64_t magic;
uint64_t model;
uint64_t count;
uint64_t names;
uint64_t ctfs;
};
struct archive_member {
uint64_t name_offset;
uint64_t ctf_offset;
};
int main(void) {
const size_t archive_size = sizeof(struct archive_header) + sizeof(struct archive_member);
const long page_size = sysconf(_SC_PAGESIZE);
unsigned char *mapping;
unsigned char *archive;
struct archive_header *header;
struct archive_member *member;
ctf_sect_t section = {0};
ctf_archive_t *handle;
ctf_dict_t *dict;
int error = 0;
if (page_size <= 0)
return 2;
mapping = mmap(NULL, (size_t)page_size * 2, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (mapping == MAP_FAILED)
return 2;
if (mprotect(mapping + page_size, (size_t)page_size, PROT_NONE) != 0)
return 2;
archive = mapping + page_size - archive_size;
memset(archive, 0, archive_size);
header = (struct archive_header *)archive;
member = (struct archive_member *)(archive + sizeof(*header));
header->magic = htole64(CTFA_MAGIC);
header->count = htole64(1);
header->names = htole64(archive_size);
header->ctfs = htole64(archive_size);
member->name_offset = htole64(0);
member->ctf_offset = htole64(0);
section.cts_name = ".ctf";
section.cts_data = archive;
section.cts_size = archive_size;
handle = ctf_arc_bufopen(§ion, NULL, NULL, &error);
if (handle == NULL) {
fprintf(stderr, "ctf_arc_bufopen failed: %s\n", ctf_errmsg(error));
return 1;
}
printf("archive_size=%zu; calling ctf_dict_open\n", archive_size);
fflush(stdout);
dict = ctf_dict_open(handle, NULL, &error);
if (dict != NULL)
ctf_dict_close(dict);
ctf_arc_close(handle);
munmap(mapping, (size_t)page_size * 2);
fprintf(stderr, "unexpectedly returned: %s\n", ctf_errmsg(error));
return 1;
}