Announce the interpreter as mapped code, so ld.so can have a unit

gex's loader places ld.so at INTERP_BASE itself, so no guest mmap reported
it and the ELF-unit hook (nt_cgen::on_elf_maps, cgen::libprov) never saw it:
it always ran on the runtime translator (1.8 MiB of it at Hollow Knight's
first frame in the page). Both hosts now push CodeMap::of_placed for its
executable segments into Kernel::new_code at load, and the drain after the
first syscall places its unit like any library's
(GEX_ELF_UNITS / ?elfunits=ld-linux-x86-64.so.2).

tests/elf-units-remap builds the interpreter's unit at 0x4a00000 too and
checks it is placed once.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_1896c5b302a54696aaa92c5f4047f20b
This commit is contained in:
velzieandClaude Opus 5.5 committed 2026-10-09 14:06:46 -04:00
1 parent 1fc1a8dd89
commit 29cbcc4dc6
4 files changed
+65 -4

No files matched your search

+38
View File
@@ -1071,6 +1071,44 @@ pub struct CodeMap {
pub offset: u64,
}
impl CodeMap {
/// The executable segments of an ELF object gex's own loader placed at
/// load bias `bias` (the interpreter, at exec): what its `mmap` would have
/// reported had `ld.so` mapped it, so a host's ELF units see it too.
pub fn of_placed(path: &str, bytes: &[u8], bias: u64) -> Vec<CodeMap> {
let rd = |o: usize, n: usize| -> Option<u64> {
let b = bytes.get(o..o + n)?;
let mut v = [0u8; 8];
v[..n].copy_from_slice(b);
Some(u64::from_le_bytes(v))
};
let mut out = Vec::new();
if bytes.get(0..4) != Some(b"\x7fELF") || bytes.get(4) != Some(&2) {
return out;
}
let (Some(phoff), Some(phentsize), Some(phnum)) = (rd(0x20, 8), rd(0x36, 2), rd(0x38, 2)) else {
return out;
};
for i in 0..phnum as usize {
let ph = phoff as usize + i * phentsize as usize;
let (Some(ty), Some(flags), Some(off), Some(vaddr), Some(filesz)) =
(rd(ph, 4), rd(ph + 4, 4), rd(ph + 8, 8), rd(ph + 16, 8), rd(ph + 32, 8))
else {
break;
};
if ty == 1 && flags & 1 != 0 && filesz != 0 {
out.push(CodeMap {
start: bias + (vaddr & !0xfff),
end: crate::mem::page_align_up(bias + vaddr + filesz),
path: path.to_string(),
offset: off & !0xfff,
});
}
}
out
}
}
/// A `MAP_SHARED` file mapping, and where its bytes belong.
///
/// A file mapping here is an eager copy rather than a page table pointed at
+8
View File
@@ -3782,6 +3782,14 @@ pub extern "C" fn gex_load(ptr: u32, len: u32, argv: u32, argv_len: u32) -> u64
// than copied: this runs once, and a second `gex_load` in the same Worker
// is an `execve`, which gets its placement from the global table like
// every other process.
// The interpreter, placed by gex's loader rather than mapped by the guest,
// announced as its `mmap` would have been: its function-tier unit
// (`?elfunits=ld-linux-x86-64.so.2`) is placed after the first syscall.
if let (Some(path), Some(_)) = (&program.exe.interp, &program.interp) {
if let Some(bytes) = read_file(path) {
kernel.new_code.extend(gex_kernel::CodeMap::of_placed(path, &bytes, program.interp_base()));
}
}
kernel.pins = std::mem::take(&mut *PINS.0.borrow_mut()).into_iter().collect();
// This host's memory has to be allocated up to whatever the map hands
// out, so the map asks before it hands anything out.
+9
View File
@@ -1005,6 +1005,15 @@ fn run_program_with(
.bind_host(path, Arc::new(w.clone()) as Arc<dyn gex_kernel::unix::HostListener>);
w
}));
// The interpreter, placed by gex's loader rather than mapped by the guest,
// announced as its `mmap` would have been: its function-tier unit
// (`GEX_ELF_UNITS=ld-linux-x86-64.so.2`) is placed after the first syscall.
if let (Some(path), Some(_)) = (&program.exe.interp, &program.interp) {
if let Some(bytes) = kernel.read_file(path) {
let maps = gex_kernel::CodeMap::of_placed(path, &bytes, program.interp_base());
kernel.new_code.extend(maps);
}
}
let tid = kernel.pid;
let prof_proc = profile::new_process(&kernel, program, kernel.pid, &opts.args, &opts.env);
+10 -4
View File
@@ -4,6 +4,8 @@
# code, and a library mapped again must get its unit again -- a unit whose
# code was unmapped is retired, not revived by the next object's mapping
# (nt_cgen::on_retire, ftab::on_retire; the browser's cgen::on_retire).
# The interpreter has a unit too: gex's loader places it, and the kernel
# announces it as the guest's own mmap would have (CodeMap::of_placed).
#
# tests/elf-units-remap/run.sh [gexc] (needs gcc; host glibc as the guest's)
set -u
@@ -17,19 +19,23 @@ gcc -O1 -o "$W/main" "$HERE/main.c" -ldl || exit 1
# Both at one pinned base, so the units built for it apply.
BASE=0x30000000
printf '%s 0x10000 liba.so\n%s 0x10000 libb.so\n' $BASE $BASE > "$W/pins.txt"
export GEX_CGEN_CACHE="$W/cgen" GEX_ELF_UNITS=liba.so,libb.so GEX_ELF_UNITS_BUILD=0
INTERP=$(readelf -lW "$W/main" | sed -n 's/.*Requesting program interpreter: \(.*\)]/\1/p')
export GEX_CGEN_CACHE="$W/cgen" GEX_ELF_UNITS=liba.so,libb.so,${INTERP##*/} GEX_ELF_UNITS_BUILD=0
for l in liba libb; do
"$GEXC" cgen-image --elf "$W/$l.so" --base $BASE --jobs 2 > "$W/$l.cgen.log" 2>&1 || { cat "$W/$l.cgen.log"; exit 1; }
done
# At gex_abi::INTERP_BASE.
"$GEXC" cgen-image --elf "$INTERP" --base 0x4a00000 --jobs 2 > "$W/interp.cgen.log" 2>&1 || { cat "$W/interp.cgen.log"; exit 1; }
native=$("$W/main" "$W")
fail=0
for flags in "" "--fjit"; do
gex=$(timeout 60 "$GEXC" run $flags --pin "$W/pins.txt" "$W/main" "$W" 2> "$W/err.txt")
placed=$(grep -c 'function tier for' "$W/err.txt")
if [ "$native" = "$gex" ] && [ "$placed" = 4 ]; then
placed=$(grep -c 'function tier for .*/lib[ab]\.so' "$W/err.txt")
interp=$(grep -c "function tier for $INTERP at 0x4a00000" "$W/err.txt")
if [ "$native" = "$gex" ] && [ "$placed" = 4 ] && [ "$interp" = 1 ]; then
echo "ok elf-units-remap ${flags:-t1}"
else
echo "FAIL elf-units-remap ${flags:-t1} (units placed $placed times, want 4)"
echo "FAIL elf-units-remap ${flags:-t1} (library units placed $placed times, want 4; the interpreter's $interp, want 1)"
printf 'native:\n%s\ngex:\n%s\n' "$native" "$gex"; tail -20 "$W/err.txt"; fail=1
fi
done