Skip to content

fix: harden N-API boundary against JS-triggerable memory bugs#60

Draft
nazarhussain wants to merge 5 commits into
mainfrom
nh/code-analysis
Draft

fix: harden N-API boundary against JS-triggerable memory bugs#60
nazarhussain wants to merge 5 commits into
mainfrom
nh/code-analysis

Conversation

@nazarhussain

@nazarhussain nazarhussain commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Fixes JS-triggerable memory-safety bugs found in a security audit of src/js and the N-API layer.

Fixes

  • BigInt word OOB read (getValueBigintWords): napi reports the required word count, which can exceed the caller's buffer; slicing by it read out of bounds. Now returns error.Overflow.
  • BigInt.toI128 overflow: out-of-range magnitudes and the valid -2^127 both tripped @intCast. Now range-checked.
  • Unknown napi enum values (Status, ValueType, TypedarrayType): exhaustive enums from napi out-params were invalid-enum UB (e.g. Float16Array, newer statuses). Made non-exhaustive; unknown values map to errors.
  • Constructor without new: wrapped native state onto globalThis. Now throws TypeError.
  • Error-path memory bugs (not reachable from the test harness): double-free in materializeClassInstance, use-after-free in registerClass list linking, catch unreachable in module registration.
  • Bonus: implemented expectType/expectTypedArrayOfType, which every js.Value.as*() method called but were never defined (compile error on use, hidden by lazy analysis).

Safe builds panic (process abort / DoS); ReleaseFast — the usual production build — gets UB.

Tests

  • zig build test:napi / test:zapi, pnpm test:js all green
  • New regression tests: i128 boundaries, out-of-range BigInts, no-new calls, value narrowing
  • examples/targets has one pre-existing, unrelated failure (musl detection on macOS)

🤖 Generated with Claude Code

nazarhussain and others added 5 commits July 22, 2026 10:19
napi sets the out word_count to the required count, which can exceed
the caller's buffer; slicing by it read out of bounds for BigInts
wider than the buffer (panic in safe builds, UB in ReleaseFast).
getValueBigintWords returns error.Overflow instead. toI128 now
range-checks the magnitude and handles -2^127, which previously
tripped @intcast before negation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Status, ValueType, and TypedarrayType were exhaustive enums populated
straight from napi out-params; a status or array type this binding
doesn't know (e.g. Float16Array, statuses added in newer Node) was
invalid-enum UB in ReleaseFast and a panic in safe builds. Make them
non-exhaustive and map unknown values to errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Without new, V8 hands the global proxy as the receiver, so the
generated constructor wrapped native state and a finalizer onto
globalThis and type-tagged it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- wrapTaggedObject destroyed the native object on tagging failure
  while materializeClassInstance's errdefer destroyed it again
  (double free); ownership now stays with the caller on error
- registerClass linked the entry before addEnvCleanupHook, so a hook
  failure freed the list head while still reachable (use-after-free)
  and leaked the ctor ref
- module registration aborted via catch unreachable when throwError
  failed with an exception already pending

None of these paths are reachable from the JS test harness (they need
napi calls failing mid-sequence), hence no regression tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
expectType and expectTypedArrayOfType were called by every as*()
narrowing method but never defined; Zig's lazy analysis hid it until
a consumer instantiated one, which then failed to compile.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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