Skip to content

[Bug]: CLI aborts with "Assertion failed: (env) != nullptr" in node::RemoveEnvironmentCleanupHook (bundled Node 24.19.0 + better-sqlite3 12.11.1) #371

Description

@zsxgib

Product or interface

CLI - interactive TUI

Version

  • mcode 0.5.5 (~/.minimax-code/current), installed with the official installer
  • bundled runtime: Node.js v24.19.0 (ABI 137) at ~/.minimax-code/runtime/node-v24.19.0-linux-x64
  • better-sqlite3 12.11.1 (optionalDependency, compiled locally against the bundled 24.19.0 headers)

Platform

Linux

OS version and architecture

Ubuntu 24.04.5 LTS x86_64, kernel 6.8.0-142-lowlatency, glibc 2.39

Issue area

Startup / install / update

Steps to reproduce

  1. Install MCode with the official installer (bundled Node 24.19.0 + better-sqlite3 12.11.1), no other changes.
  2. Run mcode.
  3. Let a session start, or exit the TUI.

This is a native GC/teardown race, so it is intermittent: reproduced once during Starting server..., and it is the same signature that other tools hit on shutdown.

Expected and actual behavior

Expected: the TUI starts and exits normally.

Actual: the process aborts with SIGABRT and prints a native stack trace ending in an assertion inside better-sqlite3's Statement destructor. The session data itself looks fine; the process just dies on teardown.

Redacted error summary

# minimax-code[125849]: void node::RemoveEnvironmentCleanupHook(v8::Isolate*, CleanupHook, void*) at ../src/api/hooks.cc:142
# Assertion failed: (env) != nullptr
----- Native stack trace -----

 1: node::Assert(node::AssertionInfo const&)
 2: node::RemoveEnvironmentCleanupHook(v8::Isolate*, void (*)(void*), void*)
 3: Statement::~Statement()  [/home/zsx/.minimax-code/releases/0.5.5/lib/node_modules/@minimax-ai/code/node_modules/better-sqlite3/build/Release/better_sqlite3.node]
 4: Statement::~Statement()  [better_sqlite3.node]
 5: ... (GC / environment teardown path in the mcode binary)

Analysis

This is not mcode logic; it is the interaction of the bundled Node 24.19.0 with the better-sqlite3 12.x native addon:

  1. Node 24.19.0 added cleanup hooks to node::ObjectWrap (src: add cleanup hooks to node::ObjectWrap nodejs/node#63642, commit 4b5eb7b72d). The header shipped inside the bundled runtime confirms it, include/node/node_object_wrap.h:

    ObjectWrap()  { refs_ = 0; AddCleanupHook(); }
    virtual ~ObjectWrap() {
      RemoveCleanupHook();
      ...
    }
    void RemoveCleanupHook() {
      RemoveEnvironmentCleanupHook(v8::Isolate::GetCurrent(), CleanupHook, this);
    }
  2. If a wrapped object survives until the final GC after the environment has been torn down, its destructor calls RemoveEnvironmentCleanupHook() with a destroyed environment, Environment::GetCurrent() returns nullptr, and the (env) != nullptr assertion fires. This is Use-after-free in CleanupHookThunkRun for every node::ObjectWrap alive at teardown nodejs/node#65195 ("Use-after-free in CleanupHookThunkRun for every node::ObjectWrap alive at teardown"); the fix PR src: fix use-after-free in CleanupHookThunkRun nodejs/node#65196 is still open, and Node 24.20.0 / 24.21.0 do not contain a fix.

  3. better-sqlite3 12.11.1 still uses node::ObjectWrap, which is compiled into the shipped addon. Verified on the installed binary:

    $ nm -D -C build/Release/better_sqlite3.node | grep -iE 'cleanup|ObjectWrap'
    W node::ObjectWrap::CleanupHook(void*)
    U node::AddEnvironmentCleanupHook(v8::Isolate*, void (*)(void*), void*)
    U node::RemoveEnvironmentCleanupHook(v8::Isolate*, void (*)(void*), void*)
    

    (node-pty 1.x and other node::ObjectWrap-based addons are affected by the same Node regression.)

Suggested fix

Upgrade better-sqlite3 from 12.11.1 to 13.x, the first N-API release (node-addon-api), which does not use node::ObjectWrap:

$ nm -D -C prebuilds/linux-x64.node | grep -i cleanup
U napi_add_env_cleanup_hook

Alternatively, ship a Node runtime that contains the nodejs/node#65195 fix / #63642 revert. As a data point, several downstream projects fixed the same abort by moving to better-sqlite3 13.x.

Workaround verified locally

Replacing the bundled package in place (13.0.3, N-API prebuilt, no compile step) removes the abort:

cd ~/.minimax-code/releases/0.5.5/lib/node_modules/@minimax-ai/code/node_modules
mv better-sqlite3 better-sqlite3.orig
cp -a <better-sqlite3@13.0.3>/node_modules/better-sqlite3 better-sqlite3

After that the TUI starts and exits cleanly (Starting server... -> Ask Mcode -> Intelligence with everyone, bye~) with no assertion, and the APIs mcode uses (prepare/exec/transaction/backup/pragma/function/aggregate/iterate/readonly/fileMustExist/close) all behave the same.

Note: MAVIS_SQLITE3_MODULE_PATH / database.sqlite3ModulePath does not work around this, because chunks/chunk-S2IS2DS4.js does a static import M8 from "better-sqlite3" that always resolves against the package's own node_modules; the bundled copy has to be replaced.

Before submitting

  • I have searched existing issues.
  • I have included my version and removed sensitive information.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingneeds-triageAwaiting maintainer assessmenttuiInteractive terminal UI (TUI)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions