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);