From 342fefa62359e3714165018d450fab972a1b2ab3 Mon Sep 17 00:00:00 2001 From: Ivan FB Date: Thu, 18 Jun 2026 20:11:39 +0200 Subject: [PATCH] fix(context): pin myLib as a GC root under refc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit myLib lives in non-GC `createShared` memory, so under --mm:refc a GC-managed lib object stored there is invisible to the cycle collector and gets reclaimed mid-operation under sustained request load — a use-after-free that crashes deep in the lib (e.g. a held chronos AsyncLock). Take a GC_ref once a handler installs myLib (tracked by FFIContext.myLibRefd) and GC_unref in freeLib so a later recycle/create can re-pin. Guarded to refc + ref types; orc tracks it precisely. Also wrap freeLib's `=destroy` in try/except: it is conservatively typed as raising, and recycleContext (its async caller) is `raises: []`, so the library would not compile under orc/arc without this. Co-Authored-By: Claude Opus 4.8 --- ffi/ffi_context.nim | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/ffi/ffi_context.nim b/ffi/ffi_context.nim index 61d806c..e0cf619 100644 --- a/ffi/ffi_context.nim +++ b/ffi/ffi_context.nim @@ -38,6 +38,11 @@ type CtxLifecycle {.pure.} = enum type FFIContext*[T] = object myLib*: ptr T # main library object (e.g., Waku, LibP2P, SDS, the one to be exposed as a library) + myLibRefd: bool + # refc only: whether we hold a GC_ref on myLib[]. myLib lives in non-GC + # shared memory, so a GC-managed lib object stored there is invisible to + # refc's cycle collector and gets reclaimed mid-use under sustained load. + # Pinned in processRequest, released in freeLib. Unused under orc/arc. ffiThread: Thread[(ptr FFIContext[T])] # represents the main FFI thread in charge of attending API consumer actions watchdogThread: Thread[(ptr FFIContext[T])] @@ -243,6 +248,15 @@ proc processRequest[T]( "Async error in processRequest for " & reqId & ": " & exc.msg ) + # Pin the lib object as a GC root once a handler has installed it. Under refc + # myLib lives in non-GC shared memory, invisible to the cycle collector, so it + # gets reclaimed mid-operation under sustained load. orc tracks it precisely. + when defined(gcRefc): + when T is ref: + if not ctx.myLibRefd and not ctx.myLib.isNil(): + GC_ref(ctx.myLib[]) + ctx.myLibRefd = true + ## handleRes may raise (OOM, GC setup) even though it is rare. try: handleRes(res, request) @@ -254,10 +268,16 @@ proc freeLib[T](ctx: ptr FFIContext[T]) {.gcsafe.} = return when not defined(gcRefc): - {.cast(gcsafe).}: - `=destroy`(ctx.myLib[]) + try: + {.cast(gcsafe).}: + `=destroy`(ctx.myLib[]) + except Exception: + discard else: - discard + when T is ref: + if ctx.myLibRefd: + GC_unref(ctx.myLib[]) + ctx.myLibRefd = false freeShared(ctx.myLib) ctx.myLib = nil