Skip to content
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

fix(es/minifier): Fix panic in bitwise logic and incorrect values #9258

Merged
merged 10 commits into from
Jul 16, 2024
58 changes: 22 additions & 36 deletions crates/swc_ecma_transforms_optimization/src/simplify/expr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ use swc_ecma_transforms_base::{
perf::{cpu_count, Parallel, ParallelExt},
};
use swc_ecma_utils::{
is_literal, prop_name_eq, to_int32, BoolType, ExprCtx, ExprExt, NullType, NumberType,
ObjectType, StringType, SymbolType, UndefinedType, Value,
is_literal,
number::{ToJsInt32, ToJsUint32},
prop_name_eq, to_int32, BoolType, ExprCtx, ExprExt, NullType, NumberType, ObjectType,
StringType, SymbolType, UndefinedType, Value,
};
use swc_ecma_visit::{as_folder, standard_only_visit_mut, VisitMut, VisitMutWith};
use Value::{Known, Unknown};
Expand Down Expand Up @@ -633,13 +635,6 @@ impl SimplifyExpr {

// Bit shift operations
op!("<<") | op!(">>") | op!(">>>") => {
/// Uses a method for treating a double as 32bits that is
/// equivalent to how JavaScript would convert a
/// number before applying a bit operation.
fn js_convert_double_to_bits(d: f64) -> i32 {
((d.floor() as i64) & 0xffff_ffff) as i32
}

fn try_fold_shift(
ctx: &ExprCtx,
op: BinaryOp,
Expand All @@ -655,36 +650,27 @@ impl SimplifyExpr {
_ => unreachable!(),
};

// only the lower 5 bits are used when shifting, so don't do anything
// if the shift amount is outside [0,32)
if !(0.0..32.0).contains(&rv) {
return Unknown;
}

let rv_int = rv as i32;
if rv_int as f64 != rv {
unimplemented!("error reporting: FRACTIONAL_BITWISE_OPERAND")
// report(FRACTIONAL_BITWISE_OPERAND, right.span());
// return n;
}

if lv.floor() != lv {
unimplemented!("error reporting: FRACTIONAL_BITWISE_OPERAND")
// report(FRACTIONAL_BITWISE_OPERAND, left.span());
// return n;
}
Known(match op {
op!("<<") => {
// https://262.ecma-international.org/5.1/#sec-11.7.1
let lnum = lv.to_js_int32();
let rnum = rv.to_js_uint32();

let bits = js_convert_double_to_bits(lv);
(lnum << (rnum & 0x1f)) as f64
}
op!(">>") => {
// https://262.ecma-international.org/5.1/#sec-11.7.2
let lnum = lv.to_js_int32();
let rnum = rv.to_js_uint32();

Known(match op {
op!("<<") => (bits << rv_int) as f64,
op!(">>") => (bits >> rv_int) as f64,
(lnum >> (rnum & 0x1f)) as f64
}
op!(">>>") => {
let res = bits as u32 >> rv_int as u32;
// JavaScript always treats the result of >>> as unsigned.
// We must force Java to do the same here.
// unimplemented!(">>> (Zerofill rshift)")
res as f64
// https://262.ecma-international.org/5.1/#sec-11.7.3
let lnum = lv.to_js_uint32();
let rnum = rv.to_js_uint32();

(lnum >> (rnum & 0x1f)) as f64
}

_ => unreachable!("Unknown bit operator {:?}", op),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,27 @@ fn test_fold_bitwise_op2() {
fold("x = 12 | NaN", "x=12");
}

#[test]
fn test_issue_9256() {
// Returns -2 prior to fix (Number.MAX_VALUE)
fold("1.7976931348623157e+308 << 1", "0");

// Isn't changed prior to fix
fold("1.7976931348623157e+308 << 1.7976931348623157e+308", "0");
fold("1.7976931348623157e+308 >> 1.7976931348623157e+308", "0");

// Panics prior to fix (Number.MIN_VALUE)
fold("5e-324 >> 5e-324", "0");
fold("5e-324 << 5e-324", "0");
fold("5e-324 << 0", "0");
fold("0 << 5e-324", "0");

// Wasn't broken prior, used to ensure overflows are handled correctly
fold("1 << 31", "-2147483648");
fold("-8 >> 2", "-2");
fold("-8 >>> 2", "1073741822");
}

#[test]
#[ignore]
fn test_folding_mix_types_early() {
Expand Down
93 changes: 93 additions & 0 deletions crates/swc_ecma_utils/src/number.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,96 @@ impl ToJsString for f64 {
buffer.format(*self).to_string()
}
}

/// Implements [ToInt32] as defined in ECMAScript Section 9.5.
///
/// [ToInt32]: https://262.ecma-international.org/5.1/#sec-9.5
pub trait ToJsInt32 {
/// Converts `self` into a signed 32-bit integer as defined in
/// ECMAScript Section 9.5, [ToInt32].
///
/// [ToInt32]: https://262.ecma-international.org/5.1/#sec-9.5
fn to_js_int32(&self) -> i32;
}

impl ToJsInt32 for f64 {
fn to_js_int32(&self) -> i32 {
if self.is_nan() || self.is_infinite() || *self == 0.0 {
return 0;
}

const TWO_32: f64 = u32::MAX as f64 + 1.0; // 2^32
const TWO_31: f64 = i32::MAX as f64 + 1.0; // 2^31

let pos_int = self.signum() * self.abs().floor();
let int32_bit = pos_int % TWO_32;

(if int32_bit >= TWO_31 {
int32_bit - TWO_32
} else {
int32_bit
}) as i32
}
}

/// Implements [ToUint32] as defined in ECMAScript Section 9.6.
///
/// [ToUint32]: https://262.ecma-international.org/5.1/#sec-9.6
pub trait ToJsUint32 {
/// Converts `self` into an unsigned 32-bit integer as defined in
/// ECMAScript Section 9.6, [ToUint32].
///
/// [ToUint32]: https://262.ecma-international.org/5.1/#sec-9.6
fn to_js_uint32(&self) -> u32;
}

impl ToJsUint32 for f64 {
fn to_js_uint32(&self) -> u32 {
if self.is_nan() || self.is_infinite() || *self == 0.0 {
return 0;
}

const TWO_32: f64 = u32::MAX as f64 + 1.0; // 2^32

let pos_int = self.signum() * self.abs().floor();
let result = pos_int % TWO_32;
// Extra step: since `x as u32` doesn't overflow, we have to add if result is
// negative
(if result < 0.0 {
result + TWO_32
} else {
result
}) as u32
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_to_js_int32() {
assert_eq!(f64::NAN.to_js_int32(), 0);
assert_eq!(f64::INFINITY.to_js_int32(), 0);
assert_eq!(f64::NEG_INFINITY.to_js_int32(), 0);
assert_eq!(0.0.to_js_int32(), 0);

assert_eq!(f64::MIN.to_js_int32(), 0);
assert_eq!(f64::MAX.to_js_int32(), 0);

assert_eq!(5.2.to_js_int32(), 5);
}

#[test]
fn test_to_js_uint32() {
assert_eq!(f64::NAN.to_js_uint32(), 0);
assert_eq!(f64::INFINITY.to_js_uint32(), 0);
assert_eq!(f64::NEG_INFINITY.to_js_uint32(), 0);
assert_eq!(0.0.to_js_uint32(), 0);

assert_eq!(f64::MIN.to_js_uint32(), 0);
assert_eq!(f64::MAX.to_js_uint32(), 0);

assert_eq!((-8.0).to_js_uint32(), 4294967288);
}
}
Loading