Skip to content

fuzz: add a target for the ELF symbol-resolution paths - #112

Open
perbu wants to merge 1 commit into
masterfrom
fuzz-elf-symbols
Open

fuzz: add a target for the ELF symbol-resolution paths#112
perbu wants to merge 1 commit into
masterfrom
fuzz-elf-symbols

Conversation

@perbu

@perbu perbu commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

fuzz.cpp drives the loader only. section_by_name(), resolve_symbol() and resolve() hang off AddressOf() / address_of() / resolve(), which the loader never calls — so those three have had zero fuzz coverage.

That is not a niche corner: resolve() runs on every guest fault, because handle_exception() calls it to symbolise the faulting address. An adversarial ELF reaches the section and symbol walkers with nothing more than a guest that crashes on purpose.

elfsymfuzzer calls all three with an input-derived symbol name and rip.

Results against the current tree

All nine seeds run in about two seconds:

seed result location
shnum_huge SEGV machine_elf.cpp:251 #100
shstrndx_oob SEGV :246 #100
stname_wild SEGV :278 #100
symtab_size_huge heap-buffer-overflow :328 #100
symbol_name_4k stack-buffer-overflow :303 #100
symtab_misaligned UBSan misaligned access :277 new, see below
valid, header_only, shoff_wrap clean

symtab_misaligned is not part of #100. resolve_symbol() casts a file offset straight to Elf64_Sym* with no alignment check, so the ELF author picks the alignment. Benign on x86-64, a fault on strict-alignment targets — this is the seed whose value is realised by running the target under UBSan on AArch64, which is the highest-value harness gap on the list.

Two harness details that are load-bearing

  • The image is copied into an aligned buffer, not pointed at in place. machine_elf.cpp casts the base pointer to Elf64_Ehdr*, and libFuzzer's input is not 8-aligned once the name/rip header is stripped off it. Without the copy, every input reports misaligned access and buries everything else. (I hit this; it is why the first version of this target looked like it found nothing.)
  • resolve()'s return length is asserted against its 2048-byte format buffer. That needs to be explicit: the copy compiles to an inline rep movsb, which ASan does not instrument, so a moderate over-read runs clean under the sanitizer.

On the seeds

gen_elf_symbol_seeds.py writes minimal ELFs with one poisoned header field each, because bit-flipping a valid binary essentially never produces an e_shnum past EOF or an offset that wraps. Two things to preserve if these are edited, both of which cost me a debugging round:

  • the searched section must be absent, or section_by_name() returns on the match long before it walks off the end;
  • .symtab must be 8-aligned, or UBSan reports the cast before the loop reaches the bug the seed is aiming at.

The phdr-side wraps in is_dynamic_elf()/elf_load_ph (the rest of #100) belong to the elffuzzer target, which drives the loader — seeds for them would be dead weight here.

🤖 Generated with Claude Code

fuzz.cpp drives the loader only. section_by_name(), resolve_symbol() and
resolve() hang off AddressOf() / address_of() / resolve(), which the loader
never calls, so those three have had zero fuzz coverage -- despite
resolve() running on every guest fault, since handle_exception() calls it
to symbolise the faulting address. An adversarial ELF reaches the section
and symbol walkers with nothing more than a guest that crashes on purpose.

elfsymfuzzer calls all three with an input-derived symbol name and rip.
Two things about the harness are load-bearing:

  - The image is copied into an aligned buffer rather than pointed at in
    place. machine_elf.cpp casts the base pointer straight to Elf64_Ehdr*,
    and libFuzzer's input is not 8-aligned once the name/rip header is
    stripped, so every single input would otherwise report misaligned
    access under UBSan and bury everything else.
  - resolve() formats into a fixed 2048-byte stack buffer, so a returned
    string at least that long means it copied past the end. That needs an
    explicit assertion: the copy compiles to an inline rep movsb, which
    ASan does not instrument, so a moderate over-read runs clean.

gen_elf_symbol_seeds.py writes nine minimal ELFs, each with one poisoned
header field, because bit-flipping a valid binary essentially never
produces an e_shnum past EOF or an offset that wraps. Two details worth
keeping if these are edited: the searched section must be *absent* or
section_by_name() returns on the match long before it walks off the end,
and .symtab must be 8-aligned or UBSan reports the cast before the loop
reaches the bug the seed is aiming at.

Against the current tree the seeds produce, all within a couple of seconds:

  shnum_huge         SEGV                    machine_elf.cpp:251   (#100)
  shstrndx_oob       SEGV                    machine_elf.cpp:246   (#100)
  stname_wild        SEGV                    machine_elf.cpp:278   (#100)
  symtab_size_huge   heap-buffer-overflow    machine_elf.cpp:328   (#100)
  symbol_name_4k     stack-buffer-overflow   machine_elf.cpp:303   (#100)
  symtab_misaligned  UBSan misaligned access machine_elf.cpp:277
  valid, header_only, shoff_wrap             clean

symtab_misaligned is not part of #100: resolve_symbol() casts a file offset
to Elf64_Sym* with no alignment check, so the ELF author picks the
alignment. Benign on x86-64, a fault on strict-alignment targets -- it is
the one seed here whose value is realised by running this target under
UBSan on AArch64.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant