CVE-2023-28081: Use After Free (CWE-416)
value: A bytecode optimization bug in Hermes prior to commit e6ed9c1a4b02dc219de1648f44cd808a56171b81 could be used to cause an use-after-free and obtain arbitrary code execution via a carefully crafted payload. 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 reuses or references memory after it has been freed. At some point afterward, the memory may be allocated again and saved in another pointer, while the original pointer references a location somewhere within the new allocation. Any operations using the original pointer are no longer valid because the memory "belongs" to the code that operates on the new pointer.
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