fix(pool): deinit context resources in full teardown

destroyFFIContext stopped/joined the worker threads and marked the slot for
rebuild, but no longer deinited the context — so the six ThreadSignalPtr fds
were orphaned every full teardown (the exact leak this path exists to prevent),
and the still-initialised Lock + event registry/queue locks were left live.

createFFIContext's rebuild path (initialized == false) reruns
initContextResources, which calls initLock / initEventRegistry / initEventQueue
and installs fresh signals over the stale handles — re-initialising a live lock
is UB. Restore the deinitContextResources() call (as the pre-recycle code did)
before marking the slot uninitialised so the rebuild starts from clean state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ivan FB 2026-06-12 17:30:08 +02:00
parent 6bc626946e
commit 79e5dc64c6
No known key found for this signature in database
GPG Key ID: DF0C67A04C543270

View File

@ -50,11 +50,19 @@ proc destroyFFIContext*[T](
## for process/pool shutdown — normal destruction uses releaseFFIContext.
ctx.stopAndJoinThreads().isOkOr:
return err("destroyFFIContext(pool): " & $error)
# Close the ThreadSignalPtr fds and deinit the lock + event registry/queue
# BEFORE the slot is marked for rebuild. createFFIContext's rebuild path reruns
# initContextResources (initLock / initEventRegistry / initEventQueue + fresh
# signals); skipping deinit here would re-init still-live locks (UB) and orphan
# the old fds — the very leak this teardown exists to prevent.
let deinitRes = ctx.deinitContextResources()
for i in 0 ..< MaxFFIContexts:
if pool.contexts[i].addr == ctx:
pool.initialized[i].store(false)
break
ctx.release()
deinitRes.isOkOr:
return err("destroyFFIContext(pool): " & $error)
return ok()
proc isValidCtx*[T](pool: var FFIContextPool[T], ctx: pointer): bool =