mirror of
https://github.com/HeyPuter/gex.git
synced 2026-11-13 20:12:43 -10:00
browser: a fork's staged child is its Worker's own
A fork staged its child (`FORK`, its descriptors, cwd, program and signal state) in statics every Worker of the process shares, claimed by the thread index that made it. Two threads forking at once on their own Workers replaced each other's words before the first Worker drained them, and the first child was never started (its parent waited for good). A vfork child running in its parent's Worker numbers its thread 0, so its fork could be claimed by the process's first thread's Worker, which copied the memory while the parent ran on: the grandchild started at the instruction after its fork on the parent's later context (a SIGSEGV in its first block). Each Worker now stages its own fork, which only it can claim. tests/fork-staged: detached starts (a vfork child forking and exiting) from two threads at once; every grandchild reports, its registers whole. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
This commit is contained in:
1 parent
52d2b58cca
commit
74b2b1324c
5 files changed
+201
-83
No files matched your search
@@ -3820,7 +3820,7 @@ export class Gex {
|
||||
vforkTrace(`[vfork] not here: ${why}`);
|
||||
if (this.#vforkCopiedSaid) return;
|
||||
this.#vforkCopiedSaid = true;
|
||||
const reason = { [-1]: 'vfork children nest too deep here', [-4]: "inside a provider's call", [-5]: 'a Windows process', [-6]: 'no global kernel', [-7]: 'another thread staged the fork' }[why] ?? `code ${why}`;
|
||||
const reason = { [-1]: 'vfork children nest too deep here', [-4]: "inside a provider's call", [-5]: 'a Windows process', [-6]: 'no global kernel', [-7]: 'no fork staged on this Worker' }[why] ?? `code ${why}`;
|
||||
console.error(`[gex] pid ${this.wlToken()}: a CLONE_VM|CLONE_VFORK child is copied, not run in place (${reason}); said once per Worker`);
|
||||
}
|
||||
|
||||
|
||||
+79
-76
@@ -9028,58 +9028,66 @@ fn do_fork(h: &mut Host, pid: i32, pc: u64, child_stack: u64, tls: Option<u64>,
|
||||
// fresh kernel starts at `/`, so hand it the parent's cwd to apply in
|
||||
// `gex_adopt_child`, or `sh -c 'cd d; make'` builds in the wrong place.
|
||||
// The calling thread's (its fs_struct's, which a fork copies).
|
||||
*FORK_CWD.0.borrow_mut() = h.kernel.caller_cwd();
|
||||
FORK_CWD.with(|c| *c.borrow_mut() = h.kernel.caller_cwd());
|
||||
// And the program, which a fork child is too: `/proc/self/{exe,cmdline}`
|
||||
// (tests/gexcc/longargs re-execs `/proc/self/exe` from a child).
|
||||
// Dispositions (an ignored SIGPIPE, SIGINT), mask, umask and limits,
|
||||
// which a child inherits and keeps across its `execve`.
|
||||
*FORK_STATE.0.borrow_mut() = state_bytes(&h.kernel, false);
|
||||
*FORK_EXE.0.borrow_mut() = std::iter::once(h.kernel.exe_path.as_str())
|
||||
FORK_STATE.with(|c| *c.borrow_mut() = state_bytes(&h.kernel, false));
|
||||
let exe = std::iter::once(h.kernel.exe_path.as_str())
|
||||
.chain(h.kernel.argv.iter().map(String::as_str))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\0");
|
||||
FORK[1].store(start.unwrap_or(pc + gex_abi::SYSCALL_INST_BYTES), std::sync::atomic::Ordering::Relaxed);
|
||||
FORK[2].store(ctx_addr, std::sync::atomic::Ordering::Relaxed);
|
||||
FORK[3].store(child_stack, std::sync::atomic::Ordering::Relaxed);
|
||||
FORK[4].store(tls.unwrap_or(0), std::sync::atomic::Ordering::Relaxed);
|
||||
FORK[5].store(h.kernel.mem.lock().brk_current(), std::sync::atomic::Ordering::Relaxed);
|
||||
// Only the forking thread's Worker may claim it (`gex_fork_pending`).
|
||||
FORK_BY.store(cur() as u64 + 1, std::sync::atomic::Ordering::Relaxed);
|
||||
// Last, because it is what the host tests: the rest must be readable by
|
||||
// the time a non-zero pid appears.
|
||||
FORK[0].store(pid as u64, std::sync::atomic::Ordering::Release);
|
||||
FORK_EXE.with(|c| *c.borrow_mut() = exe);
|
||||
let brk = h.kernel.mem.lock().brk_current();
|
||||
FORK.with(|f| {
|
||||
f.set([
|
||||
pid as u64,
|
||||
start.unwrap_or(pc + gex_abi::SYSCALL_INST_BYTES),
|
||||
ctx_addr,
|
||||
child_stack,
|
||||
tls.unwrap_or(0),
|
||||
brk,
|
||||
])
|
||||
});
|
||||
}
|
||||
|
||||
/// The child's descriptor table, as text, staged for the host to carry.
|
||||
///
|
||||
/// An `Arc<OpenFile>` is a pointer into a heap the child's Worker never saw, so
|
||||
/// the child is sent a *description* of each descriptor and rebuilds it. Only
|
||||
/// what can be rebuilt travels (a shared-root file or a global-kernel pipe, an
|
||||
/// id or path meaning the same in any process); a device node or host socket is
|
||||
/// left closed rather than silently replaced.
|
||||
///
|
||||
/// One line per descriptor:
|
||||
///
|
||||
/// <fd> <cloexec> <flags> <offset> <path>
|
||||
///
|
||||
/// Space-separated, path last, since a guest path may hold anything but newline.
|
||||
static FORK_FDS: SingleThreaded<String> = SingleThreaded(const { RefCell::new(String::new()) });
|
||||
/// The parent's working directory at `fork`, staged for the child to inherit.
|
||||
static FORK_CWD: SingleThreaded<String> = SingleThreaded(const { RefCell::new(String::new()) });
|
||||
/// The parent's exe path then its argv, NUL-separated, staged at `fork`.
|
||||
static FORK_EXE: SingleThreaded<String> = SingleThreaded(const { RefCell::new(String::new()) });
|
||||
/// The parent's signal state, umask and limits at `fork` (`state_bytes`),
|
||||
/// staged for the child to inherit.
|
||||
static FORK_STATE: SingleThreaded<Vec<u8>> = SingleThreaded(const { RefCell::new(Vec::new()) });
|
||||
thread_local! {
|
||||
/// The child's descriptor table, as text, staged for the host to carry.
|
||||
///
|
||||
/// An `Arc<OpenFile>` is a pointer into a heap the child's Worker never saw, so
|
||||
/// the child is sent a *description* of each descriptor and rebuilds it. Only
|
||||
/// what can be rebuilt travels (a shared-root file or a global-kernel pipe, an
|
||||
/// id or path meaning the same in any process); a device node or host socket is
|
||||
/// left closed rather than silently replaced.
|
||||
///
|
||||
/// One line per descriptor:
|
||||
///
|
||||
/// <fd> <cloexec> <flags> <offset> <path>
|
||||
///
|
||||
/// Space-separated, path last, since a guest path may hold anything but newline.
|
||||
///
|
||||
/// These, like `FORK`, are the forking Worker's own: only it starts the
|
||||
/// child, and a fork another thread of the process made meanwhile, on its own
|
||||
/// Worker, stages its child there.
|
||||
pub(crate) static FORK_FDS: RefCell<String> = const { RefCell::new(String::new()) };
|
||||
/// The parent's working directory at `fork`, staged for the child to inherit.
|
||||
pub(crate) static FORK_CWD: RefCell<String> = const { RefCell::new(String::new()) };
|
||||
/// The parent's exe path then its argv, NUL-separated, staged at `fork`.
|
||||
pub(crate) static FORK_EXE: RefCell<String> = const { RefCell::new(String::new()) };
|
||||
/// The parent's signal state, umask and limits at `fork` (`state_bytes`),
|
||||
/// staged for the child to inherit.
|
||||
pub(crate) static FORK_STATE: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_fork_state_ptr() -> u32 {
|
||||
FORK_STATE.0.borrow().as_ptr() as u32
|
||||
FORK_STATE.with(|c| c.borrow().as_ptr() as u32)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_fork_state_len() -> u32 {
|
||||
FORK_STATE.0.borrow().len() as u32
|
||||
FORK_STATE.with(|c| c.borrow().len() as u32)
|
||||
}
|
||||
|
||||
/// The child's side of `gex_fork_state_*`, before `gex_adopt_child`.
|
||||
@@ -9131,12 +9139,12 @@ fn apply_state(k: &mut Kernel, b: &[u8]) {
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_fork_exe_ptr() -> u32 {
|
||||
FORK_EXE.0.borrow().as_ptr() as u32
|
||||
FORK_EXE.with(|c| c.borrow().as_ptr() as u32)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_fork_exe_len() -> u32 {
|
||||
FORK_EXE.0.borrow().len() as u32
|
||||
FORK_EXE.with(|c| c.borrow().len() as u32)
|
||||
}
|
||||
|
||||
/// The child's side of `gex_fork_exe_*`, before `gex_adopt_child`.
|
||||
@@ -9181,7 +9189,7 @@ pub(crate) fn stage_fork_fds(h: &Host, child: i32, maps: bool) {
|
||||
out.push_str(&format!("m {} {}\n", gex_kernel::syscalls::SharedMaps::carry(s), name));
|
||||
}
|
||||
}
|
||||
*FORK_FDS.0.borrow_mut() = out;
|
||||
FORK_FDS.with(|c| *c.borrow_mut() = out);
|
||||
}
|
||||
|
||||
/// The name a descriptor travels to another process by -- a fork's child, or
|
||||
@@ -9287,23 +9295,23 @@ fn pty_ref(path: &str) -> Option<(i32, bool)> {
|
||||
/// Where the child's descriptor table is staged, and how long it is.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_fork_fds_ptr() -> u32 {
|
||||
FORK_FDS.0.borrow().as_ptr() as u32
|
||||
FORK_FDS.with(|c| c.borrow().as_ptr() as u32)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_fork_fds_len() -> u32 {
|
||||
FORK_FDS.0.borrow().len() as u32
|
||||
FORK_FDS.with(|c| c.borrow().len() as u32)
|
||||
}
|
||||
|
||||
/// The parent's working directory, staged at `fork` for the child to adopt.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_fork_cwd_ptr() -> u32 {
|
||||
FORK_CWD.0.borrow().as_ptr() as u32
|
||||
FORK_CWD.with(|c| c.borrow().as_ptr() as u32)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_fork_cwd_len() -> u32 {
|
||||
FORK_CWD.0.borrow().len() as u32
|
||||
FORK_CWD.with(|c| c.borrow().len() as u32)
|
||||
}
|
||||
|
||||
/// Rebuild the descriptors the parent had, in the child.
|
||||
@@ -9649,45 +9657,40 @@ pub extern "C" fn gex_adopt_windows_space(lo: u32) -> i32 {
|
||||
}
|
||||
}
|
||||
|
||||
/// A child `fork` has made that the host has not built a Worker for: pid, entry
|
||||
/// pc, context address, child stack, TLS. Plain words, not a queue: the host
|
||||
/// drains this before the parent runs again, so there is only ever one.
|
||||
static FORK: [std::sync::atomic::AtomicU64; 6] =
|
||||
[const { std::sync::atomic::AtomicU64::new(0) }; 6];
|
||||
thread_local! {
|
||||
/// A child `fork` has made that the host has not built a Worker for: pid, entry
|
||||
/// pc, context address, child stack, TLS, break. Plain words, not a queue: the
|
||||
/// host drains this before the parent runs again, so there is only ever one.
|
||||
/// The forking Worker's own (as are `FORK_FDS` and the rest): only it starts
|
||||
/// the child, and a fork another thread of the process makes meanwhile is
|
||||
/// staged on that thread's Worker. Shared, the second fork's words replaced
|
||||
/// the first's before its Worker drained them; and a vfork child running in
|
||||
/// its parent's Worker (its thread numbered 0) staged its fork where the
|
||||
/// process's first thread's Worker claimed it, and copied the memory while
|
||||
/// the parent ran on, the grandchild starting on its parent's later context.
|
||||
static FORK: std::cell::Cell<[u64; 6]> = const { std::cell::Cell::new([0; 6]) };
|
||||
}
|
||||
|
||||
/// The thread (index + 1) whose `fork` is in `FORK`.
|
||||
static FORK_BY: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
/// Whether this Worker has a fork staged (`FORK`).
|
||||
pub(crate) fn fork_staged() -> bool {
|
||||
FORK.with(|f| f.get()[0]) != 0
|
||||
}
|
||||
|
||||
/// The pid of a child needing a Worker, or zero -- and claiming it, so a `fork`
|
||||
/// several Workers can see is not started twice (see `gex_thread_pending`).
|
||||
/// The pid of a child this Worker's thread forked that needs a Worker, or
|
||||
/// zero -- and claiming it, so it is not started twice.
|
||||
///
|
||||
/// Only on the Worker running the thread that forked: the child is a copy of
|
||||
/// memory as `fork` left it, and only that Worker copies it before the parent
|
||||
/// runs on (synchronously after a wasm32 program's syscall, before the run
|
||||
/// loop resumes tier-1's). A sibling thread's Worker that drained it instead
|
||||
/// copied memory while the parent ran on -- for a wasm32 program, after
|
||||
/// `__gex_fork` had unmapped the record the child starts on, so the child
|
||||
/// read its thread pointer from a zero page and trapped (the aarch64 browser
|
||||
/// flake in tests/wasm32 fork: "vfork child exit 137" with a parked thread
|
||||
/// in the parent, 6 in 750 vforks; tests/wasm32/vforkthreads).
|
||||
/// The child is a copy of memory as `fork` left it, and only the forking
|
||||
/// Worker copies it before the parent runs on (synchronously after a wasm32
|
||||
/// program's syscall, before the run loop resumes tier-1's). A sibling
|
||||
/// thread's Worker that drained it instead copied memory while the parent ran
|
||||
/// on (tests/wasm32/vforkthreads).
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_fork_pending() -> i32 {
|
||||
if FORK[0].load(std::sync::atomic::Ordering::Acquire) == 0
|
||||
|| FORK_BY.load(std::sync::atomic::Ordering::Relaxed) != cur() as u64 + 1
|
||||
{
|
||||
return 0;
|
||||
let f = FORK.with(|f| f.replace([0; 6]));
|
||||
if f[0] != 0 {
|
||||
CLAIMED_FORK.with(|c| c.set([f[1], f[2], f[3], f[4], f[5]]));
|
||||
}
|
||||
let pid = FORK[0].swap(0, std::sync::atomic::Ordering::AcqRel) as i32;
|
||||
if pid != 0 {
|
||||
CLAIMED_FORK.with(|c| {
|
||||
let mut v = [0u64; 5];
|
||||
for (i, slot) in v.iter_mut().enumerate() {
|
||||
*slot = FORK[i + 1].load(std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
c.set(v);
|
||||
});
|
||||
}
|
||||
pid
|
||||
f[0] as i32
|
||||
}
|
||||
|
||||
/// The four words that go with a claimed pid, plus the break.
|
||||
|
||||
@@ -194,7 +194,7 @@ pub extern "C" fn gex_vfork_ok() -> i32 {
|
||||
-5
|
||||
} else if unsafe { k_present() } == 0 {
|
||||
-6
|
||||
} else if FORK[0].load(Acquire) == 0 || FORK_BY.load(Relaxed) != cur() as u64 + 1 {
|
||||
} else if !crate::fork_staged() {
|
||||
-7
|
||||
} else {
|
||||
1
|
||||
@@ -227,13 +227,13 @@ pub extern "C" fn gex_vfork_begin() -> i64 {
|
||||
let mem = crate::sigs::proc_home().mm().clone();
|
||||
// What `adoptProcess` stages before `gex_adopt_child`, from what
|
||||
// `do_fork` staged.
|
||||
*CWD.0.borrow_mut() = FORK_CWD.0.borrow().clone();
|
||||
*CHILD_EXE.0.borrow_mut() = FORK_EXE.0.borrow().clone();
|
||||
*CHILD_STATE.0.borrow_mut() = FORK_STATE.0.borrow().clone();
|
||||
*CWD.0.borrow_mut() = FORK_CWD.with(|c| c.borrow().clone());
|
||||
*CHILD_EXE.0.borrow_mut() = FORK_EXE.with(|c| c.borrow().clone());
|
||||
*CHILD_STATE.0.borrow_mut() = FORK_STATE.with(|c| c.borrow().clone());
|
||||
// Not the shared mappings `do_fork` described with them (`m` lines):
|
||||
// the memory is the parent's, and its map has them already. Adopted, each
|
||||
// was there twice, the second writable, written back at the child's exec.
|
||||
let fds: String = FORK_FDS.0.borrow().lines().filter(|l| !l.starts_with("m ")).flat_map(|l| [l, "\n"]).collect();
|
||||
let fds: String = FORK_FDS.with(|c| c.borrow().lines().filter(|l| !l.starts_with("m ")).flat_map(|l| [l, "\n"]).collect());
|
||||
// A vfork child spawning: it waits, kept aside, as its parent does.
|
||||
if active() {
|
||||
let f = Frame {
|
||||
@@ -360,7 +360,7 @@ pub(crate) fn stage_exec_parts(h: &mut Host, path: &str, argv: &[String], envp:
|
||||
cmd.push(0);
|
||||
}
|
||||
stage_fork_fds(h, pid, false);
|
||||
let fds = std::mem::take(&mut *FORK_FDS.0.borrow_mut()).into_bytes();
|
||||
let fds = FORK_FDS.with(|c| std::mem::take(&mut *c.borrow_mut())).into_bytes();
|
||||
let cwd = h.kernel.caller_cwd().into_bytes();
|
||||
let state = state_bytes(&h.kernel, true);
|
||||
let mem64 = if page_flavours() & 2 != 0 { exec_target(h, path) as i32 } else { -1 };
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/* Detached starts from two threads at once, as Qt's startDetached makes
|
||||
them: a vfork child (in its parent's memory) forks the grandchild and
|
||||
exits, and the parent goes on to a read that waits, while the other thread
|
||||
forks too. Each grandchild checks what it holds across its fork. */
|
||||
#define _GNU_SOURCE
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
static inline long sys3(long nr, long a, long b, long c)
|
||||
{
|
||||
register long x8 __asm__("x8") = nr;
|
||||
register long x0 __asm__("x0") = a, x1 __asm__("x1") = b, x2 __asm__("x2") = c;
|
||||
__asm__ volatile("svc #0" : "+r"(x0) : "r"(x8), "r"(x1), "r"(x2) : "memory");
|
||||
return x0;
|
||||
}
|
||||
static inline long sys_clone(long flags)
|
||||
{
|
||||
register long x8 __asm__("x8") = 220;
|
||||
register long x0 __asm__("x0") = flags;
|
||||
register long x1 __asm__("x1") = 0, x2 __asm__("x2") = 0, x3 __asm__("x3") = 0, x4 __asm__("x4") = 0;
|
||||
__asm__ volatile("svc #0" : "+r"(x0) : "r"(x8), "r"(x1), "r"(x2), "r"(x3), "r"(x4) : "memory");
|
||||
return x0;
|
||||
}
|
||||
__attribute__((noinline)) static long mix(long a, long b) { return a * 31 + (b ^ (a >> 3)); }
|
||||
static int out[2];
|
||||
__attribute__((noinline)) static void grandchild(long a, long b, long c)
|
||||
{
|
||||
char r = c == mix(a, b) ? 'Y' : 'N';
|
||||
sys3(64, out[1], (long)&r, 1);
|
||||
sys3(94, 0, 0, 0);
|
||||
}
|
||||
__attribute__((noinline)) static void detached(int rep, int wake[2])
|
||||
{
|
||||
long a = 0x5150 + rep, b = 0x7a7a, c = mix(a, b);
|
||||
long p = sys_clone(0x100 | 0x4000 | 17); /* CLONE_VM | CLONE_VFORK | SIGCHLD */
|
||||
if (p == 0) {
|
||||
long q = sys_clone(17);
|
||||
if (q == 0) grandchild(a, b, c);
|
||||
sys3(94, q < 0 ? 3 : 0, 0, 0);
|
||||
}
|
||||
int st;
|
||||
waitpid(p, &st, 0);
|
||||
/* Back to its loop: a read that waits. */
|
||||
char buf[512];
|
||||
read(wake[0], buf, sizeof buf);
|
||||
}
|
||||
static int n;
|
||||
static void *thr(void *arg)
|
||||
{
|
||||
int wake[2];
|
||||
if (pipe(wake)) return 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
pid_t w = fork();
|
||||
if (w == 0) { usleep(20000); write(wake[1], "w", 1); _exit(0); }
|
||||
detached(i, wake);
|
||||
waitpid(w, 0, 0);
|
||||
}
|
||||
return arg;
|
||||
}
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
n = argc > 1 ? atoi(argv[1]) : 10;
|
||||
int threads = argc > 2 ? atoi(argv[2]) : 1;
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
if (pipe(out)) return 1;
|
||||
if (threads) {
|
||||
pthread_t t[2];
|
||||
for (int k = 0; k < 2; k++) pthread_create(&t[k], NULL, thr, NULL);
|
||||
for (int k = 0; k < 2; k++) pthread_join(t[k], NULL);
|
||||
} else thr(NULL);
|
||||
close(out[1]);
|
||||
int y = 0, no = 0;
|
||||
char c;
|
||||
usleep(300000);
|
||||
while (read(out[0], &c, 1) == 1) { if (c == 'Y') y++; else no++; }
|
||||
printf("Y=%d N=%d of %d\n", y, no, n * (threads ? 2 : 1));
|
||||
_exit(0);
|
||||
}
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
# A fork's staged child belongs to the Worker that made it. Two threads, each
|
||||
# on its own Worker, fork at once, and each makes detached starts (a vfork
|
||||
# child that forks and exits) between: a fork staged where another Worker
|
||||
# could claim it, or overwrite it, lost a child (its parent waited for good)
|
||||
# or started one on another thread's context (main.c). Every grandchild must
|
||||
# report, and say its registers survived.
|
||||
#
|
||||
# tests/fork-staged/run.sh
|
||||
#
|
||||
# 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)
|
||||
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 "$cc" "$seed"; do [ -e "$t" ] || { echo "skip: $t is not there"; exit 0; }; done
|
||||
w=${WORK:-$root/target/fork-staged-test}
|
||||
rm -rf "$w"; mkdir -p "$w"
|
||||
"$cc" -O1 -o "$w/main" "$here/main.c" -lpthread || exit 1
|
||||
n=20 runs=${RUNS:-4} fail=0
|
||||
for i in $(seq 1 "$runs"); do
|
||||
(cd "$w" && GEX_SHARED=1 timeout "${TMO:-150}" node "$root/shims/browser/js/run-node.mjs" \
|
||||
--file "/lib/ld-musl-aarch64.so.1=$tc/aarch64-linux-musl/lib/libc.so" --file "$w/main=$w/main" \
|
||||
"$seed" "$w/main" $n 1 > "$w/run$i.out" 2> "$w/run$i.err" < /dev/null); rc=$?
|
||||
if [ $rc != 0 ] || ! grep -q "^Y=$((n * 2)) N=0 of $((n * 2))$" "$w/run$i.out"; then
|
||||
echo "FAIL fork-staged run $i (rc=$rc): $(cat "$w/run$i.out")"; grep -a 'killed by\|not run in place' "$w/run$i.err" | head -3
|
||||
fail=1
|
||||
fi
|
||||
done
|
||||
[ $fail = 0 ] && echo "ok fork-staged ($runs runs, $((n * 2)) detached starts each)"
|
||||
exit $fail
|
||||
Reference in new issue
Block a user