From 44e1a444e4fcb2d7474a37aa18665c6d5be1a64f Mon Sep 17 00:00:00 2001
From: Paraspandey-debugs
Date: Thu, 5 Nov 2026 17:19:25 +0530
Subject: [PATCH 1/2] fix(value): prevent saturation in
ToInt8/ToUint8/ToInt16/ToUint16 for |x| >= 2^63 (#5632)
---
core/engine/src/builtins/typed_array/tests.rs | 17 +++++
core/engine/src/value/mod.rs | 76 ++++---------------
core/engine/src/value/tests.rs | 32 ++++++++
3 files changed, 64 insertions(+), 61 deletions(-)
diff --git a/core/engine/src/builtins/typed_array/tests.rs b/core/engine/src/builtins/typed_array/tests.rs
index 4fe4d21c059..45edce286c9 100644
--- a/core/engine/src/builtins/typed_array/tests.rs
+++ b/core/engine/src/builtins/typed_array/tests.rs
@@ -54,6 +54,23 @@ fn uint8array_numeric_coercion() {
]);
}
+#[test]
+fn typed_array_large_values_numeric_coercion() {
+ run_test_actions([
+ TestAction::run("let a = new Uint8Array([1e40, 2**63, 1e20, 2**53 + 2]);"),
+ TestAction::assert_eq("a[0]", 0),
+ TestAction::assert_eq("a[1]", 0),
+ TestAction::assert_eq("a[2]", 0),
+ TestAction::assert_eq("a[3]", 2),
+ TestAction::run("let b = new Int8Array([1e40]);"),
+ TestAction::assert_eq("b[0]", 0),
+ TestAction::run("let c = new Uint16Array([1e40]);"),
+ TestAction::assert_eq("c[0]", 0),
+ TestAction::run("let d = new Int16Array([1e40]);"),
+ TestAction::assert_eq("d[0]", 0),
+ ]);
+}
+
#[test]
fn float32array_storage() {
run_test_actions([
diff --git a/core/engine/src/value/mod.rs b/core/engine/src/value/mod.rs
index e92d4809447..4ad84739309 100644
--- a/core/engine/src/value/mod.rs
+++ b/core/engine/src/value/mod.rs
@@ -4,7 +4,7 @@
use num_bigint::BigInt;
use num_integer::Integer;
-use num_traits::{ToPrimitive, Zero};
+use num_traits::ToPrimitive;
use std::{ops::Sub, sync::LazyLock};
use boa_gc::{Finalize, Trace};
@@ -1142,23 +1142,10 @@ impl JsValue {
// 1. Let number be ? ToNumber(argument).
let number = self.to_number(context)?;
- // 2. If number is NaN, +0𝔽, -0𝔽, +∞𝔽, or -∞𝔽, return +0𝔽.
- if number.is_nan() || number.is_zero() || number.is_infinite() {
- return Ok(0);
- }
-
- // 3. Let int be the mathematical value whose sign is the sign of number and whose magnitude is floor(abs(ℝ(number))).
- let int = number.abs().floor().copysign(number) as i64;
-
- // 4. Let int8bit be int modulo 2^8.
- let int_8_bit = int % 2i64.pow(8);
-
- // 5. If int8bit ≥ 2^7, return 𝔽(int8bit - 2^8); otherwise return 𝔽(int8bit).
- if int_8_bit >= 2i64.pow(7) {
- Ok((int_8_bit - 2i64.pow(8)) as i8)
- } else {
- Ok(int_8_bit as i8)
- }
+ // 2-5. ToInt32 already returns 0 for NaN/±0/±∞ and reduces modulo 2^32.
+ // Casting to i8 keeps the low 8 bits, which is int modulo 2^8 mapped
+ // into the signed range.
+ Ok(f64_to_int32(number) as i8)
}
/// `7.1.11 ToUint8 ( argument )`
@@ -1171,19 +1158,9 @@ impl JsValue {
// 1. Let number be ? ToNumber(argument).
let number = self.to_number(context)?;
- // 2. If number is NaN, +0𝔽, -0𝔽, +∞𝔽, or -∞𝔽, return +0𝔽.
- if number.is_nan() || number.is_zero() || number.is_infinite() {
- return Ok(0);
- }
-
- // 3. Let int be the mathematical value whose sign is the sign of number and whose magnitude is floor(abs(ℝ(number))).
- let int = number.abs().floor().copysign(number) as i64;
-
- // 4. Let int8bit be int modulo 2^8.
- let int_8_bit = int % 2i64.pow(8);
-
- // 5. Return 𝔽(int8bit).
- Ok(int_8_bit as u8)
+ // 2-5. ToUint32 already returns 0 for NaN/±0/±∞ and reduces modulo 2^32.
+ // Casting to u8 keeps the low 8 bits, which is int modulo 2^8.
+ Ok(f64_to_uint32(number) as u8)
}
/// `7.1.12 ToUint8Clamp ( argument )`
@@ -1243,23 +1220,10 @@ impl JsValue {
// 1. Let number be ? ToNumber(argument).
let number = self.to_number(context)?;
- // 2. If number is NaN, +0𝔽, -0𝔽, +∞𝔽, or -∞𝔽, return +0𝔽.
- if number.is_nan() || number.is_zero() || number.is_infinite() {
- return Ok(0);
- }
-
- // 3. Let int be the mathematical value whose sign is the sign of number and whose magnitude is floor(abs(ℝ(number))).
- let int = number.abs().floor().copysign(number) as i64;
-
- // 4. Let int16bit be int modulo 2^16.
- let int_16_bit = int % 2i64.pow(16);
-
- // 5. If int16bit ≥ 2^15, return 𝔽(int16bit - 2^16); otherwise return 𝔽(int16bit).
- if int_16_bit >= 2i64.pow(15) {
- Ok((int_16_bit - 2i64.pow(16)) as i16)
- } else {
- Ok(int_16_bit as i16)
- }
+ // 2-5. ToInt32 already returns 0 for NaN/±0/±∞ and reduces modulo 2^32.
+ // Casting to i16 keeps the low 16 bits, which is int modulo 2^16 mapped
+ // into the signed range.
+ Ok(f64_to_int32(number) as i16)
}
/// `7.1.9 ToUint16 ( argument )`
@@ -1272,19 +1236,9 @@ impl JsValue {
// 1. Let number be ? ToNumber(argument).
let number = self.to_number(context)?;
- // 2. If number is NaN, +0𝔽, -0𝔽, +∞𝔽, or -∞𝔽, return +0𝔽.
- if number.is_nan() || number.is_zero() || number.is_infinite() {
- return Ok(0);
- }
-
- // 3. Let int be the mathematical value whose sign is the sign of number and whose magnitude is floor(abs(ℝ(number))).
- let int = number.abs().floor().copysign(number) as i64;
-
- // 4. Let int16bit be int modulo 2^16.
- let int_16_bit = int % 2i64.pow(16);
-
- // 5. Return 𝔽(int16bit).
- Ok(int_16_bit as u16)
+ // 2-5. ToUint32 already returns 0 for NaN/±0/±∞ and reduces modulo 2^32.
+ // Casting to u16 keeps the low 16 bits, which is int modulo 2^16.
+ Ok(f64_to_uint32(number) as u16)
}
/// `7.1.15 ToBigInt64 ( argument )`
diff --git a/core/engine/src/value/tests.rs b/core/engine/src/value/tests.rs
index 5b44eef2f46..fbd14291605 100644
--- a/core/engine/src/value/tests.rs
+++ b/core/engine/src/value/tests.rs
@@ -723,6 +723,38 @@ fn to_int32() {
})]);
}
+#[test]
+fn to_small_ints_large_values() {
+ run_test_actions([TestAction::inspect_context(|ctx| {
+ let f = JsValue::new;
+
+ // |x| >= 2^63 used to saturate
+ for x in [1e40, 2f64.powi(63), 1e20, -1e20] {
+ assert_eq!(f(x).to_uint8(ctx).unwrap(), 0);
+ assert_eq!(f(x).to_int8(ctx).unwrap(), 0);
+ assert_eq!(f(x).to_uint16(ctx).unwrap(), 0);
+ assert_eq!(f(x).to_int16(ctx).unwrap(), 0);
+ }
+ assert_eq!(f(2f64.powi(53) + 2.0).to_uint8(ctx).unwrap(), 2);
+
+ // boundaries
+ assert_eq!(f(256.0).to_uint8(ctx).unwrap(), 0);
+ assert_eq!(f(-1.0).to_uint8(ctx).unwrap(), 255);
+ assert_eq!(f(128.0).to_int8(ctx).unwrap(), -128);
+ assert_eq!(f(-129.0).to_int8(ctx).unwrap(), 127);
+ assert_eq!(f(65536.0).to_uint16(ctx).unwrap(), 0);
+ assert_eq!(f(32768.0).to_int16(ctx).unwrap(), -32768);
+
+ // non-finite
+ for x in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
+ assert_eq!(f(x).to_uint8(ctx).unwrap(), 0);
+ assert_eq!(f(x).to_int8(ctx).unwrap(), 0);
+ assert_eq!(f(x).to_uint16(ctx).unwrap(), 0);
+ assert_eq!(f(x).to_int16(ctx).unwrap(), 0);
+ }
+ })]);
+}
+
#[test]
fn to_string() {
run_test_actions([TestAction::inspect_context(|ctx| {
From 229946383684afd67942ef0c7de9d257744ad4a8 Mon Sep 17 00:00:00 2001
From: Paraspandey-debugs
Date: Mon, 9 Nov 2026 13:30:44 +0530
Subject: [PATCH 2/2] fix(value): enhance numeric coercion tests for large and
boundary values in Uint8Array and Int8Array
---
core/engine/src/builtins/typed_array/tests.rs | 51 +++++++++++++++----
core/engine/src/value/tests.rs | 33 ++++++++++--
2 files changed, 69 insertions(+), 15 deletions(-)
diff --git a/core/engine/src/builtins/typed_array/tests.rs b/core/engine/src/builtins/typed_array/tests.rs
index 45edce286c9..169dd1e7d7c 100644
--- a/core/engine/src/builtins/typed_array/tests.rs
+++ b/core/engine/src/builtins/typed_array/tests.rs
@@ -57,17 +57,46 @@ fn uint8array_numeric_coercion() {
#[test]
fn typed_array_large_values_numeric_coercion() {
run_test_actions([
- TestAction::run("let a = new Uint8Array([1e40, 2**63, 1e20, 2**53 + 2]);"),
- TestAction::assert_eq("a[0]", 0),
- TestAction::assert_eq("a[1]", 0),
- TestAction::assert_eq("a[2]", 0),
- TestAction::assert_eq("a[3]", 2),
- TestAction::run("let b = new Int8Array([1e40]);"),
- TestAction::assert_eq("b[0]", 0),
- TestAction::run("let c = new Uint16Array([1e40]);"),
- TestAction::assert_eq("c[0]", 0),
- TestAction::run("let d = new Int16Array([1e40]);"),
- TestAction::assert_eq("d[0]", 0),
+ // --- Uint8Array ---
+ // Large values that used to saturate at 255; must all wrap to 0
+ TestAction::run("let a = new Uint8Array([1e40, 2**63, 1e20, -(2**63), -1e20, 2**53 + 2]);"),
+ TestAction::assert_eq("a[0]", 0), // 1e40 → 0
+ TestAction::assert_eq("a[1]", 0), // 2^63 → 0
+ TestAction::assert_eq("a[2]", 0), // 1e20 → 0
+ TestAction::assert_eq("a[3]", 0), // -(2^63) → 0
+ TestAction::assert_eq("a[4]", 0), // -1e20 → 0
+ TestAction::assert_eq("a[5]", 2), // 2^53+2: lower 8 bits → 2
+ // Large *negative* float with non-zero lower bits: -(2^53+2) → 0xFFFFFFFE mod 256 = 254
+ TestAction::run("let a2 = new Uint8Array([-(2**53 + 2)]);"),
+ TestAction::assert_eq("a2[0]", 254),
+ // 2^32+1 wraps modulo 2^32 → 1
+ TestAction::run("let a3 = new Uint8Array([2**32 + 1]);"),
+ TestAction::assert_eq("a3[0]", 1),
+ // Boundary: 256 wraps to 0, -1 wraps to 255
+ TestAction::run("let a4 = new Uint8Array([256, -1]);"),
+ TestAction::assert_eq("a4[0]", 0),
+ TestAction::assert_eq("a4[1]", 255),
+ // --- Int8Array ---
+ TestAction::run("let b = new Int8Array([1e40, -(2**53 + 2), 2**32 + 1, 128, -129]);"),
+ TestAction::assert_eq("b[0]", 0), // 1e40 → 0
+ TestAction::assert_eq("b[1]", -2), // -(2^53+2) → 0xFE → -2 as i8
+ TestAction::assert_eq("b[2]", 1), // 2^32+1 → 1
+ TestAction::assert_eq("b[3]", -128), // 128 → -128 (wraps)
+ TestAction::assert_eq("b[4]", 127), // -129 → 127 (wraps)
+ // --- Uint16Array ---
+ TestAction::run("let c = new Uint16Array([1e40, -(2**53 + 2), 2**32 + 1, 65536, -1]);"),
+ TestAction::assert_eq("c[0]", 0), // 1e40 → 0
+ TestAction::assert_eq("c[1]", 65534), // -(2^53+2) → 0xFFFFFFFE mod 65536 = 65534
+ TestAction::assert_eq("c[2]", 1), // 2^32+1 → 1
+ TestAction::assert_eq("c[3]", 0), // 65536 → 0 (wraps)
+ TestAction::assert_eq("c[4]", 65535), // -1 → 65535
+ // --- Int16Array ---
+ TestAction::run("let d = new Int16Array([1e40, -(2**53 + 2), 2**32 + 1, 32768, -32769]);"),
+ TestAction::assert_eq("d[0]", 0), // 1e40 → 0
+ TestAction::assert_eq("d[1]", -2), // -(2^53+2) → 0xFFFE → -2 as i16
+ TestAction::assert_eq("d[2]", 1), // 2^32+1 → 1
+ TestAction::assert_eq("d[3]", -32768), // 32768 → -32768 (wraps)
+ TestAction::assert_eq("d[4]", 32767), // -32769 → 32767 (wraps)
]);
}
diff --git a/core/engine/src/value/tests.rs b/core/engine/src/value/tests.rs
index fbd14291605..020ca988015 100644
--- a/core/engine/src/value/tests.rs
+++ b/core/engine/src/value/tests.rs
@@ -728,24 +728,49 @@ fn to_small_ints_large_values() {
run_test_actions([TestAction::inspect_context(|ctx| {
let f = JsValue::new;
- // |x| >= 2^63 used to saturate
+ // |x| >= 2^63 used to saturate before the fix; must all yield 0
for x in [1e40, 2f64.powi(63), 1e20, -1e20] {
assert_eq!(f(x).to_uint8(ctx).unwrap(), 0);
assert_eq!(f(x).to_int8(ctx).unwrap(), 0);
assert_eq!(f(x).to_uint16(ctx).unwrap(), 0);
assert_eq!(f(x).to_int16(ctx).unwrap(), 0);
}
- assert_eq!(f(2f64.powi(53) + 2.0).to_uint8(ctx).unwrap(), 2);
- // boundaries
+ // 2^53 + 2: a large float whose lower bits are non-zero — positive case
+ let large_pos = 2f64.powi(53) + 2.0;
+ assert_eq!(f(large_pos).to_uint8(ctx).unwrap(), 2);
+ assert_eq!(f(large_pos).to_int8(ctx).unwrap(), 2);
+ assert_eq!(f(large_pos).to_uint16(ctx).unwrap(), 2);
+ assert_eq!(f(large_pos).to_int16(ctx).unwrap(), 2);
+
+ // -(2^53 + 2): large *negative* float with non-zero lower bits.
+ // Two's-complement wrap: modulo 2^32 gives 0xFFFF_FFFE → lower bytes 0xFE / 0xFFFE.
+ let large_neg = -(2f64.powi(53) + 2.0);
+ assert_eq!(f(large_neg).to_uint8(ctx).unwrap(), 254_u8);
+ assert_eq!(f(large_neg).to_int8(ctx).unwrap(), -2_i8);
+ assert_eq!(f(large_neg).to_uint16(ctx).unwrap(), 65534_u16);
+ assert_eq!(f(large_neg).to_int16(ctx).unwrap(), -2_i16);
+
+ // 2^32 + 1: wraps modulo 2^32 → 1 in all narrower types
+ let wrap32 = 2f64.powi(32) + 1.0;
+ assert_eq!(f(wrap32).to_uint8(ctx).unwrap(), 1);
+ assert_eq!(f(wrap32).to_int8(ctx).unwrap(), 1);
+ assert_eq!(f(wrap32).to_uint16(ctx).unwrap(), 1);
+ assert_eq!(f(wrap32).to_int16(ctx).unwrap(), 1);
+
+ // boundaries — u8 / i8
assert_eq!(f(256.0).to_uint8(ctx).unwrap(), 0);
assert_eq!(f(-1.0).to_uint8(ctx).unwrap(), 255);
assert_eq!(f(128.0).to_int8(ctx).unwrap(), -128);
assert_eq!(f(-129.0).to_int8(ctx).unwrap(), 127);
+
+ // boundaries — u16 / i16
assert_eq!(f(65536.0).to_uint16(ctx).unwrap(), 0);
+ assert_eq!(f(-1.0).to_uint16(ctx).unwrap(), 65535);
assert_eq!(f(32768.0).to_int16(ctx).unwrap(), -32768);
+ assert_eq!(f(-32769.0).to_int16(ctx).unwrap(), 32767);
- // non-finite
+ // non-finite inputs must yield 0 for all narrow types
for x in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
assert_eq!(f(x).to_uint8(ctx).unwrap(), 0);
assert_eq!(f(x).to_int8(ctx).unwrap(), 0);