From dd9882bdceb31557fe57c152b8fa3407493e7542 Mon Sep 17 00:00:00 2001 From: Osei Fortune Date: Fri, 6 Nov 2026 03:37:54 -0400 Subject: [PATCH] fix(napi): don't panic when the instance cache drops at process exit With wrappers still cached, process.exit() ran the instance cache's thread-local destructor after REF_GRAVEYARD's, and the entry's Drop panicked on the destroyed thread-local ("cannot access a Thread Local Storage value during or after destruction"). The entry drop and evict_instance now skip a thread-local that is already gone; the deferred napi_ref has nothing left to clean up at that point. Repro: CryptographicBuffer.DecodeFromHexString('0a0b'); process.exit(0) --- runtime/src/napi_engine/ns_proxy.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/runtime/src/napi_engine/ns_proxy.rs b/runtime/src/napi_engine/ns_proxy.rs index bffeb2e..c33b60d 100644 --- a/runtime/src/napi_engine/ns_proxy.rs +++ b/runtime/src/napi_engine/ns_proxy.rs @@ -219,7 +219,8 @@ impl Drop for InstanceCacheEntry { fn drop(&mut self) { // Entries are dropped from InstanceState::Drop, which runs inside a GC finalizer. Deleting // the napi_ref here would call into napi mid-GC (fatal on the V8 shim), so defer it. - REF_GRAVEYARD.with(|g| g.borrow_mut().push((self.env, self.proxy_ref))); + // During thread teardown the graveyard may already be gone; the reference goes with it. + let _ = REF_GRAVEYARD.try_with(|g| g.borrow_mut().push((self.env, self.proxy_ref))); } } @@ -291,7 +292,7 @@ fn cache_instance( /// (`o.SetNamedValue is not a function`). The serial guard prevents evicting a newer proxy that /// already re-cached the same address. pub(crate) fn evict_instance(identity: usize, serial: u64) { - NAPI_INSTANCE_CACHE.with(|c| { + let _ = NAPI_INSTANCE_CACHE.try_with(|c| { let mut cache = c.borrow_mut(); if cache.get(&identity).map(|e| e.serial) == Some(serial) { cache.remove(&identity);