-
Notifications
You must be signed in to change notification settings - Fork 3
fix: harden N-API boundary against JS-triggerable memory bugs #60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
123382a
e118b09
182d1e2
5af7585
1deb61c
57fdbf3
4245470
6a6231f
a5f59d7
a04a490
ab52f61
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -100,13 +100,12 @@ pub fn doubleBigInt(n: BigInt) !BigInt { | |
| return BigInt.from(val * 2); | ||
| } | ||
|
|
||
| /// Read a BigInt's first u64 word via `getValueBigintWords` passing `null` for `sign_bit`. | ||
| /// Read a single-word BigInt as a u64 via `getValueBigintWords` passing `null` for `sign_bit`. | ||
| /// | ||
| /// Throws if word_count > 1. | ||
| pub fn bigIntFirstWord(n: BigInt) !Number { | ||
| /// Throws `Overflow` if the BigInt needs more than one word. | ||
| pub fn bigIntSingleWord(n: BigInt) !Number { | ||
| var words: [1]u64 = .{0}; | ||
| const got = try n.toValue().getValueBigintWords(null, &words); | ||
| if (got.len > 1) return error.BigIntTooLarge; | ||
| _ = try n.toValue().getValueBigintWords(null, &words); | ||
| return Number.from(words[0]); | ||
| } | ||
|
|
||
|
|
@@ -118,6 +117,14 @@ pub fn bigIntSign(n: BigInt) !Number { | |
| return Number.from(@as(u32, sign)); | ||
| } | ||
|
|
||
| /// Convert a BigInt to an i128 and render it as a decimal string. | ||
| pub fn bigIntToI128String(n: BigInt) !String { | ||
| const v = try n.toI128(); | ||
| var buf: [48]u8 = undefined; | ||
| const s = std.fmt.bufPrint(&buf, "{d}", .{v}) catch return error.FormatError; | ||
| return String.from(s); | ||
| } | ||
|
|
||
| /// Add one day (86400000ms) to a Date. | ||
| pub fn tomorrow(d: Date) Date { | ||
| const ts = d.assertTimestamp(); | ||
|
|
@@ -309,6 +316,18 @@ pub const Buffer = struct { | |
| // Section 10: Mixed DSL + N-API | ||
| // ============================================================================ | ||
|
|
||
| /// Narrow an untyped value to a Number via the `js.Value` narrowing API. | ||
| pub fn narrowToNumber(v: Value) !Number { | ||
| return v.asNumber(); | ||
| } | ||
|
|
||
| /// Narrow an untyped value to a Uint8Array and return its length. | ||
| pub fn narrowToUint8ArrayLen(v: Value) !Number { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we need the Len suffix?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Kept it — the fn returns the length (a |
||
| const arr = try v.asUint8Array(); | ||
| const slice = try arr.toSlice(); | ||
| return Number.from(@as(u32, @intCast(slice.len))); | ||
| } | ||
|
|
||
| /// Return the JS typeof string for any value. | ||
| /// Demonstrates dropping down to low-level napi to call raw N-API methods. | ||
| pub fn getTypeOf(val: Value) !String { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,6 +26,8 @@ pub const Status = enum(c.napi_status) { | |
| would_deadlock = c.napi_would_deadlock, | ||
| no_external_buffers_allowed = c.napi_no_external_buffers_allowed, | ||
| cannot_run_js = c.napi_cannot_run_js, | ||
| // Non-exhaustive: future Node versions may add statuses. | ||
| _, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this still panic
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Switched to |
||
| }; | ||
|
|
||
| pub const NapiError = error{ | ||
|
|
@@ -80,6 +82,7 @@ pub fn check(code: c_uint) NapiError!void { | |
| .would_deadlock => return error.WouldDeadlock, | ||
| .no_external_buffers_allowed => return error.NoExternalBuffersAllowed, | ||
| .cannot_run_js => return error.CannotRunJS, | ||
| _ => return error.GenericFailure, | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -112,3 +115,9 @@ test "isNapiError identifies napi errors" { | |
| try std.testing.expect(isNapiError(error.GenericFailure)); | ||
| try std.testing.expect(!isNapiError(error.OutOfMemory)); | ||
| } | ||
|
|
||
| test "check maps unknown status codes to GenericFailure" { | ||
| // Future Node versions may return statuses this enum doesn't know about; | ||
| // they must map to an error, not invalid-enum UB. | ||
| try std.testing.expectError(error.GenericFailure, check(9999)); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this is the intended behaviour, maybe rename to bigIntSingleWord? bigIntFirstWord seems to imply there's more than one word and we're taking only the first
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call — renamed to
bigIntSingleWord(6a6231f). It reads a single-word BigInt and throws otherwise, so "first" was misleading.