@@ -142,39 +142,58 @@ async function executeCode(request) {
142142 stdoutTruncated = true
143143 }
144144
145+ // Hoist all ivm handle declarations so finally can release them deterministically.
146+ // Per isolated-vm upstream issues #198 and #377: child handles (scripts, callbacks,
147+ // references, external copies) must be released before isolate.dispose() to avoid
148+ // stuck-GC states and native memory leaks outside the V8 heap.
149+ let context = null
150+ let bootstrapScript = null
151+ let userScript = null
152+ let logCallback = null
153+ let errorCallback = null
154+ let fetchCallback = null
155+ const externalCopies = [ ]
156+
145157 try {
146158 isolate = new ivm . Isolate ( { memoryLimit : 128 } )
147- const context = await isolate . createContext ( )
159+ context = await isolate . createContext ( )
148160 const jail = context . global
149161
150162 await jail . set ( 'global' , jail . derefInto ( ) )
151163
152- const logCallback = new ivm . Callback ( ( ...args ) => {
164+ logCallback = new ivm . Callback ( ( ...args ) => {
153165 const message = args . map ( ( arg ) => stringifyLogValue ( arg ) ) . join ( ' ' )
154166 appendStdout ( `${ message } \n` )
155167 } )
156168 await jail . set ( '__log' , logCallback )
157169
158- const errorCallback = new ivm . Callback ( ( ...args ) => {
170+ errorCallback = new ivm . Callback ( ( ...args ) => {
159171 const message = args . map ( ( arg ) => stringifyLogValue ( arg ) ) . join ( ' ' )
160172 appendStdout ( `ERROR: ${ message } \n` )
161173 } )
162174 await jail . set ( '__error' , errorCallback )
163175
164- await jail . set ( 'params' , new ivm . ExternalCopy ( params ) . copyInto ( ) )
165- await jail . set ( 'environmentVariables' , new ivm . ExternalCopy ( envVars ) . copyInto ( ) )
176+ const paramsCopy = new ivm . ExternalCopy ( params )
177+ externalCopies . push ( paramsCopy )
178+ await jail . set ( 'params' , paramsCopy . copyInto ( ) )
179+
180+ const envVarsCopy = new ivm . ExternalCopy ( envVars )
181+ externalCopies . push ( envVarsCopy )
182+ await jail . set ( 'environmentVariables' , envVarsCopy . copyInto ( ) )
166183
167184 for ( const [ key , value ] of Object . entries ( contextVariables ) ) {
168185 if ( value === undefined ) {
169186 await jail . set ( key , undefined )
170187 } else if ( value === null ) {
171188 await jail . set ( key , null )
172189 } else {
173- await jail . set ( key , new ivm . ExternalCopy ( value ) . copyInto ( ) )
190+ const ctxCopy = new ivm . ExternalCopy ( value )
191+ externalCopies . push ( ctxCopy )
192+ await jail . set ( key , ctxCopy . copyInto ( ) )
174193 }
175194 }
176195
177- const fetchCallback = new ivm . Reference ( async ( url , optionsJson ) => {
196+ fetchCallback = new ivm . Reference ( async ( url , optionsJson ) => {
178197 return new Promise ( ( resolve ) => {
179198 const fetchId = ++ fetchIdCounter
180199 const timeout = setTimeout ( ( ) => {
@@ -267,7 +286,7 @@ async function executeCode(request) {
267286 }
268287 `
269288
270- const bootstrapScript = await isolate . compileScript ( bootstrap )
289+ bootstrapScript = await isolate . compileScript ( bootstrap )
271290 await bootstrapScript . run ( context )
272291
273292 const wrappedCode = `
@@ -290,7 +309,7 @@ async function executeCode(request) {
290309 })()
291310 `
292311
293- const userScript = await isolate . compileScript ( wrappedCode , { filename : 'user-function.js' } )
312+ userScript = await isolate . compileScript ( wrappedCode , { filename : 'user-function.js' } )
294313 const resultJson = await userScript . run ( context , { timeout : timeoutMs , promise : true } )
295314
296315 let result = null
@@ -357,8 +376,30 @@ async function executeCode(request) {
357376 } ,
358377 }
359378 } finally {
379+ // Release child handles first (scripts, callbacks, references, external copies),
380+ // then dispose the isolate. Order matters: disposing the isolate while child
381+ // handles still exist can cause stuck-GC states (isolated-vm issue #198).
382+ // .release() is idempotent — safe to call even if the object was never assigned.
383+ const releaseables = [
384+ userScript ,
385+ bootstrapScript ,
386+ ...externalCopies ,
387+ fetchCallback ,
388+ errorCallback ,
389+ logCallback ,
390+ context ,
391+ ]
392+ for ( const obj of releaseables ) {
393+ if ( obj ) {
394+ try {
395+ obj . release ( )
396+ } catch { }
397+ }
398+ }
360399 if ( isolate ) {
361- isolate . dispose ( )
400+ try {
401+ isolate . dispose ( )
402+ } catch { }
362403 }
363404 }
364405}
0 commit comments