Skip to content

fix(elf): keep program header table intact through eu-strip#75

Merged
littledivy merged 2 commits into
mainfrom
fix/elf-eu-strip-phdr-cover
Jul 2, 2026
Merged

fix(elf): keep program header table intact through eu-strip#75
littledivy merged 2 commits into
mainfrom
fix/elf-eu-strip-phdr-cover

Conversation

@littledivy

Copy link
Copy Markdown
Member

Problem

deno compile binaries built inside a flatpak sandbox segfault at startup (exit 139) since Deno 2.9 — denoland/deno#35633. Bisected to libsui 0.16.0 (the in-place Elf::append from #72).

Root cause

Elf::append relocates the program header table past the original EOF, into the file gap just before the appended note. flatpak-builder runs eu-strip (elfutils), which lays the stripped output out itself with ELF_F_LAYOUT and zero-fills every file gap that falls between two allocated sections. With only a note-sized .note.sui section past that gap, eu-strip treats the relocated program header table as dead space and zeroes it → all-NULL program headers → segfault on exec.

Reproduced directly with deno 2.9.0 on Linux: compiled binary → eu-strip → 14 NULL program headers, exit 139.

Two other theories were ruled out empirically:

  • Dropping the .note.sui section stops the crash but eu-strip then discards the unbacked note → "Could not find standalone binary section" (payload lost).
  • The discontiguous high PT_LOAD is not the trigger — patchelf produces the same layout and survives eu-strip; the deciding factor is whether the phdr table is covered by an allocated section.

Fix

Cover the relocated program header table with its own allocated .sui.phdrs (SHT_PROGBITS) section so section-based strip tools that preserve the byte layout keep it. It has to be a separate PROGBITS section rather than folding the bytes into .note.sui: BFD strip parses SHT_NOTE contents, so a note section spanning the (non-note) program header bytes corrupts the note and loses it on strip. .note.sui keeps covering only the note; the runtime still discovers it via its PT_NOTE program header.

Test

Adds test_elf_note_survives_eu_strip: appends a note, runs eu-strip, and asserts the program header table is not zeroed, the note still round-trips, and the stripped binary still executes (skips gracefully if eu-strip is absent). Updates test_elf_append_preserves_original_bytes for the extra section header.

Verified on Linux (elfutils 0.190): before — segfault; after — runs, phdrs intact, payload found. test_elf_note_survives_strip (BFD strip) still passes.

Note: BFD strip still breaks exec on these binaries, but that is pre-existing (0.16.0 behaves the same) and flatpak uses eu-strip.

`Elf::append` relocates the program header table past the original EOF,
into the file gap just before the appended note. `eu-strip` (elfutils, as
run by flatpak-builder) lays the stripped output out itself with
`ELF_F_LAYOUT` and zero-fills every file gap that falls between two
allocated sections. With only a note-sized `.note.sui` section past that
gap, eu-strip treats the relocated program header table as dead space and
zeroes it, producing a binary with an all-zero program header table that
segfaults on exec (exit 139). This is the deno 2.9 "compiled binary
segfaults in flatpak" regression (denoland/deno#35633), introduced when
the in-place append landed in 0.16.0.

Cover the program header table with its own allocated `.sui.phdrs`
(SHT_PROGBITS) section so section-based strip tools that preserve the byte
layout keep it. It must be a separate PROGBITS section rather than folding
the bytes into `.note.sui`: BFD `strip` parses SHT_NOTE contents, so a note
section spanning the (non-note) program header bytes corrupts the note and
loses it on strip. `.note.sui` keeps covering only the note; the runtime
still discovers it through its PT_NOTE program header.

Adds a regression test that appends a note, runs `eu-strip`, and asserts the
program header table is not zeroed, the note still round-trips, and the
stripped binary still executes.
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 72.74%. Comparing base (307b58c) to head (abc271f).

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #75      +/-   ##
==========================================
+ Coverage   72.00%   72.74%   +0.73%     
==========================================
  Files           3        3              
  Lines        1036     1064      +28     
==========================================
+ Hits          746      774      +28     
  Misses        290      290              

☔ 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 2815a01 into main Jul 2, 2026
4 checks passed
littledivy added a commit to denoland/deno that referenced this pull request Jul 2, 2026
…35699)

Fixes #35633

## Problem

Standalone binaries produced by `deno compile` segfault (exit 139) after
`eu-strip` runs over them — which `flatpak-builder` does automatically
during a flatpak build. Regression in Deno 2.9, bisected to the libsui
0.16.0 bump (#35467).

## Root cause

libsui's in-place `Elf::append` (new in 0.16.0) relocates the program
header table past the original EOF, into the file gap just before the
appended note. `eu-strip` (elfutils, as run by flatpak-builder) lays the
stripped output out itself with `ELF_F_LAYOUT` and zero-fills every file
gap that falls between two allocated sections. With only a note-sized
`.note.sui` section past that gap, eu-strip treats the relocated program
header table as dead space and zeroes it → all-NULL program headers →
segfault on exec.

## Fix

libsui 0.16.1 (denoland/sui#75) covers the relocated program header
table with a dedicated allocated `.sui.phdrs` section so section-based
strip tools preserve it. This bumps the dependency and adds a spec test
that `eu-strip`s a compiled binary and asserts it still runs (skips
gracefully where `eu-strip` isn't installed).

## Verification

Reproduced and fixed against the exact flatpak toolchain (elfutils
**0.193**, built from source) on Linux x86_64:

- real `deno 2.9.0` compiled binary + eu-strip 0.193 (flatpak flags) →
**segfault 139**; with 0.16.1 → **runs**
- every binary shape (PIE+RELR, plain PIE, non-PIE, large `.bss`,
section-header-stripped, fully stripped) × eu-strip 0.193 & 0.190 →
runs, program headers intact
- payload sizes 1 B – 1 MB → payload still recoverable after strip
- real denort 2.9.0 base → program headers intact after strip
- binutils `strip` note-survival preserved (unchanged from 0.16.0)
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