page: stateful Linux providers (B1): Unity's Mono, wasm64, replaces libmonobdwgc in Chromium

- cgen/libprov.rs: a shared object named in ?wasmdlls=soname=path is replaced when it maps
  (exports bound to the provider's gexp_*, constructors to gexp__noop, its code page and
  reverse slots, data below 4 GiB), installed as Wine's PE module is (PROVIDER | number);
  guest_sym, guest_stack_end, abi, table_size, missing; the function JIT leaves its text alone;
- gex.mjs: the wasm64 provider_* imports (try/throw on a per-Worker WebAssembly.Tag, i64
  stack), its syscall marked so the shim serves it without leaving the provider's frames;
- shim: such a syscall is served as a wasm32 program's (threads staged, waits in place);
  FUTEX_LOCK_PI/LOCK_PI2 in the page (taken when free, else retried), outside the host lock;
  provider data and stacks below 4 GiB;
- page: ?wasmdlls=, browser-linux.mjs --wasmdlls.

tests/unity-dotnet/embed in Chromium (interpreter, GEXMONO_WASM_JIT=0): every line as natively,
once with "worker thread: 1 bad of 640" (the GC does not stop guest threads in the page yet).

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-07 22:21:43 -04:00
1 parent 1e4ae9d7d9
commit 182e7cfbfc
12 files changed
+562 -18

No files matched your search

