Add an x86_64 ELF whose hash tables are zeroed - #200
Conversation
|
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS Validation record for head Provenance. Reproducible from the committed input. Why it could not be built instead. No linker emits an empty bucket array: linking an What it demonstrates. Its Not run: no test suite in this repository covers fixtures, so nothing was executed here Correction (2026-08-28). The earlier version of the "What it demonstrates" paragraph gave |
|
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS What this fixture makes observable, before and after adding it. A fixture pull request has no test to run, so the two sides are the same reproducer against Before — the derived file does not exist, so the consuming loader change's regression has no input: angr/binaries master (8646be4)After — the corruption this fixture carries is one field wide and its effect is total: with this branch (6c82296) |
Some stripping and obfuscation tools blank the DT_HASH and DT_GNU_HASH tables of a finished executable. They only accelerate lookup by name, so the file otherwise survives it: DT_SYMTAB, DT_STRTAB, the relocations and the version tables are all still there. What a reader sees is a .gnu.hash header of all zeroes, which declares nbuckets == 0 and an empty bucket array, and pyelftools raises ValueError out of DynamicSegment.num_symbols() on exactly that shape. cle loses the whole load to it. No linker emits an empty bucket array. GNU ld 2.46 and LLD 21.1.8 both special-case a table with nothing to hash and write nbuckets = 1, so the fixture cannot be produced by building something, and hand-assembling an ELF would test a shape no toolchain emits. tests_src/gnu_hash_resiliency builds it instead from tests/x86_64/test_killing_ref, which carries both hash tables directly ahead of its symbol table, by zeroing the 96 bytes they occupy. Twenty bytes actually change; everything else, including the file size, is the original linker's output. Rerunning the script on the same input reproduces the fixture byte for byte. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6c82296 to
898b521
Compare
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS
Problem
The linked cle change makes the loader survive an ELF whose GNU hash table declares no buckets — the shape some stripping and obfuscation tools leave behind when they blank an executable's
DT_HASHandDT_GNU_HASH. Nothing in this repository has it, so that change's regression has no input and its reproducer is a path that does not exist:Root cause
The fixture cannot be built, only derived. No linker emits an empty bucket array: a shared object that exports nothing (
{ local: *; };,--hash-style=gnu) links tonbuckets = 1under GNU ld 2.46 and under LLD 21.1.8 alike, because binutils special-cases a table with nothing to hash. Only corruption producesnbuckets = 0. Astruct.pack-assembled header would test a shape no real toolchain emits, which is the failure mode that passes while the real format still fails.Fix
tests/x86_64/gnu_hash_resiliency_0istests/x86_64/test_killing_ref— a real linker's output — with the 96 bytes its two hash tables occupy zeroed. Same 15,960 bytes, same mode, andcmp -lreports exactly 20 differing bytes, all between offsets0x370and0x3cb.tests_src/gnu_hash_resiliency/build_gnu_hash_resiliency.pyderives it from the committed input, so a reviewer can regenerate it from material already in this repository and needs no toolchain.What the fixture makes observable, which its uncorrupted source does not:
The count a loader has to recover instead is checkable from the file alone, two independent ways:
DT_SYMTAB 0x3d0toDT_STRTAB 0x4a8atDT_SYMENT24 is 9 entries with no remainder, andDT_VERSYM 0x5feplus two bytes per symbol for 9 symbols reaches0x610, which is exactlyDT_VERNEED.Testing
No test suite in this repository covers fixtures, so nothing is executed here. What was checked instead: re-running the generator in place reproduced the committed file byte for byte and left
git statusempty, and with the cle change applied the fixture loads with 56 symbols, 30 sections, 11 relocations, 4 PLT entries and one dependency — identical to the uncorruptedtest_killing_refon every one of those, which is what the consumer's regression asserts.Validation: #200 (comment)
sync: angr/cle#792
session: sharpen