mirror of
https://github.com/HeyPuter/gex.git
synced 2026-11-13 20:12:43 -10:00
browser: call-outs read the guest stack pointer at the architecture's place
The call-out loops read the stack pointer at x86's place in the context (`ctx::x(4)`) on every guest. On aarch64 that is x4, so a watched return never matched the call-out waiting for it: the return ran on as a bypass, the call-out never ended, and each later one opened inside it until the depth limit. Their unwound check compared x4 too, and a call-out whose x4 held a large value left as unwound while its callee still ran. The shim now says where the stack pointer is (`gex_ctx_sp`). tests/callout-entry-return: a call-out returning to an entry of its unit ends at its return each time. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_8b3490cccb4848b495dde1c5cdcacba0
This commit is contained in:
1 parent
bbfe7129e9
commit
b05c1e0105
5 files changed
+123
-8
No files matched your search
@@ -301,8 +301,10 @@ const DENSE_FIRST_TABLE = 4;
|
||||
const TABLE_STEP = 1 << 20;
|
||||
/** `gex_abi::ctx::LAST_TARGET`. */
|
||||
const CTX_LAST_TARGET = 44 * 8;
|
||||
/** `gex_abi::ctx::PC`, and x86's `rsp` (`ctx::x(4)`). */
|
||||
/** `gex_abi::ctx::PC`. */
|
||||
const CTX_PC = 32 * 8;
|
||||
/** x86's `rsp` (`ctx::x(4)`): the stack pointer's place in the context
|
||||
* where the shim does not say (`gex_ctx_sp`); aarch64's is `ctx::x(31)`. */
|
||||
const CTX_SP = 4 * 8;
|
||||
/** cgen.rs `CALLOUT_UNWOUND`: 64 KiB. */
|
||||
const CALLOUT_UNWOUND = 64n << 10n;
|
||||
@@ -6049,7 +6051,7 @@ export class Gex {
|
||||
// native frame down to the innermost loop, live ones included.
|
||||
if (e.gex_nt_native_left?.()) {
|
||||
const c = this.#callouts.at(-1);
|
||||
const sp = new DataView(this.#memory.buffer).getBigUint64(Number(ctx) + CTX_SP, true);
|
||||
const sp = new DataView(this.#memory.buffer).getBigUint64(Number(ctx) + this.#ctxSp(), true);
|
||||
if (c && this.#nest.at(-1)?.startsWith('callout') && c.ret === NATIVE_RETURN_PAD && sp > c.spWant + c.unwound) {
|
||||
err.gexNativeLeft = true;
|
||||
throw err;
|
||||
@@ -7238,7 +7240,7 @@ export class Gex {
|
||||
#watchHit(ctx) {
|
||||
const dv = new DataView(this.#memory.buffer);
|
||||
const target = dv.getBigUint64(ctx + CTX_LAST_TARGET, true);
|
||||
const sp = dv.getBigUint64(ctx + CTX_SP, true);
|
||||
const sp = dv.getBigUint64(ctx + this.#ctxSp(), true);
|
||||
let i = -1;
|
||||
// Only a call-out with no NT callback nested inside it: an outer one's
|
||||
// return reached from inside a callback is not that call-out's (its
|
||||
@@ -7549,7 +7551,7 @@ export class Gex {
|
||||
// frames): a native caller's call-out leaves, at the context's PC
|
||||
// (the entry the guest makes again); anything else carries on.
|
||||
if (err?.gexNativeLeft && c.ret === NATIVE_RETURN_PAD
|
||||
&& new DataView(this.#memory.buffer).getBigUint64(ctx + CTX_SP, true) > c.spWant + c.unwound) { if (globalThis.GEX_CALLOUT_TRACE) console.error(`[callout] depth ${me} leaves: native left`); return 0; }
|
||||
&& new DataView(this.#memory.buffer).getBigUint64(ctx + this.#ctxSp(), true) > c.spWant + c.unwound) { if (globalThis.GEX_CALLOUT_TRACE) console.error(`[callout] depth ${me} leaves: native left`); return 0; }
|
||||
// The process ended: down through every frame to the run loop
|
||||
// (returning would run the guest on).
|
||||
if (e.gex_exited?.()) {
|
||||
@@ -7611,7 +7613,7 @@ export class Gex {
|
||||
if (globalThis.GEX_CALLOUT_TRACE) console.error(`[callout] depth ${me} leaves: resume on ctx ${rc.toString(16)} not ${ctx.toString(16)}`);
|
||||
return 0;
|
||||
}
|
||||
const sp = new DataView(this.#memory.buffer).getBigUint64(ctx + CTX_SP, true);
|
||||
const sp = new DataView(this.#memory.buffer).getBigUint64(ctx + this.#ctxSp(), true);
|
||||
// Native Wine's return pad, reached with nobody's watch or miss
|
||||
// taking it (cgen.rs `native_return_stray`): the guest went on
|
||||
// past this call-out to a native caller's return (an exception
|
||||
@@ -7652,13 +7654,21 @@ export class Gex {
|
||||
e.gex_callout_unwind?.(me);
|
||||
if (globalThis.GEX_CALLOUT_TRACE) {
|
||||
const dv = new DataView(this.#memory.buffer);
|
||||
console.error(`[callout] end depth ${me}: pc ${dv.getBigUint64(ctx + CTX_PC, true).toString(16)} sp ${dv.getBigUint64(ctx + CTX_SP, true).toString(16)} want ${c.spWant.toString(16)}`);
|
||||
console.error(`[callout] end depth ${me}: pc ${dv.getBigUint64(ctx + CTX_PC, true).toString(16)} sp ${dv.getBigUint64(ctx + this.#ctxSp(), true).toString(16)} want ${c.spWant.toString(16)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#calloutsMade = 0;
|
||||
|
||||
/** The guest stack pointer's byte offset in a thread's context, which
|
||||
* differs by guest architecture. */
|
||||
#ctxSpAt = -1;
|
||||
#ctxSp() {
|
||||
if (this.#ctxSpAt < 0) this.#ctxSpAt = this.#shim.exports.gex_ctx_sp?.() ?? CTX_SP;
|
||||
return this.#ctxSpAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* A guest fault the shim staged (`gex_fault_take`: a W3 process's branch
|
||||
* to non-executable memory) given to the process's fault hook, Wine's
|
||||
@@ -7721,11 +7731,11 @@ export class Gex {
|
||||
// its low 32 bits put the hook's frames, and the exception record it
|
||||
// writes, over whatever lives there, the runtime's own heap included.
|
||||
if (typeof cur === 'bigint') {
|
||||
const rsp = this.#dv().getBigUint64(ctx + CTX_SP, true);
|
||||
const rsp = this.#dv().getBigUint64(ctx + this.#ctxSp(), true);
|
||||
const low = cur !== 0n && cur < rsp ? cur : rsp;
|
||||
spG.value = (low - 4096n) & ~15n;
|
||||
} else {
|
||||
const esp = this.#dv().getUint32(ctx + CTX_SP, true);
|
||||
const esp = this.#dv().getUint32(ctx + this.#ctxSp(), true);
|
||||
const c32 = Number(cur) >>> 0;
|
||||
// 0: the program has not set it yet, as for an i64 one.
|
||||
const low = c32 !== 0 && c32 < esp ? c32 : esp;
|
||||
|
||||
@@ -2463,6 +2463,13 @@ fn exp_trap_callouts() -> bool {
|
||||
}
|
||||
pub(crate) static TRAP_CALLOUTS: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||
|
||||
/// The guest stack pointer's byte offset in a thread's context, for JS's
|
||||
/// call-out loops: their checks of where a return comes back read it.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_ctx_sp() -> i32 {
|
||||
gex_abi::ctx::x(sp_index()) as i32
|
||||
}
|
||||
|
||||
pub(crate) fn sp_index() -> u8 {
|
||||
if gex_abi::X86 {
|
||||
4
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/* co_call(cb, arg): cb(arg) + 1, the call's return address the entry of
|
||||
co_after, a function of its own: the call-out's return is an entry of the
|
||||
unit, which dispatch reaches through its bridge. */
|
||||
.text
|
||||
.globl co_call
|
||||
.type co_call, %function
|
||||
.globl co_after
|
||||
.type co_after, %function
|
||||
.p2align 2
|
||||
co_call:
|
||||
stp x29, x30, [sp, #-16]!
|
||||
mov x29, sp
|
||||
mov x2, x0
|
||||
mov x0, x1
|
||||
blr x2
|
||||
co_after:
|
||||
add x0, x0, #1
|
||||
ldp x29, x30, [sp], #16
|
||||
ret
|
||||
.size co_call, co_after-co_call
|
||||
.size co_after, .-co_after
|
||||
.section .note.GNU-stack,"",%progbits
|
||||
@@ -0,0 +1,27 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
long co_call(long (*cb)(long), long arg);
|
||||
/* Large values in the low argument registers, where x86's stack pointer
|
||||
sits in the context. */
|
||||
__attribute__((noinline)) static long cb(long i)
|
||||
{
|
||||
register long x4 __asm__("x4") = 0x7ffffff0000 + i;
|
||||
__asm__ volatile("" : "+r"(x4));
|
||||
return i * 10;
|
||||
}
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
long n = argc > 1 ? atol(argv[1]) : 40;
|
||||
int bad = 0;
|
||||
setvbuf(stdout, NULL, _IOLBF, 0);
|
||||
for (long i = 0; i < n; i++) {
|
||||
long r = co_call(cb, i);
|
||||
if (r != i * 10 + 1) {
|
||||
printf("co_call %ld: %ld\n", i, r);
|
||||
bad = 1;
|
||||
}
|
||||
}
|
||||
printf("%s\n", bad ? "wrong" : "ok");
|
||||
_exit(bad);
|
||||
}
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
# A call-out whose return is an entry of the unit that made it: co_call
|
||||
# (lib.S, a function-tier unit) calls cb, which no unit holds, and cb returns
|
||||
# to co_after's entry, which dispatch reaches through the unit's bridge, so
|
||||
# the call-out sees its return through the watched entry (gex.mjs
|
||||
# `#watchHit`). The watch compares the guest's stack pointer with the one the
|
||||
# call-out waits for, read from the thread's context at the architecture's
|
||||
# place: read from anywhere else, no return was ever the call-out's, each
|
||||
# call-out ran on inside the last and they nested until the depth limit.
|
||||
# Every call-out must end at its own return (depth 0 throughout).
|
||||
#
|
||||
# tests/callout-entry-return/run.sh [gexc]
|
||||
#
|
||||
# Needs ~/toolchains/aarch64-linux-musl-cross (GEX_TOOLCHAIN) and the shared
|
||||
# browser build in web/public/gex (shims/browser/build.sh).
|
||||
set -u
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
root=$(cd "$here/../.." && pwd)
|
||||
gexc=$(realpath "${1:-$root/target/release/gexc}")
|
||||
tc=${GEX_TOOLCHAIN:-$HOME/toolchains/aarch64-linux-musl-cross}
|
||||
cc=$tc/bin/aarch64-linux-musl-gcc
|
||||
seed=$root/web/public/gex/seed.shared.wasm
|
||||
for t in "$gexc" "$cc" "$seed"; do [ -e "$t" ] || { echo "skip: $t is not there"; exit 0; }; done
|
||||
w=${WORK:-$root/target/callout-entry-return-test}
|
||||
rm -rf "$w"; mkdir -p "$w"
|
||||
"$cc" -O2 -fPIC -shared -Wl,-soname,libco.so -o "$w/libco.so" "$here/lib.S" || exit 1
|
||||
"$cc" -O1 -o "$w/main" "$here/main.c" -L"$w" -lco -Wl,-rpath,"$w" || exit 1
|
||||
export GEX_CGEN_CACHE=$w/cgen GEX_ELF_UNITS=libco.so
|
||||
run() { # LOG N
|
||||
(cd "$w" && GEX_SHARED=1 timeout "${TMO:-300}" node "$root/shims/browser/js/run-node.mjs" --cgen-prefer \
|
||||
--file "/lib/ld-musl-aarch64.so.1=$tc/aarch64-linux-musl/lib/libc.so" \
|
||||
--file "$w/libco.so=$w/libco.so" --file "$w/main=$w/main" "$seed" "$w/main" "$2" > "$1.out" 2> "$1.err")
|
||||
}
|
||||
# A first run finds where the loader maps libco.so; its unit is built for that base.
|
||||
run "$w/probe" 2
|
||||
base=$(grep -ao 'libco\.so at 0x[0-9a-f]*' "$w/probe.err" | head -1 | sed 's/.* at //')
|
||||
[ -n "$base" ] || { echo "FAIL callout-entry-return (libco.so never mapped)"; tail -20 "$w/probe.err"; exit 1; }
|
||||
GEX_CGEN_GUARD=1 "$gexc" cgen-image --elf "$w/libco.so" --base "$base" --jobs 2 > "$w/cgen.log" 2>&1 || { cat "$w/cgen.log"; exit 1; }
|
||||
n=40
|
||||
GEX_CALLOUT_TRACE=1 run "$w/run" $n; rc=$?
|
||||
made=$(grep -ac '\[callout\] #' "$w/run.err")
|
||||
deep=$(grep -ao '\[callout\] #[0-9]* depth [0-9]*' "$w/run.err" | awk '$4 > 0' | wc -l)
|
||||
if [ $rc = 0 ] && grep -q '^ok$' "$w/run.out" && [ "$made" -ge $n ] && [ "$deep" = 0 ]; then
|
||||
echo "ok callout-entry-return ($made call-outs, each ended at its return)"
|
||||
else
|
||||
echo "FAIL callout-entry-return (rc=$rc, $made call-outs, $deep made inside another)"; cat "$w/run.out"
|
||||
grep -a '\[callout\]' "$w/run.err" | head -8
|
||||
exit 1
|
||||
fi
|
||||
Reference in new issue
Block a user