Skip to content

Commit ba4fb0d

Browse files
committed
host: preserve concurrent pthread forks during dlopen arbitration
The #839 side-module repair gave each pthread fork exclusive ownership of the process dlopen lock. In the existing two-thread, 16-round concurrent-fork fixture, one thread therefore received ENOTSUP and left its peer blocked at the next barrier. Encode the unchanged host-private slot as an exclusive negative dlopen writer or a positive pthread-fork reader count. Concurrent empty-archive pthread forks can now snapshot in parallel, while dlopen remains excluded until every parent completes unwind, SYS_FORK, and rewind. Keep child reset, archive rejection, error releases, and ownership diagnostics explicit. Validation: the exact pre-#839 base passed the concurrent fixture while the prior aggregate timed out. On this repair, fork-from-thread plus dlopen/fork replay passed 5/5. Full host Vitest reached 1,251 passes, 2 expected failures, and 95 skips; its only two failures are the lazy-rootfs memory64/posix-utils fixtures reproduced on the exact base. Host declarations and the ABI consistency check passed. Browser behavior was not rerun for this host-only arbitration follow-up, and performance was not measured.
1 parent e6d080f commit ba4fb0d

1 file changed

Lines changed: 82 additions & 16 deletions

File tree

host/src/worker-main.ts

