Skip to content

fix(elf): keep the relocated program header table reachable via AT_PHDR - #78

Merged
littledivy merged 1 commit into
mainfrom
fix/elf-gvisor-at-phdr
Jul 2, 2026
Merged

fix(elf): keep the relocated program header table reachable via AT_PHDR#78
littledivy merged 1 commit into
mainfrom
fix/elf-gvisor-at-phdr

Conversation

@littledivy

Copy link
Copy Markdown
Member

Fixes denoland/deno#35700.

Problem

deno compile binaries crash at startup when run directly under gVisor / Google Cloud Run:

Inconsistency detected by ld.so: rtld.c: rtld_setup_main_map:
  Assertion `GL(dl_rtld_map).l_libname' failed!

(a plain SIGSEGV for section-header-stripped binaries). Running through the interpreter explicitly — ld-linux-x86-64.so.2 ./app — works.

Root cause

append relocates the program header table into a new PT_LOAD past EOF, packed right above the highest existing vaddr. That gives the new segment a different file-offset→vaddr bias than the first PT_LOAD (for the deno base, ~23 MB different). Loaders that recompute AT_PHDR from the segment table (the Linux kernel, ld.so via an explicit interpreter) cope, but a loader that trusts the classic invariant

AT_PHDR = load_bias + (first_load.p_vaddr - first_load.p_offset) + e_phoff

— which gVisor does — lands tens of MB off the real table, reads garbage program headers, and dies before main. The native-addon detail in the original report is incidental: a trivial console.log binary reproduces it.

Fix

Pin the new segment's virtual address to new_phoff + first_load_bias so it carries the same bias as the first PT_LOAD, making the naive AT_PHDR computation correct. new_phoff is bumped when needed so the vaddr still clears every existing segment (and the note still lands past .bss).

Verified

Reproduced under gVisor (runsc) with elfutils/glibc from Ubuntu; the exact assertion fires before the fix and is gone after, for PIE (+RELR), non-PIE, large .bss, and section-header-stripped inputs. Composes with the eu-strip section cover (an eu-stripped binary also runs under gVisor). Adds a regression test asserting the AT_PHDR invariant.

`append` moves the program header table into a new PT_LOAD past EOF and packed
it right above the highest existing vaddr, giving that segment a different
file-offset-to-vaddr bias than the first PT_LOAD. Loaders that recompute
`AT_PHDR` from the segment table (the kernel, ld.so via an explicit
interpreter) cope, but a loader that trusts the classic invariant

    AT_PHDR = load_bias + (first_load.p_vaddr - first_load.p_offset) + e_phoff

lands ~tens of MB off the real table, reads garbage program headers, and dies
before `main`. That is what happens to `deno compile` binaries run directly
under gVisor / Google Cloud Run (denoland/deno#35700):

    Inconsistency detected by ld.so: rtld.c: rtld_setup_main_map:
      Assertion `GL(dl_rtld_map).l_libname' failed!

(and a plain SIGSEGV for stripped binaries). Running through the interpreter
explicitly — `ld-linux-x86-64.so.2 ./app` — works, because ld.so then reads
`e_phoff` from the file.

Pin the new segment's virtual address to `new_phoff + first_load_bias` so it
carries the same bias as the first PT_LOAD, making the naive `AT_PHDR`
computation correct. `new_phoff` is bumped when needed so the vaddr still
clears every existing segment (and the note still lands past .bss).

Reproduced and verified fixed under gVisor for PIE (+RELR), non-PIE, large
.bss, and section-header-stripped inputs; the fix also composes with the
eu-strip section cover. Adds a regression test asserting the AT_PHDR invariant.
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 72.82%. Comparing base (433a918) to head (5212ba9).

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #78      +/-   ##
==========================================
+ Coverage   72.53%   72.82%   +0.28%     
==========================================
  Files           3        3              
  Lines        1067     1078      +11     
==========================================
+ Hits          774      785      +11     
  Misses        293      293              

☔ 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 27caac2 into main Jul 2, 2026
4 checks passed
littledivy added a commit to denoland/deno that referenced this pull request Jul 2, 2026
…d Run (#35701)

Fixes #35700.

## Problem

A `deno compile` binary with a native npm addon crashes at startup on
Google Cloud Run (`debian:bookworm-slim`, run directly as PID 1):

```
Inconsistency detected by ld.so: rtld.c: rtld_setup_main_map:
  Assertion `GL(dl_rtld_map).l_libname' failed!
```

(a plain `Uncaught signal: 11` for the reduced repro). Invoking through
the loader explicitly — `CMD ["/lib64/ld-linux-x86-64.so.2",
"/app/main"]` — works.

## Root cause

Cloud Run runs under **gVisor**. libsui's in-place `Elf::append`
relocates the program header table into a new `PT_LOAD` past EOF, packed
just above the highest existing vaddr — giving that segment a different
file-offset→vaddr bias than the first `PT_LOAD` (~23 MB for the deno
base). gVisor computes `AT_PHDR = load_bias + (first_load.p_vaddr -
first_load.p_offset) + e_phoff`, so it lands tens of MB off the real
table, reads garbage program headers, and dies before `main`. The Linux
kernel recomputes `AT_PHDR` from the segment table, so it only breaks in
the sandbox. The native addon is incidental — a trivial `console.log`
binary reproduces it.

## Fix

libsui 0.16.3 (denoland/sui#78) pins the relocated segment's vaddr to
`new_phoff + first_load_bias`, preserving the first load's bias so the
naive `AT_PHDR` computation is correct.

## Verification

Reproduced under gVisor (`runsc`) on Linux x86_64:

- the compiled `@node-rs/bcrypt` repro built with the current libsui
crashes with the exact assertion; built with 0.16.3 it runs (`gVisor
exit=0`).
- also verified for PIE (+RELR), non-PIE, large `.bss`, and
section-header-stripped inputs, and that it composes with the eu-strip
fix (an eu-stripped binary also runs under gVisor).

The fix carries an `AT_PHDR`-invariant regression test in libsui.
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.

deno compile with native npm addon fails on Cloud Run in 2.9.x

2 participants