browser: a wasm32 program's fork child installs the module at the parent's table base

A wasm32 program exec'd by another (bash running make, apt running dpkg)
gets a table base past its predecessor's range; its fork child published
the module fresh at the first base, so every function pointer in the data
it copied was off and the child trapped (function signature mismatch in
__gex_fork_drive). The fork record carries the base (gex_wasmprog_base)
and the child publishes there (FuncMap::publish_at, gex_wasmprog_adopt_at).

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 01:47:45 -04:00
1 parent 6853931bbd
commit bdd34eb3f0
3 files changed
+60 -9

No files matched your search

+14
View File
@@ -134,6 +134,20 @@ impl FuncMap {
n
}
/// [`FuncMap::publish`] at `base` when nothing has taken it yet: a fork's
/// child of a wasm32 program installs the parent's module where the
/// parent had it, because the function pointers in the data it copied
/// are `__table_base + i` for the parent's base (an exec'd program's base
/// is past its predecessor's range, which is never reused). Returns
/// whether the install got that base.
pub fn publish_at(&mut self, base: u32, len: u32, module: u32, tier: Tier, funcs: &[(u64, u32)]) -> (u32, bool) {
if base >= self.next {
self.next = base;
}
let n = self.publish(len, module, tier, funcs);
(n, self.installs[n as usize].base == base)
}
/// Map more addresses to slots of install `install`: one module serving
/// several images (Wine's PE side as one module, docs/wine-native.md
/// §3.14, bound DLL by DLL as each maps). Nothing changes in any table
+7 -1
View File
@@ -2017,7 +2017,10 @@ export class Gex {
child.brk ?? 0n,
);
if (child.compat) e.gex_set_compat?.(1);
if (child.wasm32) e.gex_wasmprog_adopt?.(BigInt(child.wasm32));
if (child.wasm32) {
if (child.wasm32Base && e.gex_wasmprog_adopt_at) e.gex_wasmprog_adopt_at(BigInt(child.wasm32), child.wasm32Base);
else e.gex_wasmprog_adopt?.(BigInt(child.wasm32));
}
// The parent's block numbering carries on in the child (its low region),
// over slot maps of its own (`dense::child`).
this.#dense = !!(child.opts ?? {}).dense && !!this.#guest.exports.blocks && !!e.gex_set_dense;
@@ -2285,6 +2288,9 @@ export class Gex {
compat: e.gex_compat?.() ?? 0,
// A wasm32 program's entry: the child installs the same module.
wasm32: e.gex_wasmprog_entry?.() ?? 0n,
// And where its module sits in the function table: the child's copy
// of the data holds function pointers for that base.
wasm32Base: e.gex_wasmprog_base?.() ?? 0,
// Where the parent's heap had reached. Without the number the child's
// kernel thinks it has no heap and grows one from zero, over its contexts.
brk: e.gex_fork_brk?.() ?? 0n,
+39 -8
View File
@@ -29,10 +29,13 @@ struct Prog {
}
static PROGS: Mutex<Vec<Prog>> = Mutex::new(Vec::new());
/// The program to publish (entry, whether its data is fresh).
static STAGED: Mutex<Option<(u64, bool)>> = Mutex::new(None);
/// The program to publish (entry, whether its data is fresh, the table base
/// it must have: a fork's child's, the parent's).
static STAGED: Mutex<Option<(u64, bool, Option<u32>)>> = Mutex::new(None);
/// This process's program's entry while it is a wasm32 one, for a fork.
static CURRENT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
/// And its module's `__table_base`.
static CURRENT_BASE: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
pub(crate) const MAGIC: &[u8; 8] = b"GEXWASM\0";
@@ -52,7 +55,7 @@ pub(crate) fn is_wasm32(program: &gex_ld::Program) -> bool {
/// it runs if it is a wasm32 one.
pub(crate) fn stage(program: Option<&gex_ld::Program>) {
CURRENT.store(0, Ordering::Relaxed);
*lock(&STAGED) = program.filter(|p| is_wasm32(p)).map(|p| (p.exe.entry, true));
*lock(&STAGED) = program.filter(|p| is_wasm32(p)).map(|p| (p.exe.entry, true, None));
}
/// A fork's child of a wasm32 program at `entry`: the same module, over the
@@ -60,10 +63,26 @@ pub(crate) fn stage(program: Option<&gex_ld::Program>) {
#[no_mangle]
pub extern "C" fn gex_wasmprog_adopt(entry: u64) {
if entry != 0 {
*lock(&STAGED) = Some((entry, false));
*lock(&STAGED) = Some((entry, false, None));
}
}
/// [`gex_wasmprog_adopt`] with the parent's table base
/// ([`gex_wasmprog_base`]): the data the child copied holds function
/// pointers for that base, so the module goes there.
#[no_mangle]
pub extern "C" fn gex_wasmprog_adopt_at(entry: u64, base: u32) {
if entry != 0 {
*lock(&STAGED) = Some((entry, false, (base != 0).then_some(base)));
}
}
/// The running wasm32 program's `__table_base` (the fork record's).
#[no_mangle]
pub extern "C" fn gex_wasmprog_base() -> u32 {
if CURRENT.load(Ordering::Relaxed) == 0 { 0 } else { CURRENT_BASE.load(Ordering::Relaxed) }
}
/// The running program's entry if it is a wasm32 one (the fork record's).
#[no_mangle]
pub extern "C" fn gex_wasmprog_entry() -> u64 {
@@ -73,8 +92,8 @@ pub extern "C" fn gex_wasmprog_entry() -> u64 {
/// Publish the staged program: its provider module number, or -1.
#[no_mangle]
pub extern "C" fn gex_wasmprog_serve() -> i32 {
let Some((entry, relocate)) = lock(&STAGED).take() else { return -1 };
match publish(entry, relocate) {
let Some((entry, relocate, at)) = lock(&STAGED).take() else { return -1 };
match publish(entry, relocate, at) {
Ok(n) => n as i32,
Err(e) => {
crate::warn_line(&format!("gex: wasm32 program: {e}\n"));
@@ -84,7 +103,7 @@ pub extern "C" fn gex_wasmprog_serve() -> i32 {
}
}
fn publish(entry: u64, relocate: bool) -> Result<u32, String> {
fn publish(entry: u64, relocate: bool, want_base: Option<u32>) -> Result<u32, String> {
let mem = crate::guest_bytes();
let at = entry.wrapping_sub(0x40) as usize;
let h = mem.get(at..at + 36).ok_or("header outside memory")?;
@@ -101,7 +120,19 @@ fn publish(entry: u64, relocate: bool) -> Result<u32, String> {
let slot = |n: &str| names.iter().position(|s| s.as_deref() == Some(n)).map(|k| k as u32).ok_or(format!("{n} is not in its table"));
let pairs = vec![(entry, slot("__gex_start")?), (thread_entry, slot("__gex_thread_start")?)];
let number = lock(&PROVIDERS).len() as u32;
ftab(|m| m.publish(names.len() as u32, PROVIDER | number, Tier::Unit, &pairs));
let install = ftab(|m| match want_base {
Some(base) => {
let (n, ok) = m.publish_at(base, names.len() as u32, PROVIDER | number, Tier::Unit, &pairs);
if !ok {
crate::warn_line(&format!(
"gex: wasm32 fork child: table base {base} is taken, its function pointers will be wrong\n"
));
}
n
}
None => m.publish(names.len() as u32, PROVIDER | number, Tier::Unit, &pairs),
});
CURRENT_BASE.store(ftab(|m| m.install(install).base), Ordering::Relaxed);
let addrs: Vec<u64> = pairs.iter().map(|&(a, _)| a).collect();
for &a in &addrs {
gex_kernel::ftab::fcache_clear(crate::guest_bytes(), a);