Skip to content

JS_FreeRuntime SIGABRT when Dart GC finalizes JsAsyncRuntime (non-empty gc_obj_list) #8

@wytzepiet

Description

@wytzepiet

Problem

When a JsAsyncRuntime Dart object is garbage collected (e.g. during Flutter hot restart), the Rust Drop calls QuickJS's JS_FreeRuntime, which hits this assertion:

Assertion failed: (list_empty(&rt->gc_obj_list)), function JS_FreeRuntime, file quickjs.c, line 2308.

This is a SIGABRT — it kills the process. It happens because QuickJS still has objects on its GC list (modules, bridge closures, built-in objects from JsBuiltinOptions.essential()) that weren't freed before the runtime was dropped.

Reproduction

  1. Create a runtime, context, and engine with a bridge
  2. Evaluate some JS (especially modules via evaluateModule)
  3. Let the JsAsyncRuntime Dart object get garbage collected (e.g. via Flutter hot restart, or by dropping all references)
final runtime = await JsAsyncRuntime.withOptions(
  builtin: JsBuiltinOptions.essential(),
);
final context = await JsAsyncContext.from(runtime: runtime);
final engine = JsEngine(context: context);
await engine.init(bridge: (v) async => const JsResult.ok(JsValue.none()));
await engine.evaluateModule(
  module: JsModule.code(module: '/test', code: 'export default 1;'),
);
// Later: runtime goes out of scope → Dart GC → Rust Drop → JS_FreeRuntime → SIGABRT

Calling engine.dispose() first doesn't help — the assertion still fires when the runtime is eventually dropped.

Workaround

Bump the Arc ref count so the Rust Drop never runs:

import 'package:flutter_rust_bridge/src/misc/rust_opaque.dart';

void preventNativeDrop(Object obj) {
  if (obj is RustOpaque) {
    obj.frbInternalCstEncode(move: false); // increments Arc strong count
  }
}

// After creation:
preventNativeDrop(runtime);

This permanently leaks the QuickJS runtime (the Arc never reaches 0), but avoids the crash.

Suggested Fix

Before calling JS_FreeRuntime in the Rust Drop, run JS_RunGC and/or iterate rt->gc_obj_list to free remaining objects. Alternatively, skip the assertion and clean up gracefully — QuickJS's assert is unconditional (not behind #ifdef DEBUG).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions