From d3eff70a96ea9f470ee27af1e834cba05c8e4027 Mon Sep 17 00:00:00 2001 From: bing Date: Tue, 26 May 2026 21:35:44 +0800 Subject: [PATCH 1/2] fix: fix UB in getValueBigintWords Casting a `u1` to `*int` is UB since we're trying to read 4 bytes from a 1 bit-wide integer The fix: we pass a `c_int` into the c api and then read that result back into the `u1` value. --- examples/js_dsl/mod.test.ts | 18 ++++++++++++++++++ examples/js_dsl/mod.zig | 18 ++++++++++++++++++ src/Value.zig | 19 ++++++++++++++++--- 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/examples/js_dsl/mod.test.ts b/examples/js_dsl/mod.test.ts index 6d290f9..9df6556 100644 --- a/examples/js_dsl/mod.test.ts +++ b/examples/js_dsl/mod.test.ts @@ -94,6 +94,24 @@ describe("primitive types", () => { expect(value).toEqual(2 ** 63); }); + describe("getValueBigintWords", () => { + it("reads words with null sign_bit (unsigned only)", () => { + expect(mod.bigIntFirstWord(0n)).toEqual(0); + expect(mod.bigIntFirstWord(1n)).toEqual(1); + expect(mod.bigIntFirstWord(0xdeadbeefn)).toEqual(0xdeadbeef); + }); + + it("reads correct sign (non-null sign_bit path)", () => { + expect(mod.bigIntSign(0n)).toEqual(0); + expect(mod.bigIntSign(1n)).toEqual(0); + expect(mod.bigIntSign(0xffffffffffffffffn)).toEqual(0); + + expect(mod.bigIntSign(-1n)).toEqual(1); + expect(mod.bigIntSign(-0xffffffffffffffffn)).toEqual(1); + }); + + }); + it("tomorrow adds one day", () => { const now = new Date("2025-01-01T00:00:00Z"); const result = mod.tomorrow(now); diff --git a/examples/js_dsl/mod.zig b/examples/js_dsl/mod.zig index 0cf16f3..1eef6cd 100644 --- a/examples/js_dsl/mod.zig +++ b/examples/js_dsl/mod.zig @@ -100,6 +100,24 @@ 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`. +/// +/// Throws if word_count > 1. +pub fn bigIntFirstWord(n: BigInt) !Number { + var words: [1]u64 = .{0}; + const got = try n.toValue().getValueBigintWords(null, &words); + if (got.len > 1) return error.BigIntTooLarge; + return Number.from(words[0]); +} + +/// Read a BigInt's sign as 0/1 via `getValueBigintWords` passing a non-null `sign_bit`. +pub fn bigIntSign(n: BigInt) !Number { + var sign: u1 = 0; + var words: [1]u64 = .{0}; + _ = try n.toValue().getValueBigintWords(&sign, &words); + return Number.from(@as(u32, sign)); +} + /// Add one day (86400000ms) to a Date. pub fn tomorrow(d: Date) Date { const ts = d.assertTimestamp(); diff --git a/src/Value.zig b/src/Value.zig index 534b21b..f30ade2 100644 --- a/src/Value.zig +++ b/src/Value.zig @@ -216,12 +216,25 @@ pub fn getValueBigintUint64(self: Value, lossless: ?*bool) NapiError!u64 { return bigint; } -/// https://nodejs.org/api/n-api.html#napi_get_value_bigint_words -pub fn getValueBigintWords(self: Value, sign_bit: *u1, words: []u64) NapiError![]u64 { +/// Reads a JS `BigInt` into `words`. +/// +/// Pass `null` for `sign_bit` to skip getting the sign result. +/// +/// In Ethereum's context, this is useful for big integers defined in the spec to be +/// unsigned. +/// +/// NOTE: napi's C entry takes `int*` (4-byte aligned). Casting a u1 to int* is UB, since that +/// is 4-bytes aligned. We use a local `c_int` for the napi call and narrow back to `u1` for the caller. +/// +/// Source: /// https://nodejs.org/api/n-api.html#napi_get_value_bigint_words +pub fn getValueBigintWords(self: Value, sign_bit: ?*u1, words: []u64) NapiError![]u64 { var word_count: usize = words.len; + var raw_sign: c_int = 0; try status.check( - c.napi_get_value_bigint_words(self.env, self.value, @alignCast(@ptrCast(sign_bit)), &word_count, @ptrCast(words)), + c.napi_get_value_bigint_words(self.env, self.value, &raw_sign, &word_count, words.ptr), ); + // napi guarantees raw_sign ∈ {0, 1} + if (sign_bit) |s| s.* = @intCast(raw_sign); return words[0..word_count]; } From 3aff499436bc07d1d4f7b39f14cfedac4d0aecf0 Mon Sep 17 00:00:00 2001 From: bing Date: Wed, 27 May 2026 23:38:28 +0800 Subject: [PATCH 2/2] fix doc comment oopsies --- src/Value.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Value.zig b/src/Value.zig index f30ade2..bd3b206 100644 --- a/src/Value.zig +++ b/src/Value.zig @@ -226,7 +226,7 @@ pub fn getValueBigintUint64(self: Value, lossless: ?*bool) NapiError!u64 { /// NOTE: napi's C entry takes `int*` (4-byte aligned). Casting a u1 to int* is UB, since that /// is 4-bytes aligned. We use a local `c_int` for the napi call and narrow back to `u1` for the caller. /// -/// Source: /// https://nodejs.org/api/n-api.html#napi_get_value_bigint_words +/// Source: https://nodejs.org/api/n-api.html#napi_get_value_bigint_words pub fn getValueBigintWords(self: Value, sign_bit: ?*u1, words: []u64) NapiError![]u64 { var word_count: usize = words.len; var raw_sign: c_int = 0;