Lines changed: 82 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -305,10 +305,15 @@ function buildDlopenImports(
305305
mainDlopenDepth++;
306306
return true;
307307
}
308-
const owner = Atomics.compareExchange(archiveLock, 0, 0, 1);
308+
const owner = Atomics.compareExchange(
309+
archiveLock,
310+
0,
311+
DLOPEN_LOCK_IDLE,
312+
DLOPEN_LOCK_WRITER,
313+
);
309314
if (owner !== 0) {
310-
hostDlopenError = owner === 2
311-
? "dlopen is temporarily unavailable while a pthread is forking"
315+
hostDlopenError = owner > 0
316+
? "dlopen is temporarily unavailable while pthreads are forking"
312317
: "dlopen is temporarily unavailable while another dlopen operation owns the process lock";
313318
return false;
314319
}
@@ -321,7 +326,17 @@ function buildDlopenImports(
321326
}
322327
mainDlopenDepth--;
323328
if (mainDlopenDepth === 0) {
324-
Atomics.store(archiveLock, 0, 0);
329+
const owner = Atomics.compareExchange(
330+
archiveLock,
331+
0,
332+
DLOPEN_LOCK_WRITER,
333+
DLOPEN_LOCK_IDLE,
334+
);
335+
if (owner !== DLOPEN_LOCK_WRITER) {
336+
throw new Error(
337+
`dlopen process lock lost writer ownership (state=${owner})`,
338+
);
339+
}
325340
Atomics.notify(archiveLock, 0);
326341
}
327342
};
@@ -1081,13 +1096,18 @@ const DLOPEN_HEAD_OFFSET_WASM32 = 12;
10811096
const DLOPEN_HEAD_OFFSET_WASM64 = 24;
10821097
const DLOPEN_ACTIVE_SIDE_FORK_OFFSET_WASM32 = 16;
10831098
const DLOPEN_ACTIVE_SIDE_FORK_OFFSET_WASM64 = 32;
1084-
// Atomic host-private arbitration between process-main dlopen and pthread
1085-
// fork. The pthread holds value 2 from its pre-unwind archive check through
1086-
// memory-copy/SYS_FORK and parent rewind; process dlopen holds value 1 until
1087-
// the complete archive entry is published. A fork child clears its copied
1088-
// value before replay because its memory is already independent.
1099+
// Atomic host-private reader/writer arbitration between process-main dlopen
1100+
// and pthread fork. A negative value is the exclusive main-worker dlopen
1101+
// writer; a positive value counts concurrent pthread forks from their
1102+
// pre-unwind archive check through memory-copy/SYS_FORK and parent rewind.
1103+
// This preserves Kandelo's existing concurrent-pthread-fork behavior while
1104+
// preventing a new archive entry from racing any fork snapshot. A fork child
1105+
// clears its copied value before replay because its memory is independent.
10891106
const DLOPEN_LOCK_OFFSET_WASM32 = 20;
10901107
const DLOPEN_LOCK_OFFSET_WASM64 = 40;
1108+
const DLOPEN_LOCK_IDLE = 0;
1109+
const DLOPEN_LOCK_WRITER = -1;
1110+
const DLOPEN_LOCK_MAX_READERS = 0x7fff_ffff;
10911111
// Each entry also carries the side module's instance-local TLS base. Fork
10921112
// copies the TLS bytes in memory, but a new replay instance's mutable global
10931113
// must be restored explicitly. Zero is the explicit no-TLS sentinel; TLS
@@ -2168,11 +2188,50 @@ export async function centralizedThreadWorkerMain(
21682188
let threadInstance: WebAssembly.Instance | undefined;
21692189
let processDlopenLock: Int32Array | undefined;
21702190
let pthreadForkLockHeld = false;
2191+
const acquirePthreadForkLock = (): boolean => {
2192+
if (!processDlopenLock) {
2193+
throw new Error(`pid=${pid} tid=${tid}: missing process dlopen lock`);
2194+
}
2195+
if (pthreadForkLockHeld) {
2196+
throw new Error(`pid=${pid} tid=${tid}: pthread fork lock already held`);
2197+
}
2198+
for (;;) {
2199+
const owner = Atomics.load(processDlopenLock, 0);
2200+
if (owner < DLOPEN_LOCK_IDLE) return false;
2201+
if (owner >= DLOPEN_LOCK_MAX_READERS) {
2202+
throw new Error(
2203+
`pid=${pid} tid=${tid}: process dlopen lock reader overflow`,
2204+
);
2205+
}
2206+
if (
2207+
Atomics.compareExchange(processDlopenLock, 0, owner, owner + 1)
2208+
=== owner
2209+
) {
2210+
pthreadForkLockHeld = true;
2211+
return true;
2212+
}
2213+
}
2214+
};
21712215
const releasePthreadForkLock = (): void => {
21722216
if (!pthreadForkLockHeld || !processDlopenLock) return;
2173-
Atomics.store(processDlopenLock, 0, 0);
2174-
Atomics.notify(processDlopenLock, 0);
2175-
pthreadForkLockHeld = false;
2217+
for (;;) {
2218+
const owner = Atomics.load(processDlopenLock, 0);
2219+
if (owner <= DLOPEN_LOCK_IDLE) {
2220+
pthreadForkLockHeld = false;
2221+
throw new Error(
2222+
`pid=${pid} tid=${tid}: pthread fork lost reader ownership ` +
2223+
`(state=${owner})`,
2224+
);
2225+
}
2226+
if (
2227+
Atomics.compareExchange(processDlopenLock, 0, owner, owner - 1)
2228+
=== owner
2229+
) {
2230+
pthreadForkLockHeld = false;
2231+
if (owner === 1) Atomics.notify(processDlopenLock, 0);
2232+
return;
2233+
}
2234+
}
21762235
};
21772236

21782237
try {
@@ -2240,8 +2299,11 @@ export async function centralizedThreadWorkerMain(
22402299
const getState = threadInstance.exports.wpk_fork_state as () => number;
22412300
const state = getState();
22422301
if (state === 2) {
2243-
(threadInstance.exports.wpk_fork_rewind_end as () => void)();
2244-
releasePthreadForkLock();
2302+
try {
2303+
(threadInstance.exports.wpk_fork_rewind_end as () => void)();
2304+
} finally {
2305+
releasePthreadForkLock();
2306+
}
22452307
return forkResult;
22462308
}
22472309

@@ -2250,10 +2312,9 @@ export async function centralizedThreadWorkerMain(
22502312
// instance, so fork must fail before unwind once the process has ever
22512313
// loaded a side module. The head is read live from shared memory so a
22522314
// dlopen after pthread creation is still observed.
2253-
if (Atomics.compareExchange(processDlopenLock!, 0, 0, 2) !== 0) {
2315+
if (!acquirePthreadForkLock()) {
22542316
return -95; // ENOTSUP: process-main dlopen is active
22552317
}
2256-
pthreadForkLockHeld = true;
22572318
if (processHasDlopenArchive()) {
22582319
releasePthreadForkLock();
22592320
return -95; // ENOTSUP: pthreads cannot replay process side modules
@@ -2400,6 +2461,11 @@ export async function centralizedThreadWorkerMain(
24002461
}
24012462
}
24022463

2464+
// A well-formed fork releases its reader token from the state=2 import
2465+
// above. Keep normal-return cleanup defensive so an unexpected
2466+
// instrumenter state cannot strand the process-wide writer lock.
2467+
releasePthreadForkLock();
2468+
24032469
// A normal return has not passed through libc's noreturn kernel_exit
24042470
// import, so publish SYS_EXIT here. When kernel_exit already ran it sent
24052471
// and completed SYS_EXIT before the compiler's trailing unreachable was

0 commit comments

Comments
 (0)