Skip to content

fix(elf): preserve relative relocations by appending notes in-place - #72

Merged
littledivy merged 3 commits into
mainfrom
fix/elf-preserve-relr-relocations
Jun 24, 2026
Merged

fix(elf): preserve relative relocations by appending notes in-place#72
littledivy merged 3 commits into
mainfrom
fix/elf-preserve-relr-relocations

Conversation

@littledivy

@littledivy littledivy commented Jun 16, 2026

Copy link
Copy Markdown
Member

Problem

Elf::append rebuilt the entire binary through object::build::elf::Builder, which reconstructs the file purely from its section table. That is lossy for modern relocatable executables:

  • object 0.36 has no SHT_RELR supportBuilder::read errors (Unsupported section type 13) on any binary carrying a .relr.dyn section header.
  • Any segment bytes not covered by a surviving section are silently dropped on write.

So when a binary's section headers are stripped (as deno compile's release-linux base is), the .relr.dyn relative relocations survive only in a PT_LOAD referenced by DT_RELR — and the rebuild drops them.

The v8 149.4.0 bump added the first load-bearing relative relocation (a C++ static-init guard's mutex pointer). Left un-relocated, it deadlocks/aborts at startup:

libc++abi: __cxa_guard_acquire failed to acquire mutex

Only reproduces on release-linux-x86_64, where the strip is aggressive enough to leave .relr.dyn outside the section table. Verified locally against object 0.36.3: flipping a fixture section to SHT_RELR makes Builder::read fail, and Builder::write only round-trips section-backed bytes.

Fix

Replace the full rebuild with an in-place, patchelf --add-note style append that keeps every original byte:

  1. Append the note payload + an enlarged copy of the program header table at a page-aligned offset past EOF.
  2. Add a PT_LOAD mapping that region and a PT_NOTE pointing at the note, and repoint e_phoff/e_phnum + PT_PHDR at the new table (so the loader's load-bias math, AT_PHDR - PT_PHDR.p_vaddr, stays correct). This is what the runtime find_section/dl_iterate_phdr reads.
  3. When the input still has a section header table, also add a real SHF_ALLOC SHT_NOTE .note.sui section — relocating + growing the section header table and .shstrtab past EOF — so the note survives a later strip (BFD tools rebuild from sections). A fully stripped binary keeps the note via PT_NOTE alone.

The original program/section header tables, .shstrtab, segment contents, and all relocations are copied (enlarged), never edited in place — so .relr.dyn survives unchanged.

Tests

  • test_elf executes the produced binary on Linux CI — exercises the relocated PT_PHDR. ✅
  • test_elf_note_survives_strip confirms the note is recoverable after GNU strip. ✅
  • test_elf_note_mapped_and_preserves / test_elf_note_does_not_overlap_bss rewritten to verify the note via its PT_NOTE program header. ✅
  • New test_elf_append_preserves_original_bytes: all bytes past the ELF header preserved, section table relocated + grew by one, program header table grew by two, SUI note discoverable. ✅

All four CI runners green.

Note: no version bump included — left for a follow-up release when deno bumps the libsui pin.

The ELF `append` path rebuilt the whole binary through
`object::build::elf::Builder`, which reconstructs the file purely from its
section table. That is lossy for modern relocatable executables:

  * `object` 0.36 has no `SHT_RELR` support and errors out on any binary
    that still carries a `.relr.dyn` section header; and
  * any segment bytes not covered by a surviving section are silently
    dropped on write.

When a binary's section headers are stripped (as `deno compile`'s
release-linux base is), the `.relr.dyn` relative relocations live only in a
`PT_LOAD` referenced by `DT_RELR` and get dropped by the rebuild. With the
v8 149.4.0 bump adding the first load-bearing relative relocation, a C++
static-init guard's mutex pointer is left un-relocated, deadlocking at
startup (`__cxa_guard_acquire failed to acquire mutex`).

Replace the full rebuild with an in-place, `patchelf --add-note` style
append: keep every original byte, write the note plus an enlarged copy of
the program header table past EOF, add a `PT_LOAD` mapping that region and a
`PT_NOTE` pointing at the note, and repoint `e_phoff`/`e_phnum` (and
`PT_PHDR`). The section header table is never touched, so `.relr.dyn` and
all other relocations survive unchanged.

Adds a regression test asserting byte/section-header preservation and a
discoverable SUI `PT_NOTE`.
The surgical `append` no longer creates a `.note.sui` section — the note is
discoverable only through its PT_NOTE program header (which is what the
runtime `find_section`/`dl_iterate_phdr` path uses, and the only mechanism
that survives a full section-header strip).

Update the ELF note tests accordingly:
  * find_section_in_bytes falls through to program headers even when a
    section table is present, instead of bailing out.
  * test_elf_note_mapped_and_preserves verifies a PT_NOTE carries the SUI
    note, that a PT_LOAD maps it, and that the GNU note is preserved.
  * test_elf_note_does_not_overlap_bss derives the note's mapped range from
    its PT_NOTE program header.
The PT_NOTE-only append left the note unreachable after a subsequent
`strip`: BFD-based tools rebuild the file from its section table and discard
bytes that no section describes. Restore the previous guarantee by also
adding a real SHF_ALLOC SHT_NOTE section for the note, but do it surgically:
the original section header table and .shstrtab are copied (enlarged) past
EOF and the ELF header is repointed at them — the originals, and every
relocation, are left byte-for-byte intact.

A fully stripped binary (no section header table) keeps the note via its
PT_NOTE program header alone, which is all that is possible there and all the
runtime needs.

Updates test_elf_append_preserves_original_bytes for the relocated section
table (e_shoff moves past EOF, e_shnum grows by one).
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.59459% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.00%. Comparing base (cc8922e) to head (8e66f32).

Files with missing lines Patch % Lines
lib.rs 94.59% 10 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #72      +/-   ##
==========================================
+ Coverage   68.95%   72.00%   +3.05%     
==========================================
  Files           3        3              
  Lines         963     1036      +73     
==========================================
+ Hits          664      746      +82     
+ Misses        299      290       -9     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@littledivy
littledivy merged commit 9c63027 into main Jun 24, 2026
4 checks passed
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.

2 participants