CVE-2023-25933: Access of Resource Using Incompatible Type ('Type Confusion') (CWE-843)
value: A type confusion bug in TypedArray prior to commit e6ed9c1a4b02dc219de1648f44cd808a56171b81 could have been used by a malicious attacker to execute arbitrary code via untrusted JavaScript. Note that this is only exploitable in cases where Hermes is used to execute untrusted JavaScript. Hence, most React Native applications are not affected.
CWE description: The product allocates or initializes a resource such as a pointer, object, or variable using one type, but it later accesses that resource using a type that is incompatible with the original type.
CVSS v3 base score: 9.8
Published: 2023-05-18T22:15Z
Repository: https://github.com/facebook/hermes
Language: JavaScript
Files changed: include/hermes/VM/HermesValue.h, lib/Optimizer/Scalar/TypeInference.cpp, lib/VM/JSLib/TypedArray.cpp, test/BCGen/HBC/arguments.js, test/Optimizer/type_infer.js, test/hermes/regress-bigint-type-inference.js, test/hermes/regress-typed-array-at-nan.js
Commit date: 2023-04-05 06:59:33 -0700
Commit message: Re-sync with internal repository (#960)

Co-authored-by: Facebook Community Bot <6422482+facebook-github-bot@users.noreply.github.com>
Diff: diff --git a/include/hermes/VM/HermesValue.h b/include/hermes/VM/HermesValue.h
index a1a617ba90..89fdd17de6 100644
--- a/include/hermes/VM/HermesValue.h
+++ b/include/hermes/VM/HermesValue.h
@@ -351,10 +351,30 @@ class HermesValue {
       encodeNumberValue(T num) {
     assert((double)num == num && "value not representable as double");
     return encodeDoubleValue((double)num);
   }
 
+  /// Encode a numeric value into the best possible representation based on the
+  /// static type of the parameter. Right now we only have one representation
+  /// (double), but that could change in the future. It cannot be guaranteed
+  /// that the NaN bits are all zeroes.
+  inline static HermesValue encodeUntrustedNumberValue(double num) {
+    return encodeUntrustedDoubleValue(num);
+  }
+
+  /// Encode a numeric value into the best possible representation based on the
+  /// static type of the parameter. Right now we only have one representation
+  /// (double), but that could change in the future. It cannot be guaranteed
+  /// that the NaN bits are all zeroes.
+  template <typename T>
+  inline static
+      typename std::enable_if<std::is_integral<T>::value, HermesValue>::type
+      encodeUntrustedNumberValue(T num) {
+    assert((double)num == num && "value not representable as double");
+    return encodeUntrustedDoubleValue((double)num);
+  }
+
   static HermesValue encodeNaNValue() {
     return HermesValue(
         llvh::DoubleToBits(std::numeric_limits<double>::quiet_NaN()));
   }
 
diff --git a/lib/Optimizer/Scalar/TypeInference.cpp b/lib/Optimizer/Scalar/TypeInference.cpp
index 6e1e79a27c..267a308c30 100644
--- a/lib/Optimizer/Scalar/TypeInference.cpp
+++ b/lib/Optimizer/Scalar/TypeInference.cpp
@@ -58,10 +58,16 @@ class TypeInferenceImpl {
 
  public:
   bool runOnModule(Module *M);
 };
 
+/// \return if the given \p type is a BigInt|Object, which used to determine if
+/// unary/binary operations may have a BigInt result.
+static bool isBigIntOrObject(Type type) {
+  return type.canBeBigInt() || type.canBeObject();
+}
+
 static bool inferUnaryArith(UnaryOperatorInst *UOI, Type numberResultType) {
   Value *op = UOI->getSingleOperand();
 
   if (op->getType().isNumberType()) {
     UOI->setType(numberResultType);
@@ -71,14 +77,15 @@ static bool inferUnaryArith(UnaryOperatorInst *UOI, Type numberResultType) {
   if (op->getType().isBigIntType()) {
     UOI->setType(Type::createBigInt());
     return true;
   }
 
-  Type mayBeBigInt =
-      op->getType().canBeBigInt() ? Type::createBigInt() : Type::createNoType();
+  Type mayBeBigInt = isBigIntOrObject(op->getType()) ? Type::createBigInt()
+                                                     : Type::createNoType();
 
-  // - ?? => Number|?BigInt. BigInt is only possible if op.Type canBeBigInt.
+  // - ?? => Number|?BigInt. BigInt is only possible if op.Type is
+  // BigInt|Object.
   UOI->setType(Type::unionTy(numberResultType, mayBeBigInt));
   return true;
 }
 
 static bool inferUnaryArithDefault(UnaryOperatorInst *UOI) {
@@ -296,30 +303,30 @@ static bool inferBinaryArith(
   if (LeftTy.isBigIntType() && RightTy.isBigIntType()) {
     BOI->setType(Type::createBigInt());
     return true;
   }
 
-  Type mayBeBigInt = LeftTy.canBeBigInt() && RightTy.canBeBigInt()
+  Type mayBeBigInt = (isBigIntOrObject(LeftTy) && isBigIntOrObject(RightTy))
       ? Type::createBigInt()
       : Type::createNoType();
 
-  // ?? - ?? => Number|?BigInt. BigInt is only possible if both operands can be
-  // BigInt due to the no automatic BigInt conversion.
+  // ?? - ?? => Number|?BigInt. BigInt is only possible if both operands are
+  // BigInt|Object due to the no automatic BigInt conversion.
   BOI->setType(Type::unionTy(numberType, mayBeBigInt));
   return true;
 }
 
 static bool inferBinaryBitwise(BinaryOperatorInst *BOI) {
   Type LeftTy = BOI->getLeftHandSide()->getType();
   Type RightTy = BOI->getRightHandSide()->getType();
 
-  Type mayBeBigInt = LeftTy.canBeBigInt() && RightTy.canBeBigInt()
+  Type mayBeBigInt = (isBigIntOrObject(LeftTy) && isBigIntOrObject(RightTy))
       ? Type::createBigInt()
       : Type::createNoType();
 
-  // ?? - ?? => Int32|?BigInt. BigInt is only possible if both operands can be
-  // BigInt due to the no automatic BigInt conversion.
+  // ?? - ?? => Int32|?BigInt. BigInt is only possible if both operands are
+  // BigInt|Object due to the no automatic BigInt conversion.
   BOI->setType(Type::unionTy(Type::createInt32(), mayBeBigInt));
   return true;
 }
 } // anonymous namespace
 
@@ -389,11 +396,11 @@ static bool inferBinaryInst(BinaryOperatorInst *BOI) {
       }
 
       // ?BigInt + ?BigInt => ?BigInt. Both operands need to "may be a BigInt"
       // for a possible BigInt result from this operator. This is true because
       // there's no automative BigInt type conversion.
-      Type mayBeBigInt = (LeftTy.canBeBigInt() && RightTy.canBeBigInt())
+      Type mayBeBigInt = (isBigIntOrObject(LeftTy) && isBigIntOrObject(RightTy))
           ? Type::createBigInt()
           : Type::createNoType();
 
       // handy alias for number|maybe(BigInt).
       Type numeric = Type::unionTy(Type::createNumber(), mayBeBigInt);
diff --git a/lib/VM/JSLib/TypedArray.cpp b/lib/VM/JSLib/TypedArray.cpp
index 0ab09ecc3e..da47b942ea 100644
--- a/lib/VM/JSLib/TypedArray.cpp
+++ b/lib/VM/JSLib/TypedArray.cpp
@@ -815,11 +815,11 @@ typedArrayPrototypeAt(void *, Runtime &runtime, NativeArgs args) {
   case CellKind::name##ArrayKind: {                                        \
     auto *arr = vmcast<JSTypedArray<type, CellKind::name##ArrayKind>>(*O); \
     if (!arr->attached(runtime)) {                                         \
       return runtime.raiseTypeError("Underlying ArrayBuffer detached");    \
     }                                                                      \
-    return HermesValue::encodeNumberValue(arr->at(runtime, k));            \
+    return HermesValue::encodeUntrustedNumberValue(arr->at(runtime, k));   \
   }
   switch (O->getKind()) {
 #include "hermes/VM/TypedArrays.def"
     default:
       llvm_unreachable("Invalid TypedArray after ValidateTypedArray call");
diff --git a/test/BCGen/HBC/arguments.js b/test/BCGen/HBC/arguments.js
index 6a9b9ae452..2f6af8c956 100644
--- a/test/BCGen/HBC/arguments.js
+++ b/test/BCGen/HBC/arguments.js
@@ -41,11 +41,11 @@ function check_phi_handling(x) {
 // CHECK-NEXT:  $Reg2 @3 [empty]	%3 = StorePropertyInst %1 : closure, %2 : object, "count" : string
 // CHECK-NEXT:  $Reg2 @4 [5...6) 	%4 = HBCCreateFunctionInst %select#0#1()#3, %0
 // CHECK-NEXT:  $Reg2 @5 [empty]	%5 = StorePropertyInst %4 : closure, %2 : object, "select" : string
 // CHECK-NEXT:  $Reg2 @6 [7...8) 	%6 = HBCCreateFunctionInst %build#0#1()#4 : object, %0
 // CHECK-NEXT:  $Reg2 @7 [empty]	%7 = StorePropertyInst %6 : closure, %2 : object, "build" : string
-// CHECK-NEXT:  $Reg2 @8 [9...10) 	%8 = HBCCreateFunctionInst %buffalobuffalo#0#1()#5 : string|number, %0
+// CHECK-NEXT:  $Reg2 @8 [9...10) 	%8 = HBCCreateFunctionInst %buffalobuffalo#0#1()#5 : string|number|bigint, %0
 // CHECK-NEXT:  $Reg2 @9 [empty]	%9 = StorePropertyInst %8 : closure, %2 : object, "buffalobuffalo" : string
 // CHECK-NEXT:  $Reg0 @10 [11...12) 	%10 = HBCCreateFunctionInst %check_phi_handling#0#1()#6 : object, %0
 // CHECK-NEXT:  $Reg0 @11 [empty]	%11 = StorePropertyInst %10 : closure, %2 : object, "check_phi_handling" : string
 // CHECK-NEXT:  $Reg0 @12 [13...14) 	%12 = HBCLoadConstInst undefined : undefined
 // CHECK-NEXT:  $Reg0 @13 [empty]	%13 = ReturnInst %12 : undefined
@@ -83,20 +83,20 @@ function check_phi_handling(x) {
 // CHECK-NEXT:  $Reg1 @3 [empty]	%3 = HBCReifyArgumentsInst %0
 // CHECK-NEXT:  $Reg0 @4 [5...6) 	%4 = LoadStackInst %0
 // CHECK-NEXT:  $Reg0 @5 [empty]	%5 = ReturnInst %4
 // CHECK-NEXT:function_end
 
-// CHECK:function buffalobuffalo#0#1()#2 : string|number
+// CHECK:function buffalobuffalo#0#1()#2 : string|number|bigint
 // CHECK-NEXT:frame = []
 // CHECK-NEXT:%BB0:
 // CHECK-NEXT:  $Reg0 @0 [1...5) 	%0 = AllocStackInst $arguments
 // CHECK-NEXT:  $Reg1 @1 [2...3) 	%1 = HBCLoadConstInst undefined : undefined
 // CHECK-NEXT:  $Reg1 @2 [empty]	%2 = StoreStackInst %1 : undefined, %0
 // CHECK-NEXT:  $Reg1 @3 [empty]	%3 = HBCReifyArgumentsInst %0
 // CHECK-NEXT:  $Reg0 @4 [5...6) 	%4 = LoadStackInst %0
 // CHECK-NEXT:  $Reg0 @5 [6...7) 	%5 = BinaryOperatorInst '+', %4, %4
-// CHECK-NEXT:  $Reg0 @6 [empty]	%6 = ReturnInst %5 : string|number
+// CHECK-NEXT:  $Reg0 @6 [empty]	%6 = ReturnInst %5 : string|number|bigint
 // CHECK-NEXT:function_end
 
 // CHECK:function check_phi_handling#0#1(x)#2 : object
 // CHECK-NEXT:frame = []
 // CHECK-NEXT:%BB0:
diff --git a/test/Optimizer/type_infer.js b/test/Optimizer/type_infer.js
index 179d9833e5..c43ebc1473 100644
--- a/test/Optimizer/type_infer.js
+++ b/test/Optimizer/type_infer.js
@@ -90,11 +90,11 @@ function test_unary(x) {
 // CHECK-NEXT:  %16 = CallInst %x, undefined : undefined, %15 : string
 // CHECK-NEXT:  %17 = CallInst %x, undefined : undefined, 0 : number
 // CHECK-NEXT:  %18 = AllocObjectInst 0 : number, empty
 // CHECK-NEXT:  %19 = AllocObjectInst 0 : number, empty
 // CHECK-NEXT:  %20 = BinaryOperatorInst '+', %18 : object, %19 : object
-// CHECK-NEXT:  %21 = CallInst %x, undefined : undefined, %20 : string|number
+// CHECK-NEXT:  %21 = CallInst %x, undefined : undefined, %20 : string|number|bigint
 // CHECK-NEXT:  %22 = CallInst %x, undefined : undefined, NaN : number
 // CHECK-NEXT:  %23 = ReturnInst undefined : undefined
 // CHECK-NEXT:function_end
 
 // CHECK:function test_unary#0#1(x)#4 : undefined
diff --git a/test/hermes/regress-bigint-type-inference.js b/test/hermes/regress-bigint-type-inference.js
new file mode 100644
index 0000000000..fbe134d536
--- /dev/null
+++ b/test/hermes/regress-bigint-type-inference.js
@@ -0,0 +1,113 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+// RUN: %hermes -non-strict --dump-bytecode -O %s -fno-inline | %FileCheck %s --check-prefix=BC
+// RUN: %hermes -non-strict -O %s -fno-inline | %FileCheck %s --check-prefix=EXE
+
+function test() {
+    var o = { valueOf() { return 1n; } }
+    var bigint = 1n;
+    var result;
+
+    (() => result = o + bigint)();
+    print(result);
+    // BC: Add
+    // BC: StoreToEnvironment
+    // EXE: 2
+
+    (() => result = o - bigint)();
+    print(result);
+    // BC: Sub
+    // BC: StoreToEnvironment
+    // EXE: 0
+
+    (() => result = o * bigint)();
+    print(result);
+    // BC: Mul
+    // BC: StoreToEnvironment
+    // EXE: 1
+
+    (() => result = o / bigint)();
+    print(result);
+    // BC: Div
+    // BC: StoreToEnvironment
+    // EXE: 1
+
+    (() => result = o % bigint)();
+    print(result);
+    // BC: Mod
+    // BC: StoreToEnvironment
+    // EXE: 0
+
+    (() => result = o << bigint)();
+    print(result);
+    // BC: LShift
+    // BC: StoreToEnvironment
+    // EXE: 2
+
+    (() => result = o >> bigint)();
+    print(result);
+    // BC: RShift
+    // BC: StoreToEnvironment
+    // EXE: 0
+
+    (() => result = o | bigint)();
+    print(result);
+    // BC: BitOr
+    // BC: StoreToEnvironment
+    // EXE: 1
+
+    (() => result = o ^ bigint)();
+    print(result);
+    // BC: BitXor
+    // BC: StoreToEnvironment
+    // EXE: 0
+
+    (() => result = o & bigint)();
+    print(result);
+    // BC: BitAnd
+    // BC: StoreToEnvironment
+    // EXE: 1
+
+    (() => result = - bigint)();
+    print(result);
+    // BC: Negate
+    // BC: StoreToEnvironment
+    // EXE: -1
+
+    (() => result = ~ bigint)();
+    print(result);
+    // BC: BitNot
+    // BC: StoreToEnvironment
+    // EXE: -2
+
+    (() => result = o++)();
+    print(result);
+    // BC: Inc
+    // BC: StoreToEnvironment
+    // EXE: 1
+
+    (() => result = o--)();
+    print(result);
+    // BC: Dec
+    // BC: StoreToEnvironment
+    // EXE: 2
+
+    (() => result = ++o)();
+    print(result);
+    // BC: Inc
+    // BC: StoreToEnvironment
+    // EXE: 2
+
+    (() => result = --o)();
+    print(result);
+    // BC: Dec
+    // BC: StoreToEnvironment
+    // EXE: 1
+}
+
+test();
diff --git a/test/hermes/regress-typed-array-at-nan.js b/test/hermes/regress-typed-array-at-nan.js
new file mode 100644
index 0000000000..d3e5cee266
--- /dev/null
+++ b/test/hermes/regress-typed-array-at-nan.js
@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+// RUN: %hermes -O -target=HBC %s | %FileCheck --match-full-lines %s
+// RUN: %hermes -O -target=HBC -emit-binary -out %t.hbc %s && %hermes %t.hbc | %FileCheck --match-full-lines %s
+// Test typed array will check for NaN when reading from an index.
+
+print("typed-array-at-nan");
+//CHECK-LABEL: typed-array-at-nan
+let ab = new ArrayBuffer(8);
+let i32a = new Int32Array(ab);
+
+i32a[0] = 0xcafebabe;
+i32a[1] = 0xffff0000;
+
+let f32a = new Float64Array(ab);
+var shouldBeNan = f32a.at(0)
+print(shouldBeNan);
+// CHECK: NaN

Vulnerable code:   Type mayBeBigInt =
      op->getType().canBeBigInt() ? Type::createBigInt() : Type::createNoType();
  // - ?? => Number|?BigInt. BigInt is only possible if op.Type canBeBigInt.
  Type mayBeBigInt = LeftTy.canBeBigInt() && RightTy.canBeBigInt()
  // ?? - ?? => Number|?BigInt. BigInt is only possible if both operands can be
  // BigInt due to the no automatic BigInt conversion.
  Type mayBeBigInt = LeftTy.canBeBigInt() && RightTy.canBeBigInt()
  // ?? - ?? => Int32|?BigInt. BigInt is only possible if both operands can be
  // BigInt due to the no automatic BigInt conversion.
      Type mayBeBigInt = (LeftTy.canBeBigInt() && RightTy.canBeBigInt())
    return HermesValue::encodeNumberValue(arr->at(runtime, k));            \
// CHECK-NEXT:  $Reg2 @8 [9...10) 	%8 = HBCCreateFunctionInst %buffalobuffalo#0#1()#5 : string|number, %0
// CHECK:function buffalobuffalo#0#1()#2 : string|number
// CHECK-NEXT:  $Reg0 @6 [empty]	%6 = ReturnInst %5 : string|number
// CHECK-NEXT:  %21 = CallInst %x, undefined : undefined, %20 : string|number
Fixed code:   /// Encode a numeric value into the best possible representation based on the
  /// static type of the parameter. Right now we only have one representation
  /// (double), but that could change in the future. It cannot be guaranteed
  /// that the NaN bits are all zeroes.
  inline static HermesValue encodeUntrustedNumberValue(double num) {
    return encodeUntrustedDoubleValue(num);
  }

  /// Encode a numeric value into the best possible representation based on the
  /// static type of the parameter. Right now we only have one representation
  /// (double), but that could change in the future. It cannot be guaranteed
  /// that the NaN bits are all zeroes.
  template <typename T>
  inline static
      typename std::enable_if<std::is_integral<T>::value, HermesValue>::type
      encodeUntrustedNumberValue(T num) {
    assert((double)num == num && "value not representable as double");
    return encodeUntrustedDoubleValue((double)num);
  }

/// \return if the given \p type is a BigInt|Object, which used to determine if
/// unary/binary operations may have a BigInt result.
static bool isBigIntOrObject(Type type) {
  return type.canBeBigInt() || type.canBeObject();
}

  Type mayBeBigInt = isBigIntOrObject(op->getType()) ? Type::createBigInt()
                                                     : Type::createNoType();
  // - ?? => Number|?BigInt. BigInt is only possible if op.Type is
  // BigInt|Object.
  Type mayBeBigInt = (isBigIntOrObject(LeftTy) && isBigIntOrObject(RightTy))
  // ?? - ?? => Number|?BigInt. BigInt is only possible if both operands are
  // BigInt|Object due to the no automatic BigInt conversion.
  Type mayBeBigInt = (isBigIntOrObject(LeftTy) && isBigIntOrObject(RightTy))
  // ?? - ?? => Int32|?BigInt. BigInt is only possible if both operands are
  // BigInt|Object due to the no automatic BigInt conversion.
      Type mayBeBigInt = (isBigIntOrObject(LeftTy) && isBigIntOrObject(RightTy))
    return HermesValue::encodeUntrustedNumberValue(arr->at(runtime, k));   \
// CHECK-NEXT:  $Reg2 @8 [9...10) 	%8 = HBCCreateFunctionInst %buffalobuffalo#0#1()#5 : string|number|bigint, %0
// CHECK:function buffalobuffalo#0#1()#2 : string|number|bigint
// CHECK-NEXT:  $Reg0 @6 [empty]	%6 = ReturnInst %5 : string|number|bigint
// CHECK-NEXT:  %21 = CallInst %x, undefined : undefined, %20 : string|number|bigint
/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

// RUN: %hermes -non-strict --dump-bytecode -O %s -fno-inline | %FileCheck %s --check-prefix=BC
// RUN: %hermes -non-strict -O %s -fno-inline | %FileCheck %s --check-prefix=EXE

function test() {
    var o = { valueOf() { return 1n; } }
    var bigint = 1n;
    var result;

    (() => result = o + bigint)();
    print(result);
    // BC: Add
    // BC: StoreToEnvironment
    // EXE: 2

    (() => result = o - bigint)();
    print(result);
    // BC: Sub
    // BC: StoreToEnvironment
    // EXE: 0

    (() => result = o * bigint)();
    print(result);
    // BC: Mul
    // BC: StoreToEnvironment
    // EXE: 1

    (() => result = o / bigint)();
    print(result);
    // BC: Div
    // BC: StoreToEnvironment
    // EXE: 1

    (() => result = o % bigint)();
    print(result);
    // BC: Mod
    // BC: StoreToEnvironment
    // EXE: 0

    (() => result = o << bigint)();
    print(result);
    // BC: LShift
    // BC: StoreToEnvironment
    // EXE: 2

    (() => result = o >> bigint)();
    print(result);
    // BC: RShift
    // BC: StoreToEnvironment
    // EXE: 0

    (() => result = o | bigint)();
    print(result);
    // BC: BitOr
    // BC: StoreToEnvironment
    // EXE: 1

    (() => result = o ^ bigint)();
    print(result);
    // BC: BitXor
    // BC: StoreToEnvironment
    // EXE: 0

    (() => result = o & bigint)();
    print(result);
    // BC: BitAnd
    // BC: StoreToEnvironment
    // EXE: 1

    (() => result = - bigint)();
    print(result);
    // BC: Negate
    // BC: StoreToEnvironment
    // EXE: -1

    (() => result = ~ bigint)();
    print(result);
    // BC: BitNot
    // BC: StoreToEnvironment
    // EXE: -2

    (() => result = o++)();
    print(result);
    // BC: Inc
    // BC: StoreToEnvironment
    // EXE: 1

    (() => result = o--)();
    print(result);
    // BC: Dec
    // BC: StoreToEnvironment
    // EXE: 2

    (() => result = ++o)();
    print(result);
    // BC: Inc
    // BC: StoreToEnvironment
    // EXE: 2

    (() => result = --o)();
    print(result);
    // BC: Dec
    // BC: StoreToEnvironment
    // EXE: 1
}

test();
/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

// RUN: %hermes -O -target=HBC %s | %FileCheck --match-full-lines %s
// RUN: %hermes -O -target=HBC -emit-binary -out %t.hbc %s && %hermes %t.hbc | %FileCheck --match-full-lines %s
// Test typed array will check for NaN when reading from an index.

print("typed-array-at-nan");
//CHECK-LABEL: typed-array-at-nan
let ab = new ArrayBuffer(8);
let i32a = new Int32Array(ab);

i32a[0] = 0xcafebabe;
i32a[1] = 0xffff0000;

let f32a = new Float64Array(ab);
var shouldBeNan = f32a.at(0)
print(shouldBeNan);
// CHECK: NaN