Skip to content

fix: keep the terminal class reference per Node environment - #23

Merged
ThomasK33 merged 2 commits into
mainfrom
fix/worker-thread-class-ref
Oct 29, 2026
Merged

ThomasK33 merged 2 commits into
mainfrom
fix/worker-thread-class-ref

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

What breaks today

Any process that loads this package in more than one Node environment crashes. That includes the main thread plus a worker_threads Worker, or two Workers. The crash happens on the next createTerminal in an environment whose class reference was overwritten:

  • With the other Worker still alive, V8 aborts with FATAL ERROR: v8::HandleScope::CreateHandle() Cannot create a handle without a HandleScope (SIGABRT).
  • After that Worker exited, the process segfaults (SIGSEGV).

Cause

TerminalWrap kept its class in one process-wide static Napi::FunctionReference constructor. Node-API addons are loaded once per environment, so each environment ran TerminalWrap::Init and overwrote the static:

  1. The main thread imports the package. Init stores the main thread's reference.
  2. A Worker imports the package. constructor = Napi::Persistent(func) deletes the main thread's reference through the main thread's napi_env, from the worker thread, and stores the worker's reference.
  3. The main thread calls createTerminal. constructor.New opens a handle scope in the worker's environment and crashes.

Fix

TerminalWrap::Init now stores the reference in the environment's instance data (env.SetInstanceData), and TerminalWrap::Create reads it with env.GetInstanceData. Each environment creates terminals from its own class, and Node deletes the reference when that environment shuts down. This is the pattern that the node-addon-api ObjectWrap docs use.

Tests

test/workers.test.ts runs three scenarios, each in a child process so that a crash cannot kill the test runner:

  1. The main thread, then a live worker, then the main thread creates.
  2. The main thread, then a worker that exits, then the main thread creates.
  3. Two workers, each creating after the other loaded.

Before the fix all three failed (SIGABRT, SIGSEGV, SIGABRT). After the fix all three pass.

Validation (local, linux-x64, Node v26.12.1, zig 0.15.2)

  • npm run build:native, npm run build, npm run typecheck, npm test (9/9 passed), npm run smoke: all exit 0.
  • test/workers.test.ts run 10 more times: 10/10 passed.
  • Stress script: 20 rounds of 4 Workers that each create 50 terminals while the main thread creates 200, then terminate. 4,000 main-thread terminals, exit 0.
  • The packed package, installed into a scratch consumer project, passed all 18 user journeys of the verify-libghostty-vt-node skill (top PR of this stack). That includes the worker-thread journey, which crashed before this fix.

Found by the TLA+ model in the next PR of this stack (verify-native-lifecycle).


Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high

TerminalWrap kept its class in one process-wide static
Napi::FunctionReference. Every environment that loads the addon (the
main thread and each worker_threads Worker) ran TerminalWrap::Init and
overwrote it. createTerminal in one thread then used another thread's
reference: V8 aborted with "Cannot create a handle without a
HandleScope", or the process segfaulted once that worker had exited.

Store the reference in the environment's instance data instead, so each
environment creates terminals from its own class. Node deletes it when
the environment shuts down.

test/workers.test.ts runs three worker scenarios in child processes.
All three crashed before this change and pass after it.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_

Change-Id: I9b923f58ec18c7f246bf37a0c1b3ab0856a550f3
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33
ThomasK33 added this pull request to stack #26 October 29, 2026 13:09
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-29T13:29:08.185223Z 0d25536 Manual request
🔒 Security Review ✅ Completed 2026-10-29T13:29:48.328318Z 0d25536 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

Reviewed commit: 4aa7039266

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: 4aa7039266

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

Each load of the addon gets its own napi_env, so the instance-data slot
is always empty when TerminalWrap::Init runs: assert that instead of
deleting a previous reference (which ran before the new reference
existed). Two process.dlopen calls of the addon in one env still work,
in the main thread and in a Worker.

test/workers.test.ts no longer requires an empty stderr (NODE_OPTIONS
can print there) and shows it as the failure message. Each child run is
bounded at 20 s and each test at 30 s.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_

Change-Id: Id7d371f6bbbaa47fd6f2b3cf7d411a695d3d3d27
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

Reviewed commit: 0d255369dd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: 0d255369dd

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@ThomasK33
ThomasK33 merged commit ead6190 into main Oct 29, 2026
4 checks passed
This was referenced Oct 30, 2026
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