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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions