page: a replaced library's bound exports get their bridge back on a miss

In Hollow Knight's page run (hp2, hp3) a thread ran the original x86
mono_unity_liveness_finalize of libmonobdwgc (bound to the Mono provider)
and trapped on a wild pointer: its address reached tier-1 instead of the
provider. The miss path re-bridged only ELF export providers' addresses;
`elfprov::provided` now counts a replaced library's bound addresses too,
and a miss elsewhere in a replaced library's text is said
(`libprov::note_stray`). hp4 reaches HK's main menu.

browser-linux.mjs: --home DIR (entries linked into the guest's $HOME).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_d2453757b8a84fdbb4ee7c9e093141df
This commit is contained in:
velzieandClaude Opus 5.5 committed 2026-10-08 12:07:24 -04:00
1 parent 8c2c9a7dca
commit be33e8a41b
4 files changed
+28 -4

No files matched your search

+1 -1
View File
@@ -51,7 +51,7 @@ pub extern "C" fn gex_elfprov_off() {
/// Whether an ELF provider holds `addr` (the function JIT leaves it alone).
pub(crate) fn provided(addr: u64) -> bool {
lock(&BOUND).iter().any(|b| b.addrs.contains(&addr))
lock(&BOUND).iter().any(|b| b.addrs.contains(&addr)) || super::libprov::bound(addr)
}
/// A new process (a forked child): start over from what its kernel has
+17
View File
@@ -76,6 +76,23 @@ pub(crate) fn replaced(addr: u64) -> bool {
lock(&REPLACED).iter().any(|&(lo, hi)| addr >= lo && addr < hi)
}
/// Whether `addr` is one a replaced library's provider is bound at (its
/// pairs are sorted by address).
pub(crate) fn bound(addr: u64) -> bool {
lock(&LIBS).iter().any(|l| l.pairs.binary_search_by_key(&addr, |&(a, _)| a).is_ok())
}
/// A dispatch miss in a replaced library's own text that no binding
/// explains: its x86 code was never meant to run (said once per address).
pub(crate) fn note_stray(pc: u64) {
static SEEN: Mutex<Vec<u64>> = Mutex::new(Vec::new());
let mut seen = lock(&SEEN);
if seen.len() < 64 && !seen.contains(&pc) {
seen.push(pc);
crate::warn_line(&format!("gex: the guest reached {pc:#x}, the replaced library's own code (not a bound export): tier-1 runs it\n"));
}
}
/// Guest pages below 4 GiB: a provider's data and code page (its data base
/// and the guest addresses it binds are 32-bit numbers to the page).
fn alloc_low(size: u64, prot: u32, name: &str) -> Result<u64, String> {
+5 -1
View File
@@ -10878,7 +10878,8 @@ pub extern "C" fn gex_dispatch_miss(_ctx: i32, pc: i64) -> i32 {
});
trap();
}
// An address an ELF export provider holds whose slot lost its bridge.
// An address an ELF export provider (or a replaced library's provider)
// holds whose slot lost its bridge.
if cgen::elfprov::rebridge(pc) {
with_host(|h| {
set_resume(pc);
@@ -10886,6 +10887,9 @@ pub extern "C" fn gex_dispatch_miss(_ctx: i32, pc: i64) -> i32 {
});
trap();
}
if cgen::libprov::replaced(pc) {
cgen::libprov::note_stray(pc);
}
remiss(pc);
if tracing() || unsafe { TRACE_MISSES } {
// Before the translation: the question is which block the translator
+5 -2
View File
@@ -6,7 +6,7 @@
// native run uses, tests/games/*.wl), timed from the page's Nth GL present.
//
// node tests/games/browser-linux.mjs --name NAME --dir GAME_DIR --exe REL
// [--cwd REL] [--args ARGS] [--libs DIR:DIR] [--files LIST]
// [--cwd REL] [--args ARGS] [--libs DIR:DIR] [--files LIST] [--home DIR]
// [--input SCRIPT.wl] [--size WxH] [--minutes N] [--shot DIR]
// [--profile AT,SECS[,MATCH]] [--gameplay N] [--mem64] [--fjit] [--wasmdlls SO=PATH] [--query K=V&...] [--env K=V,K=V] [--end-on-exit]
//
@@ -26,7 +26,7 @@
// Prints `ok` lines (first frame, the input script's start, gameplay) and the
// frame rate over the last minute, with a screenshot every 30 s.
import { existsSync, mkdirSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { deflateSync, crc32 } from 'node:zlib';
@@ -91,6 +91,9 @@ function bundle() {
rmSync(stage, { recursive: true, force: true });
mkdirSync(join(stage, 'gex/libs'), { recursive: true });
mkdirSync(join(stage, 'home/g'), { recursive: true });
// --home DIR: its entries in the guest's $HOME (saved settings, so a game skips first-run screens).
const home = opt('--home', null);
if (home) for (const e of readdirSync(home)) symlinkSync(join(home, e), join(stage, 'home/g', e));
mkdirSync(join(stage, 'tmp'), { recursive: true });
symlinkSync(gameDir, join(stage, 'game'));
symlinkSync(GL, join(stage, 'gex/gl'));