+1
View File
@@ -9,6 +9,7 @@ crate-type = ["cdylib"]
[dependencies]
gex-abi.workspace = true
gex-kernel.workspace = true
gex-atomic.workspace = true
gex-wayland.workspace = true
gex-xwm.workspace = true
gex-xlate = { workspace = true, features = ["fjit"] }
+105 -3
View File
@@ -688,6 +688,8 @@ export class Gex {
/// writer of `DISPATCH_DIR` reports itself, naming the one that moves it.
/// See #136.
static watchDir = -1;
/** `soname=path,...` (the page's `?wasmdlls=`): libraries replaced by wasm64 providers. */
static wasmDlls = '';
static #whySlot = 0;
#bytes() {
@@ -999,6 +1001,7 @@ export class Gex {
// there, and their units are looked up then.
if (this.#cgenMode) this.#shim.exports.gex_cgen_set_mode?.(this.#cgenMode);
if (this.#noElfProviders) this.#shim.exports.gex_elfprov_off?.();
this.#libprovConfig();
this.#shim.exports.gex_set_wl?.(this.#wl ? 1 : 0);
// `kernel.net` is decided inside `gex_load` from this flag, so it must be
// set before it (not merely in `useNetwork`, before the shim exists).
@@ -1440,6 +1443,7 @@ export class Gex {
// its parent's providers made code (cgen/elfprov.rs).
if (this.#cgenMode) this.#shim.exports.gex_cgen_set_mode?.(this.#cgenMode);
if (this.#noElfProviders) this.#shim.exports.gex_elfprov_off?.();
this.#libprovConfig();
// Narrate one page's directory writes, in every process. See #136.
if (Gex.watchDir >= 0) this.#shim.exports.gex_watch_dir?.(BigInt(Gex.watchDir));
}
@@ -2410,6 +2414,18 @@ export class Gex {
#cgenMode = 0;
/** No ELF export providers (cgen/elfprov.rs): A/B runs, every process of
* the run (a forked child's Worker is told too). Before the shim exists. */
/** `?wasmdlls=soname=path,...`: libraries this process replaces with wasm64
* providers (cgen/libprov.rs). */
#libprovConfig() {
const e = this.#shim.exports;
if (!Gex.wasmDlls || !e.gex_libprov_config) return;
const b = new TextEncoder().encode(Gex.wasmDlls);
const p = e.gex_alloc(Math.max(8, b.length)) >>> 0;
this.#bytes().set(b, p);
e.gex_libprov_config(p, b.length);
e.gex_free?.(p, Math.max(8, b.length));
}
noElfProviders() {
this.#noElfProviders = true;
this.#shim?.exports.gex_elfprov_off?.();
@@ -2525,6 +2541,8 @@ export class Gex {
const wasmdll = prov >= 0 && (e.gex_cgen_pmod_wasm_len?.(prov) >>> 0) > 0;
// A native wasm32 program (cgen/wasmprog.rs): its bytes come the same way.
const wasmprog = wasmdll && !!e.gex_cgen_pmod_wasmprog?.(prov);
// A replaced Linux library's wasm64 provider (cgen/libprov.rs): the 64-bit provider_* imports.
const libprov = wasmdll && !!e.gex_cgen_pmod_libprov?.(prov);
const mod = wasmdll
? this.#cgenFileModule(this.#cgenStr(e.gex_cgen_pmod_file, prov),
new Uint8Array(this.#memory.buffer, e.gex_cgen_pmod_wasm_ptr(prov) >>> 0, e.gex_cgen_pmod_wasm_len(prov) >>> 0).slice())
@@ -2550,7 +2568,7 @@ export class Gex {
// but a position-independent module imports its pointer all the same.
// A stateful provider's is this Worker's (one C stack per thread).
// A wasm32 program sets its own at each thread's entry.
__stack_pointer: memBase && !wasmprog ? this.#providerStack() : new WebAssembly.Global(m64() ? { value: 'i64', mutable: true } : { value: 'i32', mutable: true }, m64() ? 0n : 0),
__stack_pointer: memBase && !wasmprog ? (libprov ? this.#providerStack64() : this.#providerStack()) : new WebAssembly.Global(m64() ? { value: 'i64', mutable: true } : { value: 'i32', mutable: true }, m64() ? 0n : 0),
},
gex: {
...this.#imports, lookup: st.lookup, ftab: st.ftab,
@@ -2569,8 +2587,8 @@ export class Gex {
code_changed: (addr, len) => e.gex_code_changed?.(addr, len),
wine_pe_slot: (ptr, len) => e.gex_wine_pe_slot?.(ptr, len) ?? 0,
} : {}),
...(memBase && !wasmprog ? this.#providerImports() : {}),
...(wasmprog ? this.#wasmprogImports(() => inst) : wasmdll ? { syscall: this.#nativeSyscall } : {}),
...(memBase && !wasmprog ? (libprov ? this.#providerImports64() : this.#providerImports()) : {}),
...(wasmprog ? this.#wasmprogImports(() => inst) : libprov ? { syscall: this.#providerSyscall } : wasmdll ? { syscall: this.#nativeSyscall } : {}),
},
});
if (wasmprog ? e.gex_wasmprog_relocate(prov) : memBase && e.gex_provider_relocate(memBase)) inst.exports.__wasm_apply_data_relocs?.();
@@ -2612,6 +2630,71 @@ export class Gex {
return this.#provStack;
}
/** This Worker's stack for a wasm64 provider (i64 `__stack_pointer`), and its lowest address. */
#provStack64 = null;
#provStackLo = 0n;
#providerStack64() {
if (!this.#provStack64) {
const size = 1 << 20;
const e = this.#shim.exports;
const base = e.gex_provider_stack(size) >>> 0;
if (!base) throw new Error('gex: no memory for a provider stack');
this.#provStackLo = BigInt(base);
this.#provStack64 = new WebAssembly.Global({ value: 'i64', mutable: true }, BigInt(base + size - 16));
}
return this.#provStack64;
}
/** This Worker's exception tag for a wasm64 provider's `provider_throw`: a wasm
* exception, so code the provider publishes at run time (its JIT, which imports it)
* can catch it with `try_table`, and `provider_try` takes it when nothing did. */
#provTag = null;
#providerTag() {
this.#provTag ??= new WebAssembly.Tag({ parameters: ['i32'] });
return this.#provTag;
}
/** What a replaced Linux library's wasm64 provider imports from `gex` (the native
* `libprov::register64`; docs/unity-dotnet.md P5 B1). Pointers and table indices
* are i64 (BigInt). */
#providerImports64() {
const e = this.#shim.exports;
const ftab = this.#cgen.ftab;
const tag = this.#providerTag();
return {
provider_try: (f, arg) => {
try {
ftab.get(f)(arg);
return 0;
} catch (err) {
if (err instanceof WebAssembly.Exception && err.is(tag)) return err.getArg(tag, 0);
throw err;
}
},
provider_throw: (code) => {
throw new WebAssembly.Exception(tag, [code]);
},
provider_missing: (addr) => e.gex_libprov_missing(addr),
provider_thread_entry: () => e.gex_libprov_thread_entry(),
provider_code_page: () => e.gex_libprov_thread_entry(),
provider_stack: () => this.#provStackLo,
provider_guest_sym: (ptr, len) => e.gex_libprov_guest_sym(ptr, len),
provider_guest_stack_end: (sp) => e.gex_libprov_guest_stack_end(sp),
provider_abi: (which) => e.gex_libprov_abi(which),
provider_table_size: (base) => e.gex_libprov_table_size(base),
// Stopping the guest's threads around the runtime's collection: not yet in the page
// (each guest thread is a Worker; the stop needs the shim's safepoints).
provider_thread_register: () => {},
provider_park: () => {},
provider_stop_guest: () => {},
provider_start_guest: () => {},
// The runtime's wasm JIT: not yet in the page (B2); it runs interpreted.
provider_jit_reserve: () => 0n,
provider_jit_instantiate: () => -1,
provider_jit_table: () => 0n,
};
}
/** What a stateful provider imports from `gex` (the native `libprov`). */
#providerImports() {
const e = this.#shim.exports;
@@ -2668,6 +2751,13 @@ export class Gex {
const e = this.#shim.exports;
const n = e.gex_cgen_provider_count?.(u) >>> 0;
for (let i = 0; i < n; i++) {
// A replaced Linux library (cgen/libprov.rs): its wasm64 provider, installed as a module.
if (e.gex_cgen_provider_libprov?.(u, i)) {
const number = e.gex_libprov_publish(u, i);
this.#cgenCatchUp();
if (number >= 0) e.gex_cgen_pmod_installed(number);
continue;
}
if (e.gex_cgen_provider_wasmdll?.(u, i)) {
const number = e.gex_wasmdll_publish(u, i);
this.#cgenCatchUp();
@@ -2846,6 +2936,18 @@ export class Gex {
* install, a thread to start -- is served here and returns, so the native
* frames below it survive, as natively. Anything else (the process or
* thread ending, a callback's return) goes on up. */
/** A wasm64 provider's `gex.syscall(ctx, pc)` (cgen/libprov.rs): marked, so the shim
* serves it without leaving the provider's frames (a blocking call waits in place, a
* thread is staged); the host then starts what it staged, and the call returns. */
#providerSyscall = (ctx, pc) => {
const e = this.#shim.exports;
e.gex_syscall(ctx, BigInt.asIntN(64, BigInt(pc) | (1n << 61n)));
if (e.gex_wasmprog_staged()) {
this.#drainFork().catch((err) => console.error(`[gex] starting a process: ${err.message ?? err}`));
this.#drainThread().catch((err) => console.error(`[gex] starting a thread: ${err.message ?? err}`));
}
};
#nativeSyscall = (ctx, pc) => {
const e = this.#shim.exports;
const p = BigInt.asUintN(64, BigInt(pc));
+1
View File
@@ -414,6 +414,7 @@ async function handle(msg) {
if (msg.denseDebug) globalThis.GEX_DENSE_DEBUG = 1;
// A 64-bit guest memory (docs/mem64.md step 6): the wasm64 shim's process.
if (msg.mem64) globalThis.GEX_MEM64 = 1;
if (msg.wasmDlls) Gex.wasmDlls = msg.wasmDlls;
if (msg.calloutTrace) globalThis.GEX_CALLOUT_TRACE = 1;
if (msg.lazyThread) globalThis.GEX_LAZY_THREAD = 1;
// `kernelStack` is this Worker's slice of the kernel's stack array. Absent for
+3 -1
View File
@@ -36,6 +36,7 @@ use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
pub(crate) mod elfprov;
pub(crate) mod libprov;
mod wasmdll;
pub(crate) mod wasmprog;
@@ -866,7 +867,8 @@ fn provider_alloc_bytes(size: u64, name: &str) -> Result<u64, String> {
fn linux_alloc(size: u64, name: &str) -> Result<u64, String> {
use gex_kernel::mem::{flags, prot};
let len = (size + 0xfff) & !0xfff;
crate::with_host(|h| h.kernel.mem.mmap(0, len, prot::READ | prot::WRITE, flags::PRIVATE | flags::ANONYMOUS, name)).map_err(|e| format!("mapping {name}: errno {}", e.0))
// Below 4 GiB in a 64-bit memory too: the page hands these addresses around as 32 bits.
crate::with_host(|h| h.kernel.mem.mmap_low(0, len, prot::READ | prot::WRITE, flags::PRIVATE | flags::ANONYMOUS, name)).map_err(|e| format!("mapping {name}: errno {}", e.0))
}
/// [`provider_alloc_bytes`] from the top down (`boot::alloc_private_top`): a
+6 -2
View File
@@ -80,8 +80,12 @@ fn rescan_if_new() -> bool {
/// After a syscall: bind the providers of each newly mapped object. Returns
/// whether JS has providers to serve before the guest goes on.
pub(crate) fn maps(mapped: &[gex_kernel::CodeMap]) -> bool {
// A replaced library (a stateful Linux provider, libprov.rs) is served the same way.
if let Some(u) = super::libprov::maps(mapped) {
lock(&QUEUE).push_back(u);
}
if !on() {
return false;
return !lock(&QUEUE).is_empty();
}
if PID.load(Ordering::Acquire) == 0 {
PID.store(crate::with_host(|h| h.kernel.pid), Ordering::Release);
@@ -191,7 +195,7 @@ fn unbind_overlapped(m: &gex_kernel::CodeMap, trace: bool) {
#[no_mangle]
pub extern "C" fn gex_elfprov_wanted() -> i32 {
if !on() {
return -1;
return lock(&QUEUE).pop_front().map_or(-1, |u| u as i32);
}
rescan_if_new();
lock(&QUEUE).pop_front().map_or(-1, |u| u as i32)
+364
View File
@@ -0,0 +1,364 @@
//! Stateful Linux providers in the page (docs/unity-dotnet.md P5 B1): the
//! native host's `libprov` for a Linux process with a 64-bit memory. A shared
//! object named in the page's `wasmdlls` (`soname=guest path of a wasm64 PIC
//! module`, Unity's `libmonobdwgc-2.0.so=/gex/mono.wasm`) is replaced when it
//! maps: every export it defines is bound to the module's `gexp_<name>` (or
//! `gexp__missing`), its constructors and destructors to `gexp__noop`, and none
//! of its own code runs. The module is installed as Wine's PE module is
//! (`wasmdll.rs`): `PROVIDER | number` in the function table, its data from
//! low guest memory, its bytes in the shim's heap for every Worker to
//! instantiate (`gex.mjs` `#cgenInstall`, with the wasm64 `provider_*` imports).
//!
//! The protocol with `gex.mjs` (`#cgenProviders`):
//! - a unit's provider `i` with `gex_cgen_provider_libprov(u, i)` set is
//! published by `gex_libprov_publish(u, i)`, which returns its provider
//! module number; JS catches up (instantiating it) and calls
//! `gex_cgen_pmod_installed`.
//! - `gex_cgen_pmod_libprov(number)` says a module is one of these (its
//! imports are the 64-bit `provider_*` set).
use super::*;
use std::collections::HashMap;
use std::sync::atomic::AtomicU64;
/// `soname=path` pairs from the page (`gex_libprov_config`).
static DLLS: Mutex<Vec<(String, String)>> = Mutex::new(Vec::new());
/// One replaced library, mapped and waiting for (or past) its publish.
struct LinuxProv {
/// The library's path, for messages.
path: String,
/// The provider module.
wasm: Vec<u8>,
/// (guest address, slot) pairs.
pairs: Vec<(u64, u32)>,
/// Its data (`__memory_base`), below 4 GiB.
data: u64,
/// Its provider module number once published.
number: Option<u32>,
}
static LIBS: Mutex<Vec<LinuxProv>> = Mutex::new(Vec::new());
/// The guest address bound to the module's `__gex_thread_start` (threads it
/// creates start there; its code page: return addresses and reverse slots).
static THREAD_ENTRY: AtomicU64 = AtomicU64::new(0);
/// The replaced libraries' text: the function JIT leaves it alone (it would
/// otherwise follow a PLT into code that must never run).
static REPLACED: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
/// Exports bound to `gexp__missing`, by guest address: for the message.
static MISSING: Mutex<Option<HashMap<u64, String>>> = Mutex::new(None);
const CODE_PAGE: u64 = 0x10000;
const REVERSE_FIRST: u64 = 0x1000;
const REVERSE_STRIDE: u64 = 16;
/// `soname=path,...`: the libraries this process replaces (the page's
/// `?wasmdlls=`), before the program starts.
#[no_mangle]
pub extern "C" fn gex_libprov_config(ptr: u32, len: u32) {
let spec = String::from_utf8_lossy(&crate::guest_bytes()[ptr as usize..ptr as usize + len as usize]).into_owned();
let mut v = lock(&DLLS);
for item in spec.split(',') {
if let Some((so, path)) = item.split_once('=') {
let pair = (so.trim().to_string(), path.trim().to_string());
if !v.contains(&pair) {
v.push(pair);
}
}
}
}
/// Whether `addr` is in a replaced library's text.
pub(crate) fn replaced(addr: u64) -> bool {
lock(&REPLACED).iter().any(|&(lo, hi)| addr >= lo && addr < hi)
}
/// 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> {
use gex_kernel::mem::flags;
let len = (size + 0xfff) & !0xfff;
crate::with_host(|h| h.kernel.mem.mmap_low(0, len, prot, flags::PRIVATE | flags::ANONYMOUS, name)).map_err(|e| format!("mapping {name}: errno {}", e.0))
}
/// After a syscall that mapped code: a replaced library becomes a unit with
/// one provider for JS to serve (`elfprov`'s queue). Returns whether one was
/// queued.
pub(crate) fn maps(mapped: &[gex_kernel::CodeMap]) -> Option<usize> {
let dlls = lock(&DLLS).clone();
if dlls.is_empty() {
return None;
}
let mut queued = None;
for m in mapped {
let file = m.path.rsplit('/').next().unwrap_or(&m.path);
let Some((_, wasm_path)) = dlls.iter().find(|(so, _)| so == file) else { continue };
match prepare(m, wasm_path) {
Ok(Some(u)) => queued = Some(u),
Ok(None) => {}
Err(e) => {
crate::warn_line(&format!("gex: replacing {}: {e}\n", m.path));
}
}
}
queued
}
fn prepare(m: &gex_kernel::CodeMap, wasm_path: &str) -> Result<Option<usize>, String> {
let bytes = crate::with_host(|h| h.kernel.read_file(&m.path)).ok_or("cannot read the library")?;
let image = gex_ld::elf::load(&bytes, 0).map_err(|e| format!("{e:?}"))?;
let text = image.segments.iter().find(|s| s.executable).ok_or("no executable segment")?;
let bias = m.start.wrapping_sub(text.vaddr & !0xfff);
if lock(&LIBS).iter().any(|l| l.path == m.path) {
return Ok(None);
}
let (lo, hi) = (text.vaddr, text.vaddr + text.bytes.len() as u64);
lock(&REPLACED).push((lo.wrapping_add(bias), hi.wrapping_add(bias)));
let wasm = crate::with_host(|h| h.kernel.read_file(wasm_path)).ok_or_else(|| format!("cannot read its provider module {wasm_path}"))?;
let slots = gex_cgen::provider::slot_names(&wasm).map_err(|e| e.to_string())?;
let slot = |n: &str| slots.iter().position(|s| s.as_deref() == Some(n)).map(|k| k as u32);
let missing = slot("gexp__missing").ok_or("no gexp__missing in its module's table")?;
let mut provided: HashMap<&str, u32> = HashMap::new();
for (k, s) in slots.iter().enumerate() {
if let Some(e) = s.as_deref().and_then(|s| s.strip_prefix("gexp_")).filter(|e| !e.starts_with('_') && !e.starts_with("ptr_")) {
provided.insert(e, k as u32);
}
}
let mut pairs = Vec::new();
let mut unprovided = Vec::new();
for y in image.dynsyms.iter().filter(|y| !y.undefined && !y.tls && y.value >= lo && y.value < hi) {
let addr = y.value.wrapping_add(bias);
match provided.get(y.name.as_str()) {
Some(&k) => pairs.push((addr, k)),
None => {
pairs.push((addr, missing));
unprovided.push((addr, y.name.clone()));
}
}
}
if let Some(k) = slot("__gex_thread_start") {
let at = alloc_low(CODE_PAGE, gex_kernel::mem::prot::READ, &format!("[provider {file} code]", file = wasm_path))?;
pairs.push((at, k));
THREAD_ENTRY.store(at, Ordering::Release);
if let Some(r) = slot("gexp__reverse") {
for a in (at + REVERSE_FIRST..at + CODE_PAGE).step_by(REVERSE_STRIDE as usize) {
pairs.push((a, r));
}
}
}
if let Some(k) = slot("gexp__noop") {
let mut ctors: Vec<u64> = image.init_array.clone();
ctors.extend(elf_fini(&bytes));
for a in ctors.into_iter().filter(|&v| v >= lo && v < hi).map(|v| v.wrapping_add(bias)) {
pairs.retain(|&(x, _)| x != a);
pairs.push((a, k));
}
}
pairs.sort_by_key(|&(a, k)| (a, k == missing));
pairs.dedup_by_key(|&mut (a, _)| a);
lock(&MISSING).get_or_insert_with(Default::default).extend(unprovided);
let (size, align) = dylink_memory(&wasm)?;
let a = 1u64 << align;
let data = alloc_low((size as u64).max(16) + a, gex_kernel::mem::prot::READ | gex_kernel::mem::prot::WRITE, &format!("[provider {wasm_path} data]"))?;
let data = (data + a - 1) & !(a - 1);
crate::warn_line(&format!("gex: {} replaced by {wasm_path}: {} addresses bound, data at {data:#x}\n", m.path, pairs.len()));
let index = {
let mut libs = lock(&LIBS);
libs.push(LinuxProv { path: m.path.clone(), wasm, pairs, data, number: None });
libs.len() - 1
};
let u = {
let mut units = lock(&UNITS);
units.push(Unit {
key: String::new(),
path: m.path.clone(),
base: m.start,
wine: true,
pages: Vec::new(),
regs: Vec::new(),
dirs: Vec::new(),
slots: 0,
providers: vec![Provided { file: format!("libprov:{index}"), binds: Vec::new(), stateful: None, data: false, skeleton: None }],
identity: String::new(),
key_base: String::new(),
});
units.len() - 1
};
Ok(Some(u))
}
/// Whether provider `i` of unit `u` is a replaced Linux library's.
#[no_mangle]
pub extern "C" fn gex_cgen_provider_libprov(u: u32, i: u32) -> i32 {
lock(&UNITS).get(u as usize).and_then(|x| x.providers.get(i as usize)).is_some_and(|p| p.file.starts_with("libprov:")) as i32
}
/// Publish provider `i` of unit `u` (a replaced library): its provider
/// module number, or -1 (said).
#[no_mangle]
pub extern "C" fn gex_libprov_publish(u: u32, i: u32) -> i32 {
let index = {
let units = lock(&UNITS);
let Some(p) = units.get(u as usize).and_then(|x| x.providers.get(i as usize)) else { return -1 };
let Some(Ok(index)) = p.file.strip_prefix("libprov:").map(|s| s.parse::<usize>()) else { return -1 };
index
};
let mut libs = lock(&LIBS);
let Some(l) = libs.get_mut(index) else { return -1 };
if let Some(n) = l.number {
return n as i32;
}
let len = match gex_cgen::provider::slot_names(&l.wasm) {
Ok(s) => s.len() as u32,
Err(e) => {
crate::warn_line(&format!("gex: {}: {e}\n", l.path));
return -1;
}
};
let number = lock(&PROVIDERS).len() as u32;
ftab(|t| t.publish(len, PROVIDER | number, Tier::Unit, &l.pairs));
let addrs: Vec<u64> = l.pairs.iter().map(|&(a, _)| a).collect();
let mem = crate::guest_bytes();
for &a in &addrs {
gex_kernel::ftab::fcache_clear(mem, a);
}
let (regs, dirs, slots) = bridges(&addrs);
lock(&PROVIDERS).push(ProviderModule { file: format!("libprov:{}", l.path), regs, dirs, slots, memory_base: l.data as u32 });
l.number = Some(number);
number as i32
}
/// The module bytes of provider module `number` when it is a replaced
/// Linux library's (`gex_cgen_pmod_wasm_ptr`/`len`).
pub(crate) fn wasm(number: u32) -> Option<(u32, u32)> {
lock(&LIBS).iter().find(|l| l.number == Some(number)).map(|l| (l.wasm.as_ptr() as usize as u32, l.wasm.len() as u32))
}
/// Whether provider module `number` is a replaced Linux library's: its
/// imports are the 64-bit `provider_*` set.
#[no_mangle]
pub extern "C" fn gex_cgen_pmod_libprov(number: u32) -> i32 {
lock(&LIBS).iter().any(|l| l.number == Some(number)) as i32
}
/// `gex.provider_thread_entry()` / `provider_code_page()`.
#[no_mangle]
pub extern "C" fn gex_libprov_thread_entry() -> u64 {
THREAD_ENTRY.load(Ordering::Acquire)
}
/// `gex.provider_missing(addr)`: the program called an export the provider
/// does not implement. Ends the process, saying which.
#[no_mangle]
pub extern "C" fn gex_libprov_missing(addr: u64) {
let name = lock(&MISSING).as_ref().and_then(|m| m.get(&addr).cloned()).unwrap_or_else(|| "?".into());
panic!("the program called {name} ({addr:#x}), an export of a replaced library that its provider does not implement");
}
/// `gex.provider_table_size(base)`: the slots of the install at table
/// index `base`.
#[no_mangle]
pub extern "C" fn gex_libprov_table_size(base: u64) -> u64 {
ftab(|t| t.installs().iter().find(|i| i.base as u64 == base).map_or(0, |i| i.len as u64))
}
/// The dynamic symbols of a mapped object, by path: (name -> value, its
/// executable segment's page-aligned vaddr).
type ObjSyms = (HashMap<String, u64>, u64);
static OBJ_SYMS: Mutex<Option<HashMap<String, Arc<ObjSyms>>>> = Mutex::new(None);
/// `gex.provider_guest_sym(name, len)`: the guest address of `name` in the
/// first executable mapping that defines it (RTLD_DEFAULT's order), or 0.
#[no_mangle]
pub extern "C" fn gex_libprov_guest_sym(ptr: u64, len: u64) -> u64 {
let name = String::from_utf8_lossy(crate::guest_bytes().get(ptr as usize..(ptr + len) as usize).unwrap_or(&[])).into_owned();
let maps: Vec<(u64, String)> = crate::with_host(|h| {
h.kernel.mem.vmas().iter().filter(|v| v.prot & gex_kernel::mem::prot::EXEC != 0).filter_map(|v| v.code.as_ref().map(|(p, _)| (v.start, p.clone()))).collect()
});
let mut seen = std::collections::HashSet::new();
for (start, path) in maps {
if !seen.insert(path.clone()) {
continue;
}
let cached = lock(&OBJ_SYMS).get_or_insert_with(Default::default).get(&path).cloned();
let syms = match cached {
Some(s) => s,
None => {
let Some(bytes) = crate::with_host(|h| h.kernel.read_file(&path)) else { continue };
let Ok(image) = gex_ld::elf::load(&bytes, 0) else { continue };
let Some(text) = image.segments.iter().find(|s| s.executable) else { continue };
let map = image.dynsyms.iter().filter(|y| !y.undefined && !y.tls && y.value != 0).map(|y| (y.name.clone(), y.value)).collect();
let s = Arc::new((map, text.vaddr & !0xfff));
lock(&OBJ_SYMS).get_or_insert_with(Default::default).insert(path.clone(), Arc::clone(&s));
s
}
};
if let Some(v) = syms.0.get(&name) {
return start.wrapping_sub(syms.1).wrapping_add(*v);
}
}
0
}
/// `gex.provider_guest_stack_end(sp)`: the end of the mapping holding `sp`.
#[no_mangle]
pub extern "C" fn gex_libprov_guest_stack_end(sp: u64) -> u64 {
crate::with_host(|h| h.kernel.mem.vmas().iter().find(|v| v.start <= sp && sp < v.end).map_or(0, |v| v.end))
}
/// `gex.provider_abi(which)`: constants of gex's ABI a provider calling
/// guest code needs.
#[no_mangle]
pub extern "C" fn gex_libprov_abi(which: u32) -> u64 {
match which {
0 => gex_abi::FCACHE,
1 => gex_abi::FCACHE_BITS as u64,
2 => gex_abi::ctx::V as u64,
_ => u64::MAX,
}
}
/// DT_FINI and FINI_ARRAY's entries (unbiased), as the native host reads them.
fn elf_fini(bytes: &[u8]) -> Vec<u64> {
let u64at = |o: usize| bytes.get(o..o + 8).map(|b| u64::from_le_bytes(b.try_into().unwrap()));
let u32at = |o: usize| bytes.get(o..o + 4).map(|b| u32::from_le_bytes(b.try_into().unwrap()));
let u16at = |o: usize| bytes.get(o..o + 2).map(|b| u16::from_le_bytes(b.try_into().unwrap()));
let mut out = Vec::new();
let (Some(phoff), Some(phentsize), Some(phnum)) = (u64at(0x20), u16at(0x36), u16at(0x38)) else { return out };
let mut loads = Vec::new();
let mut dynamic = None;
for i in 0..phnum as usize {
let ph = phoff as usize + i * phentsize as usize;
let (Some(ty), Some(off), Some(va), Some(fsz)) = (u32at(ph), u64at(ph + 8), u64at(ph + 16), u64at(ph + 32)) else { continue };
if ty == 1 {
loads.push((va, off, fsz));
}
if ty == 2 {
dynamic = Some((off, fsz));
}
}
let file_of = |va: u64| loads.iter().find(|&&(v, _, s)| va >= v && va < v + s).map(|&(v, o, _)| (va - v + o) as usize);
let Some((doff, dsz)) = dynamic else { return out };
let (mut fini_array, mut fini_arraysz) = (None, 0u64);
for i in 0..dsz / 16 {
let (Some(tag), Some(val)) = (u64at((doff + i * 16) as usize), u64at((doff + i * 16 + 8) as usize)) else { break };
match tag {
0 => break,
13 => out.push(val),
26 => fini_array = Some(val),
28 => fini_arraysz = val,
_ => {}
}
}
if let Some(fa) = fini_array {
for i in 0..fini_arraysz / 8 {
if let Some(v) = file_of(fa + i * 8).and_then(u64at) {
out.push(v);
}
}
}
out
}
+2 -2
View File
@@ -217,7 +217,7 @@ fn bind(install: u32, pairs: &[(u64, u32)]) {
/// wasm32 program (in the shim's heap), else 0.
#[no_mangle]
pub extern "C" fn gex_cgen_pmod_wasm_ptr(number: u32) -> u32 {
if let Some((p, _)) = super::wasmprog::wasm(number) {
if let Some((p, _)) = super::wasmprog::wasm(number).or_else(|| super::libprov::wasm(number)) {
return p;
}
lock(&WINE_PE).as_ref().filter(|w| w.number == number).map_or(0, |w| w.wasm.as_ptr() as u32)
@@ -225,7 +225,7 @@ pub extern "C" fn gex_cgen_pmod_wasm_ptr(number: u32) -> u32 {
#[no_mangle]
pub extern "C" fn gex_cgen_pmod_wasm_len(number: u32) -> u32 {
if let Some((_, n)) = super::wasmprog::wasm(number) {
if let Some((_, n)) = super::wasmprog::wasm(number).or_else(|| super::libprov::wasm(number)) {
return n;
}
lock(&WINE_PE).as_ref().filter(|w| w.number == number).map_or(0, |w| w.wasm.len() as u32)
+1 -1
View File
@@ -157,7 +157,7 @@ pub(crate) fn translate(addr: u64) -> Option<u32> {
return None;
}
// An address an ELF provider holds is not code to translate (cgen::elfprov).
let executable = |a: u64| exec.iter().any(|&(s, e)| a >= s && a < e) && !crate::cgen::elfprov::provided(a);
let executable = |a: u64| exec.iter().any(|&(s, e)| a >= s && a < e) && !crate::cgen::elfprov::provided(a) && !crate::cgen::libprov::replaced(a);
let read_only = |a: u64, n: u64| ro.iter().any(|&(s, e)| a >= s && a.saturating_add(n) <= e);
// Writable-and-executable code (a JIT's) stays tier-1, as natively.
let volatile = |a: u64| wx.iter().any(|&(s, e)| a >= s && a < e);
+73 -8
View File
@@ -5871,6 +5871,15 @@ pub extern "C" fn gex_syscall(ctx: i32, pc: i64) {
if let Some(p) = nt::process() {
return nt::syscall(p, ctx, pc);
}
// A stateful provider's syscall (`PROVIDER_SYSCALL` set by the page's glue,
// cgen/libprov.rs): its wasm frames cannot be left, so it is served as a
// wasm32 program's is -- a blocking call waits here, a thread or a child
// is staged for the host -- and the call returns.
let pc = {
let provider = (pc as u64) & PROVIDER_SYSCALL != 0;
IN_PROVIDER.with(|c| c.set(provider));
((pc as u64) & !PROVIDER_SYSCALL) as i64
};
// Which syscall is running, so the allocator can name who was asking (wasm
// has no backtrace). See `HEAP_BY_CALL`.
{
@@ -6404,7 +6413,7 @@ pub extern "C" fn gex_syscall(ctx: i32, pc: i64) {
h.jit.ever_threaded = true;
}
stage_thread(tid, ctx_addr, start);
if h.kernel.wasm32 {
if frames_stay(h) {
// A wasm32 program's frames cannot be left: the call
// returns, and `#wasmprogSyscall` starts the thread.
WASM32_STAGED.store(true, std::sync::atomic::Ordering::Relaxed);
@@ -6452,10 +6461,10 @@ pub extern "C" fn gex_syscall(ctx: i32, pc: i64) {
deadline: deadline.map(|d| now_ns() + d.as_nanos() as u64),
},
gex_kernel::BlockOp::Sleep(d) => Waiting::Sleep(now_ns() + d.as_nanos() as u64),
// Priority-inheritance locks are not served here yet: the
// call finishes at once with ENOSYS, as before gex had them
// (`finish_parked`). docs/mem64.md step 6 brings the page.
gex_kernel::BlockOp::FutexLockPi { .. } => Waiting::Sleep(now_ns()),
// A priority-inheritance lock: tried when the thread runs
// again (`finish_parked`), and the syscall made again until
// it is taken (`PI_RETRY`); a short sleep between tries.
gex_kernel::BlockOp::FutexLockPi { .. } => Waiting::Sleep(now_ns() + 100_000),
gex_kernel::BlockOp::ReadStream { handle, .. } => {
// What the thing being waited on says about itself,
// at the moment of parking. A reader on a pipe whose
@@ -6528,7 +6537,7 @@ pub extern "C" fn gex_syscall(ctx: i32, pc: i64) {
child_stack,
tls,
} => {
if h.kernel.wasm32 {
if frames_stay(h) {
// A wasm32 process clones at its thread entry (the
// context's PC, as the musl fork's __clone leaves it);
// the call returns, and `#wasmprogSyscall` hands the
@@ -6614,7 +6623,7 @@ pub extern "C" fn gex_syscall(ctx: i32, pc: i64) {
recompile_objects(mapped);
// 3 means the running thread gave up the wasm stack: pick another, waiting
// first if it is waiting on another Worker (see `park_until_ready`).
let stop = if stop == 3 && with_host(|h| h.kernel.wasm32) {
let stop = if stop == 3 && with_host(|h| frames_stay(h)) {
wasm32_wait(pc as u64)
} else if stop == 3 {
// Ask first, wait second: most parked calls are already ready by now
@@ -6736,6 +6745,14 @@ pub extern "C" fn gex_syscall(ctx: i32, pc: i64) {
};
trace_line(&format!("[gex] t{i} = {}{split}{clk}\n", x0 as i64));
}
// A provider's priority-inheritance lock still held (`PI_RETRY` set the
// thread to make the call again): it has no instruction to go back to, so
// the call is made again here, outside the host lock.
if stop != 0 && IN_PROVIDER.with(|c| c.get()) && resume() == Some(pc as u64) {
set_resume_none();
set_resume_ctx_none();
return gex_syscall(ctx, (pc as u64 | PROVIDER_SYSCALL) as i64);
}
if stop != 0 {
trap();
}
@@ -6760,6 +6777,21 @@ const LOW_END: u64 = gex_abi::DISPATCH_DIR + gex_abi::DISPATCH_MAX_PAGES * 4;
// parent's regions, `wait4`/`kill`/`exit` route through the kernel's table, and
// each process is a Worker the engine schedules.
/// The mark the page's glue puts on a stateful provider's syscalls (as gexrt's
/// `NATIVE_SYSCALL` on native Wine's, `nt.rs`).
const PROVIDER_SYSCALL: u64 = 1 << 61;
thread_local! {
/// The syscall being served is a stateful provider's (see `gex_syscall`).
static IN_PROVIDER: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
/// Whether the syscall being served cannot leave its wasm frames: a wasm32
/// program's, or a stateful provider's.
fn frames_stay(h: &Host) -> bool {
h.kernel.wasm32 || IN_PROVIDER.with(|c| c.get())
}
/// A thread or a fork a wasm32 program's syscall staged without trapping
/// (`gex_wasmprog_staged`).
static WASM32_STAGED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
@@ -9518,6 +9550,12 @@ fn schedule() -> i32 {
let mut gm = GuestMem::new(guest_bytes());
h.kernel.enter_thread(&kstate);
let r = finish_parked(&op, timed_out, by_signal, &mut gm);
// A priority-inheritance lock still held: the syscall again,
// from its instruction (the registers are as the guest made the call).
if r == Err(PI_RETRY) {
h.kernel.leave_thread(&mut h.threads[cur()].kstate);
return Some((kstate.ctx_addr, pc - gex_abi::SYSCALL_INST_BYTES));
}
// A parked call's answer never reaches the syscall ring (the
// ring is written when the call is made), so an error here is
// otherwise invisible -- and glibc aborts over a bad futex one.
@@ -9594,6 +9632,10 @@ fn schedule() -> i32 {
///
/// A woken futex returns zero and the guest re-checks its own condition, the
/// same as on Linux, where a wake is never a promise about the word.
/// `finish_parked`'s answer for a priority-inheritance lock still held: make
/// the syscall again (never seen by the guest).
const PI_RETRY: Errno = Errno(0x7fff);
fn finish_parked(
op: &gex_kernel::BlockOp,
timed_out: bool,
@@ -9626,7 +9668,30 @@ fn finish_parked(
Err(Errno::ETIMEDOUT)
}
gex_kernel::BlockOp::FutexWait { .. } | gex_kernel::BlockOp::Sleep(_) => Ok(0),
gex_kernel::BlockOp::FutexLockPi { .. } => Err(Errno::ENOSYS),
// Take the lock if it is free (the waiters bit stays set, so the
// next unlock goes through the kernel); else `PI_RETRY`: the caller
// makes the syscall again, which parks once more.
gex_kernel::BlockOp::FutexLockPi { uaddr, tid, .. } => {
use gex_kernel::futex::pi;
loop {
let v = gex_atomic::load_u32(gm.bytes, *uaddr).ok_or(Errno::EFAULT)?;
match v & pi::TID_MASK {
0 => {
if gex_atomic::cas_u32(gm.bytes, *uaddr, v, *tid | pi::WAITERS).ok_or(Errno::EFAULT)?.is_ok() {
return Ok(0);
}
continue;
}
owner if owner == *tid => return Err(Errno::EDEADLK),
_ => {}
}
let want = v | pi::WAITERS;
if v != want && gex_atomic::cas_u32(gm.bytes, *uaddr, v, want).ok_or(Errno::EFAULT)?.is_err() {
continue;
}
return Err(PI_RETRY);
}
}
// The read the guest asked for happens now, into the buffer it named.
// Doing it here rather than re-executing the instruction is what keeps
// the guest's pc moving forward: a syscall that parks is finished by
+2 -1
View File
@@ -8,7 +8,7 @@
// node tests/games/browser-linux.mjs --name NAME --dir GAME_DIR --exe REL
// [--cwd REL] [--args ARGS] [--libs DIR:DIR] [--files LIST]
// [--input SCRIPT.wl] [--size WxH] [--minutes N] [--shot DIR]
// [--gameplay N] [--mem64] [--fjit] [--env K=V,K=V] [--end-on-exit]
// [--gameplay N] [--mem64] [--fjit] [--wasmdlls SO=PATH] [--env K=V,K=V] [--end-on-exit]
//
// --files lists host files the game opens, one per line (collected from a
// native `gexc run --strace`; tests/games/retrocycles-linux-files.txt).
@@ -128,6 +128,7 @@ async function main() {
const { evaluate, state, shot, consoleLines } = await openPage(
`tar=/gex/nt/${basename(tar)}&exe=/game/${exe}&cwd=/game/${cwd}&gl=1&glbackend=wgpu&hostx=1`
+ (argv.includes('--mem64') ? '&mem64=1' : '') + (argv.includes('--fjit') ? '&fjit=1' : '')
+ (opt('--wasmdlls', null) ? `&wasmdlls=${encodeURIComponent(opt('--wasmdlls'))}` : '')
+ `&size=${width}x${height}&trapstack=1&env=${encodeURIComponent(genv)}&args=${encodeURIComponent(opt('--args', ''))}`,
{ width: width + 20, height: height + 100, shotDir },
);
+2
View File
@@ -115,6 +115,8 @@ async function main() {
if (q.get('mem64') === '1') globalThis.GEX_MEM64 = 1;
// `?fjit=1`: the function JIT (gexc run --fjit's browser half, fjit.rs).
if (q.get('fjit')) globalThis.GEX_FJIT = Number(q.get('fjit')) || 1;
// `?wasmdlls=soname=path,...`: shared objects replaced by wasm64 providers (cgen/libprov.rs).
if (q.get('wasmdlls')) globalThis.GEX_WASM_DLLS = q.get('wasmdlls');
if (q.get('dense') === '0') globalThis.GEX_DENSE = 0;
if (q.get('dense') === '2') globalThis.GEX_DENSE = 2;
// cgen=1|2 (prefer|require units), in the unguarded flavor of i386 units
+2
View File
@@ -671,6 +671,8 @@ export async function runProgram(
mem64: globalThis.GEX_MEM64 ? 1 : undefined,
// `?fjit=1`: the function JIT in every process.
fjit: globalThis.GEX_FJIT || undefined,
// `?wasmdlls=soname=path,...`: libraries replaced by wasm64 providers (cgen/libprov.rs).
wasmDlls: globalThis.GEX_WASM_DLLS || undefined,
traceMisses: !!globalThis.GEX_TRACE_MISSES,
// `?sharetrace=1`: say how compiled modules are shared between a process's Workers.
shareTrace: flag('sharetrace') ? 1 : undefined,