@@ -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;
10811096const DLOPEN_HEAD_OFFSET_WASM64 = 24 ;
10821097const DLOPEN_ACTIVE_SIDE_FORK_OFFSET_WASM32 = 16 ;
10831098const 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.
10891106const DLOPEN_LOCK_OFFSET_WASM32 = 20 ;
10901107const 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