Summary
After an OwnedIsolate is dropped, the IsolateAnnex (and its HashMap slots, FinalizerMap, and Mutex) are not freed. The Arc<IsolateAnnex> strong count does not reach 0 after dispose_annex() runs, indicating a leaked IsolateHandle somewhere.
Evidence
Using macOS leaks tool on a Deno process that creates and destroys 100 Web Workers (each with its own V8 isolate):
- 0 workers (baseline): 0 leaks
- 100 workers: 387 leaks, 49,712 bytes (~4 objects / ~500 bytes per isolate)
The leaked objects per isolate are:
- The
IsolateAnnex struct itself (via Arc)
slots: HashMap<TypeId, RawSlot> backing allocation
finalizer_map: FinalizerMap backing allocation
isolate_mutex: Mutex<()> (pthread_mutex_t)
The leak scales linearly with isolate count and persists after aggressive GC and 30+ second waits.
Leak stack (from leaks --groupByType)
STACK OF 99 INSTANCES OF 'ROOT LEAK: malloc in v8::isolate::Isolate::initialize':
thread_start → _pthread_start →
op_create_worker → tokio::block_on → ... →
WebWorker::from_options →
JsRuntime::new → JsRuntime::try_new → JsRuntime::new_inner →
v8::isolate::Isolate::new → v8::isolate::Isolate::initialize →
malloc
Grouped:
387 (48.5K) ROOT LEAK: malloc in v8::isolate::Isolate::initialize
193 (27.1K) malloc in hashbrown::raw::RawTableInner::fallible_with_capacity
95 (5.94K) pthread_mutex_t
Analysis
dispose_annex() correctly calls Arc::from_raw(annex) to decrement the strong count by 1, but at least one IsolateHandle (Arc<IsolateAnnex>) outlives the isolate, preventing the annex from being freed.
In the Deno runtime, IsolateHandle references are held by:
- Raw pointer in isolate data slot → freed by
dispose_annex()
WebWorkerInternalHandle in worker's OpState → freed during JsRuntime cleanup
WebWorkerInternalHandle in WebWorker struct → freed when WebWorker is dropped (after JsRuntime)
InspectorWaker inside the JsRuntimeInspector → freed when inspector is dropped during cleanup
All of these appear to be dropped before or during the isolate teardown sequence. The strong count should reach 0, but leaks confirms the memory is unreachable (no pointers to it anywhere in the process), meaning the Arc was dropped but the count never hit 0.
Reproduction
Any program that repeatedly creates and destroys V8 isolates via OwnedIsolate should exhibit this. In Deno:
const WORKER_SRC = `self.postMessage("ready");`;
const WORKER_URL = URL.createObjectURL(
new Blob([WORKER_SRC], { type: "text/javascript" }),
);
for (let i = 0; i < 100; i++) {
const worker = new Worker(WORKER_URL, { type: "module" });
await new Promise(r => { worker.onmessage = r; });
worker.terminate();
}
Environment
- v8 crate version: 147.0.0
- macOS 15.5, ARM64
- Also reproduced on Linux (aarch64, Docker)
Summary
After an
OwnedIsolateis dropped, theIsolateAnnex(and itsHashMapslots,FinalizerMap, andMutex) are not freed. TheArc<IsolateAnnex>strong count does not reach 0 afterdispose_annex()runs, indicating a leakedIsolateHandlesomewhere.Evidence
Using macOS
leakstool on a Deno process that creates and destroys 100 Web Workers (each with its own V8 isolate):The leaked objects per isolate are:
IsolateAnnexstruct itself (viaArc)slots: HashMap<TypeId, RawSlot>backing allocationfinalizer_map: FinalizerMapbacking allocationisolate_mutex: Mutex<()>(pthread_mutex_t)The leak scales linearly with isolate count and persists after aggressive GC and 30+ second waits.
Leak stack (from
leaks --groupByType)Grouped:
Analysis
dispose_annex()correctly callsArc::from_raw(annex)to decrement the strong count by 1, but at least oneIsolateHandle(Arc<IsolateAnnex>) outlives the isolate, preventing the annex from being freed.In the Deno runtime,
IsolateHandlereferences are held by:dispose_annex()WebWorkerInternalHandlein worker'sOpState→ freed duringJsRuntimecleanupWebWorkerInternalHandleinWebWorkerstruct → freed whenWebWorkeris dropped (afterJsRuntime)InspectorWakerinside theJsRuntimeInspector→ freed when inspector is dropped during cleanupAll of these appear to be dropped before or during the isolate teardown sequence. The strong count should reach 0, but
leaksconfirms the memory is unreachable (no pointers to it anywhere in the process), meaning the Arc was dropped but the count never hit 0.Reproduction
Any program that repeatedly creates and destroys V8 isolates via
OwnedIsolateshould exhibit this. In Deno:Environment