Repository navigation
fix(native): reject typed arrays other than Uint8Array in feed - #49
Conversation
The non-Uint8Array cases fail: napi_is_buffer accepts every ArrayBufferView, so the Buffer branch takes them first. Signed-off-by: Thomas Kosiewski <tk@coder.com> --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `medium`_ Change-Id: Iae93d5917e6918dd8161be33ba7cf7ba3d8c173c
napi_is_buffer is true for every ArrayBufferView, so the IsBuffer branch read Uint16Array, Float32Array, Uint8ClampedArray and DataView as bytes before the Uint8Array check could run. A Buffer is a Uint8Array, so the typed-array branch covers it. Signed-off-by: Thomas Kosiewski <tk@coder.com> --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `medium`_ Change-Id: Ib6eeb52941e0da75444187109d01e5b0192d1727
|
Verdict: PASS Parent: 9f8ec06 (merge base with main). Node v26.12.1 inside npm scripts ( Scenario 1: repro script. Command:
Scenario 2: byte input, raw addon and public API. A local probe ran 34 inputs through both surfaces (68 lines) and printed the visible text, the cursor, or the error.
Gates at head (each exit 0):
Diff review (
Observation (same at parent and head, not caused by this PR): a Generated with |
## Why Ship the `snapshot()` crash fix (#46) to npm. Every earlier version, `0.1.0-beta.2` included, crashes Node when a JS accessor on `Object.prototype` disposes the terminal while `snapshot()` builds its result. ## What changed - `package.json` and `package-lock.json` go from `0.1.0-beta.2` to `0.1.0-beta.3`. The branch name matches the version, so `publish.yml` creates `v0.1.0-beta.3` after merge. - The `release-changelog` workflow adds the CHANGELOG entry to this branch. - beta.3 also ships #48 (tests only) and #49 (the raw native addon's `feed` now rejects typed arrays other than `Uint8Array`). The package exports only `createTerminal`'s module, which already rejected those values in JS, so neither gets a CHANGELOG line. - The beta.2 `## Fixed` heading, which the changelog bot had joined to the previous bullet, is restored. ## Blast Radius The merge publishes `0.1.0-beta.3` to npm under the `beta` dist-tag. `latest` stays on `0.1.0-beta.0`. Snapshot output is byte-identical to beta.2, as #46's verifier checked. ## Verification - CI (linux-x64, linux-arm64, linux-x64-ubsan, macos-arm64) passes on head `3365375`. Codex code and security reviews are clean on that head. - `prepare-release-changelog.mjs` reports `changelog-entry-exists`. `resolve-npm-publish-tag.mjs` prints `beta`. The remote has no `v0.1.0-beta.3` tag. - An independent verifier built prebuilds, ran `verify:prebuilds` (loads `prebuilds/linux-x64`), packed, and installed into a scratch consumer: `0.1.0-beta.3` loads from `prebuilds/`, and 8 accessor cases that crash beta.2 exit 0. It passed the CHANGELOG at `da96287`. --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_
## Why The `verify-native-lifecycle` skill keeps a sync commit. At that commit, its TLA+ model describes the covered native code. The sync commit was still `7366b75` (#12). Since then, #40 to #43, #46, and #49 changed covered code. #46 promised this follow-up. ## What changed - The sync commit moves to `c54051e`, the current main. It includes #48 and #49. - The coverage rows now name `ghostty_terminal_set` (scrollback byte limit) in the constructor and its failure path, `Resize`'s second `ghostty_terminal_resize` call, and the `GHOSTTY_TERMINAL_DATA_MODE` read in `EncodeMouse`. Ghostty v1.4.0 (#43) brought these. - The "no JS inside native methods" assumption now says how each method reads its arguments: 1. `Snapshot` reads its options before `RequireTerminal`, and it builds its result with `DefineValue` and `DefineIndex`. 2. `Feed` and `Resize` call `RequireTerminal` first. After that, they only test types and read strings, bytes, and numbers, which runs no JS. 3. `GetVisibleText`, `FormatPlain`, and `FormatHtml` read no arguments. 4. The assumption was false for `snapshot` until #46. - The bullet that lists where caller JS can run now names public `feed`: `assertFeedData` runs `data instanceof Uint8Array`, which runs a Proxy's `getPrototypeOf` trap after `#assertUsable`. The native null check covers it, as it does for `snapshot`. - `AddonEnvLifecycle.tla` changes in comments only. The `CreateFails` comment now names the `ghostty_terminal_set` failure too. The model itself does not change. #48 changes only a test. #49 changes only `Feed`'s argument checks, so the `Use(t)` step still describes `feed`. ## Verification - I checked each row against `native/terminal.cc` and `src/index.ts` at `c54051e`. - `git diff 7366b75 c54051e` on the covered paths adds no new static state, `Napi::Reference`, or new place that frees `terminal_` or `mouse_encoder_`. - `run.sh`: all 35 rows match EXPECT (`OK: every result matches EXPECT.`). The current config holds all 5 rules. Each of the 6 mutants breaks the rule it guards. The verifier's two notes on `0ff0941` (the `feed` window and the `CreateFails` comment) are fixed in `584c561`. `run.sh` still passes all 35 rows. Workspace 36beaed175 drafted the first version of this commit. I corrected the assumption bullet: it said that every method reads its arguments before `RequireTerminal`, but `Feed` and `Resize` do not. --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_
Why
The goal is to handle the items still open after the #12 follow-ups and land each on main as its own PR. This one fixes the raw addon's
feed.native/README.mdsays the addon'sfeedthrowsTypeError("feed typed array must be Uint8Array")for a typed array that is not aUint8Array. It throwsTypeError("feed data must be a string, Buffer, or Uint8Array")for any other value. The typed-array check inTerminalWrap::Feednever ran. TheIsBuffer()branch before it is true for every ArrayBufferView in Node, becausenapi_is_bufferaccepts any view. So aUint16ArrayorFloat32Arraywas written as raw bytes, and aDataViewfailed withError: Invalid argument.What changed
native/terminal.cc: I deleted theIsBuffer()branch inTerminalWrap::Feed. A Buffer is aUint8Array, so the typed-array branch now handles it.test/terminal.test.ts: new raw-addon tests.Uint16Array,Float32ArrayandUint8ClampedArraythrow the typed-arrayTypeError. ADataViewthrows the genericTypeError.Buffer,Uint8Arrayand a Buffer slice with a byte offset still write "AB".native/README.md: the Verification section no longer lists the typed-array errors as untested.Scope
Only the raw addon's
feedargument check changes.src/index.tsalready rejects these values before native code runs, so the public API keeps its behavior.Blast Radius
Code that calls the raw addon directly with a non-
Uint8Arraytyped array or aDataViewnow gets aTypeError. Before, it got raw bytes written orError: Invalid argument. Package users who go throughcreateTerminalfrom the package see no change.Verification
The test commit (1727d01) fails on the unfixed code:
With the fix (fabeeaf):
Repro script (
node repro-feed.mjsafternpm run build), after the fix. On main the rawUint16Array,Float32ArrayandUint8ClampedArraycases returned "ok", andDataViewthrewError: Invalid argument.Gates, each exit 0:
npm ci && LIBGHOSTTY_VT_PREFIX=<ReleaseFast> npm run build:native && npm run build && npm run verify: 84 tests passed.rm -rf dist && LIBGHOSTTY_VT_PREFIX=<ReleaseFast> CXXFLAGS="-fsanitize=undefined,float-cast-overflow -fno-sanitize-recover=all" LDFLAGS="-fsanitize=undefined,float-cast-overflow" npm run build:native && npm test && npm run smoke: 84 tests passed, smoke passed..agents/skills/verify-native-lifecycle/run.sh:OK: every result matches EXPECT.Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:medium