Fix two PHT-relocation correctness bugs (#643, #639) - #652
Conversation
Add the genuine shim from MeshLib daily release v3.1.2.247 (2026-05-16, clang-21/lld-21) as a fixture: .init at 0x224 right after an 8-entry PHT ending at 0x200, DT_INIT=0x224. A --set-rpath that forces a 9th PT_LOAD relocates .init; control patchelf leaves DT_INIT stale, PR NixOS#652 syncs it. Asserted via readelf (no dlopen; the shim's deps aren't on the runner). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…valid) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Confirming this addresses a real downstream case — I filed #639 after hitting it while packaging MeshLib's Python bindings (an lld-built I recovered the actual triggering binary from one of our daily builds (clang/lld 21). It has exactly the tight layout that's hard to synthesize — Reproduction with patchelf 0.18.0 (the released line → One caveat if you want it as a regression test: on current |
|
Thanks for the detailed confirmation and for recovering the real binary. You're right, and it goes a bit deeper than this one binary. In Given that, I'll keep the #639 fix as defensive correctness (it mirrors the existing Thanks again, the binary is a useful end to end sanity check. |
Add a regression test to ensure that PHT collision detection correctly detects when growing the PHT in place would cause it to collide with another section, even if the PHT is not at the start of the ELF file.
Patching a shared library might require extending its program header table, which can then cause it to run out of its originally allocated space. After 484d349, patchelf handles this by detecting that growing the PHT in place would result in a collision with a section that won't be moved, in which case it instead moves the PHT to the end of the ELF file. The logic checking for collisions incorrectly assumes that the PHT is originally at the start of the file, though. It checks every section after the first to see if it overlaps with the PHT given its new size, but it assumes that the PHT's original position is immediately after the ELF header, `sizeof(Elf_Ehdr)` bytes into the file. In other words, it's currently looking for collisions in the range: [0, roundUp(sizeof(Elf_Ehdr) + newPhtSize, sectionAlignment)] This commit fixes it to check for collisions in the correct range: [hdr()->e_phoff, hdr()->e_phoff + roundUp(newPhtSize, sectionAlignment)]
The .dynamic fixup loop rewrites DT_STRTAB/DT_SYMTAB/DT_HASH/... to the new addresses of their sections, but never updated DT_INIT, DT_FINI, DT_INIT_ARRAY, DT_FINI_ARRAY or DT_PREINIT_ARRAY. When relocating one of those sections (e.g. growing the PHT pushes .init to the end of the file, common with lld's tight layout), the loader then jumps to a stale DT_INIT and the process SIGSEGVs, typically on dlopen. Update those entries from the relocated section addresses like the others. Closes NixOS#639 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
939c0ae to
100aa4a
Compare
|
Restructured this PR to build on @godlygeek's #644 rather than duplicate it:
Verified locally: |
ELF does not require section headers to be sorted by sh_offset, and this path never calls sortShdrs(). Breaking on the first header past the PHT window can skip a later-indexed colliding section.
|
@domenkozar feel free to drop a release |
Build a shared library whose DT_INIT points at a symbol in .text rather than the .init section, and check that patchelf leaves the tag alone.
These tags hold a function address (-Wl,-init/-fini), not the section start. .init/.fini are SHT_PROGBITS and never relocated here, so the rewrite only broke binaries with a custom init symbol. Keep the DT_*_ARRAY cases, which are section addresses by spec.
Exclude libstdc++.so.6 and libgcc_s.so.1 from auditwheel repair so patchelf is not invoked. patchelf <= 0.18.0 moves protodesc_cold without updating R_X86_64_RELATIVE addends, breaking protobuf init. Ref: NixOS/patchelf#652
Exclude libstdc++.so.6 and libgcc_s.so.1 from auditwheel repair so patchelf is not invoked. patchelf <= 0.18.0 moves protodesc_cold without updating R_X86_64_RELATIVE addends, breaking protobuf init. Ref: NixOS/patchelf#652
Fixes two independent correctness bugs in the program-header-table (PHT) relocation logic, both of which currently block bumping patchelf in nixpkgs.
#643 — in-place PHT growth assumes the PHT is right after the ELF header
rewriteSectionsLibrarydecides whether a section collides with the grown PHT by comparing the section's file offset againstphtSize, which only makes sense when the PHT sits at its canonical location right after the ELF header. When the PHT is elsewhere in the file — e.g. the OpenSSLlibcrypto.so.1.1shipped in many manylinux wheels, whose PHT sits near the end — that check looks in the wrong place, the PHT is wrongly kept in place, and patching aborts withcannot find section '.hash'.The fix detects the non-canonical PHT location up front (
e_phoff != sizeof(Elf_Ehdr)) and relocates the PHT to the end of the file in that case.Verified on the real
libcrypto.so.1.1from the issue: before the fix patchelf errorscannot find section '.hash'; after,--set-rpathsucceeds and the result is a structurally valid ELF.A regression test is included (
tests/pht-relocation.sh). The fixture is that reallibcrypto.so.1.1, stripped and xz-compressed (decompressed at test time, skipped ifxzis unavailable — mirroring the existingshort-first-segment.gzfixture). I could not reproduce the corruption with a minimally-synthesized binary — it depends on the real multi-segment / many-section layout — so the fixture is larger (~730 KB) than the existing ones. Happy to swap it for a smaller trigger if anyone knows of one.#639 —
.dynamicfixup never updatedDT_INIT/DT_FINI/DT_*_ARRAYThe
.dynamicfixup loop rewritesDT_STRTAB/DT_SYMTAB/DT_HASH/… to the new addresses of their sections, but never updatedDT_INIT,DT_FINI,DT_INIT_ARRAY,DT_FINI_ARRAYorDT_PREINIT_ARRAY. When growing the PHT relocates.init(common with lld's tight layout), the loader then jumps to a staleDT_INITand the process SIGSEGVs, typically ondlopen. The fix updates those entries from the relocated section addresses, like the others.This fix is correct by construction and the full test suite stays green, but I was not able to synthesize the exact lld layout that relocates
.init, so it has no dedicated regression test. Guidance on producing such a fixture welcome.These are independent; I'm happy to split into two PRs if that's easier to review/merge.
Assisted-by: Claude Code (Claude Opus 4.8)