Skip to content

Move off the driver that Node 24.19 aborts - #160

Merged
bazauto merged 1 commit into
mainfrom
chore/better-sqlite3-13
Aug 23, 2026
Merged

bazauto merged 1 commit into
mainfrom
chore/better-sqlite3-13

Conversation

@bazauto

@bazauto bazauto commented Aug 23, 2026

Copy link
Copy Markdown
Owner

The bench box crashed and restarted repeatedly on 2026-08-23 with no common operator action behind it. The stack trace pointed straight at the driver:

#  void node::RemoveEnvironmentCleanupHook(...) at ../src/api/hooks.cc:142
#  Assertion failed: (env) != nullptr
#  Statement::~Statement() [better-sqlite3.node]
status=6/ABRT

What was aborting

Node 24.19.0 landed on the bench that morning (NodeSource, 4 minutes before the service first started) and added self-removing cleanup hooks to node::ObjectWrap (nodejs/node#63642). better-sqlite3 v11 derives Statement/Database/etc. from the NAN-style ObjectWrap, whose destructor calls RemoveEnvironmentCleanupHook. That runs from a V8 GC weak callback with no entered context, so Environment::GetCurrent(isolate) is null and the CHECK_NOT_NULL aborts.

The trigger is the garbage collector, not any request — which is exactly why it looked intermittent and unrelatable. drizzle prepares and discards statements per query, so candidates are produced continuously; the timing is a lottery. 8 aborts in the journal, all on 24.19.0; the dev machine on 24.15.0 never saw it.

It is also not fail-safe: a loco under power keeps running until systemd restarts the unit ~5 s later.

The fix

better-sqlite3 ^11 → ^13.0.3. v13's node-addon-api rewrite removes the crashing path entirely — this is structural, not probabilistic. Comparing the two prebuilt binaries on the bench:

v11.10.0  ->  U node::AddEnvironmentCleanupHook / U node::RemoveEnvironmentCleanupHook   (napi symbols: 0)
v13.0.3   ->  (no such imports)                                                          (napi symbols: 60)

v13 also ships N-API prebuilds for every platform, so nothing compiles on the box any more, and the ABI coupling that caused this is gone permanently — a floating 24.x stays safe.

Between v11.10.0 and v13.0.3 the only breaking change was v12.0.0 dropping EOL Node 18; v13 is additive API only. SQLite 3.49.2 → 3.53.4.

@fastify/static in the same PR (security)

npm audit flagged a high on @fastify/static 8.3.0 — four advisories including route-guard bypass via encoded path separators and authorization bypass via non-canonical URL paths. That lands squarely on deployment.md D3, the deny-by-default predicate exempting the SPA from the auth hook. Bumped ^8.3.0 → ^10.1.3, which clears it. v10's only breaking change is setHeaders(reply) instead of res; we don't use setHeaders. The remaining 4 moderate advisories are the documented, unreachable dev-only esbuild/drizzle-kit chain (--force stays banned).

Verified

  • npm test from root: 1427 backend + 273 frontend passing, exit 0
  • npm run lint + tsc: clean
  • staticFrontend.test.ts (the SPA auth-exemption path, incl. a traversal negative and the deny-by-default 401): green against v10.1.3
  • drizzle-kit migrate: all 15 migrations apply to a fresh DB
  • SqliteError.code preserved (SQLITE_CONSTRAINT_UNIQUE, SQLITE_CONSTRAINT_TRIGGER) — the last-admin guard and username-taken translation in authRepository.ts still work
  • VACUUM INTO / readonly / fileMustExist: work (deploy/backup-db.cjs unaffected)
  • Live Westgate Hollow DB under v13 on the bench: integrity_check ok, foreign_key_check empty, WAL intact, all 13 tables and 15 migrations present, opened read-only

Docs moved with the code

  • engines.node >=20 → >=22 (v13's floor)
  • docs/deployment.md — the pin rationale: why the driver floor is >=13 and why not to pin Node back to dodge a build
  • docs/current-state.md — the long form of that in the Deployment row
  • deploy/bootstrap.sh — corrected native-module note (better-sqlite3 no longer compiles on the box; serialport and argon2 still can)

🤖 Generated with Claude Code

Node 24.19.0 added self-removing cleanup hooks to node::ObjectWrap
(nodejs/node#63642). better-sqlite3 v11 uses the NAN-style ObjectWrap,
whose statement destructor calls RemoveEnvironmentCleanupHook under GC
with no entered context, so the process aborts: "Assertion failed:
(env) != nullptr" at hooks.cc:142, SIGABRT. It fires on garbage
collection, not on any request, which is why the bench crashes looked
intermittent and unrelatable to what the operator was doing. The abort
is not fail-safe - a powered loco keeps running until systemd restarts
the unit ~5 s later.

v13's node-addon-api rewrite removes the path entirely: the prebuilt
linux-x64.node imports zero cleanup-hook symbols (60 napi symbols, none
of AddEnvironmentCleanupHook/RemoveEnvironmentCleanupHook), confirmed
against the binary on the bench. v13 also ships prebuilds that load on
any Node >=22, so it no longer compiles on the box.

Verified end to end: full suite green (1427 backend + 273 frontend),
lint and tsc clean, drizzle-kit migrate applies all 15 migrations, the
SqliteError codes the auth guard reads (SQLITE_CONSTRAINT_UNIQUE,
SQLITE_CONSTRAINT_TRIGGER) survive, VACUUM INTO / readonly /
fileMustExist still work, and the live Westgate Hollow DB passes
integrity_check and foreign_key_check opened read-only under v13 on the
bench.

Bumps @fastify/static 8.3.0 -> 10.1.3 in the same PR, clearing a high
advisory (route-guard bypass via encoded path separators / non-canonical
paths) that lands on the deployment.md D3 SPA auth exemption. v10's only
breaking change is setHeaders(reply); we don't use setHeaders.
staticFrontend.test.ts covers the exemption path, traversal negative
included, and stays green.

engines.node raised 20 -> 22 (v13's floor). Docs moved with the code:
deployment.md gains the pin rationale, current-state.md the long form,
bootstrap.sh the corrected native-module note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@bazauto
bazauto merged commit b48289f into main Aug 23, 2026
1 check passed
@bazauto
bazauto deleted the chore/better-sqlite3-13 branch August 23, 2026 12:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant