CVE-2023-30470: Use After Free (CWE-416)
value: A use-after-free related to unsound inference in the bytecode generation when optimizations are enabled for Hermes prior to commit da8990f737ebb9d9810633502f65ed462b819c09 could have been used by an attacker to achieve remote code execution. 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: lib/Optimizer/Scalar/SimpleCallGraphProvider.cpp, test/Optimizer/non-strict-opts.js, test/hermes/regress-arguments-callee.js
Commit date: 2023-04-07 13:31:05 -0700
Commit message: Treat usages of arguments.callee as unknown callsites

Summary:
arguments.callee in non-strict mode are indirect references to the current function. As such,
make sure that call site inference bails if there's any possibility such a reference is made. This
is inexact by nature, so the analysis needs to be conservative.

nocommit until a solution is decided.

Reviewed By: neildhar

Differential Revision: D44674327

fbshipit-source-id: c118b23d91ffe872e65dea0560b3c622802c2eef

Diff: diff --git a/lib/Optimizer/Scalar/SimpleCallGraphProvider.cpp b/lib/Optimizer/Scalar/SimpleCallGraphProvider.cpp
index 597df62519..189f2a9c63 100644
--- a/lib/Optimizer/Scalar/SimpleCallGraphProvider.cpp
+++ b/lib/Optimizer/Scalar/SimpleCallGraphProvider.cpp
@@ -64,10 +64,17 @@ static bool identifyCallees(CallInst *CI, llvh::DenseSet<Function *> &callees) {
 /// Auxiliary method to figure out the call sites at which F may be
 /// invoked.  Returns true if the complete set of call sites is known.
 static bool identifyCallsites(
     Function *F,
     llvh::DenseSet<CallInst *> &callSites) {
+  // Non-strict functions can be aliased in multiple ways (
+  // arguments.callee, Function.prototype.caller etc). Do not try to identify
+  // the callsites for those.
+  if (!F->isStrictMode()) {
+    return false;
+  }
+
   for (auto *CU : F->getUsers()) {
     if (auto *CI = llvh::dyn_cast<CallInst>(CU)) {
       if (!isDirectCallee(F, CI))
         return false;
       callSites.insert(CI);
diff --git a/test/Optimizer/non-strict-opts.js b/test/Optimizer/non-strict-opts.js
index 76080e56ca..90bcf798a3 100644
--- a/test/Optimizer/non-strict-opts.js
+++ b/test/Optimizer/non-strict-opts.js
@@ -37,11 +37,11 @@ function main()  {
 // CHECK-NEXT:  %1 = CreateFunctionInst %foo#1#2()#3 : string, %0
 // CHECK-NEXT:  %2 = CallInst %1 : closure, undefined : undefined, 2 : number
 // CHECK-NEXT:  %3 = ReturnInst undefined : undefined
 // CHECK-NEXT:function_end
 
-// CHECK:function foo#1#2(p1 : number)#3 : string
+// CHECK:function foo#1#2(p1)#3 : string
 // CHECK-NEXT:S{foo#1#2()#3} = []
 // CHECK-NEXT:%BB0:
 // CHECK-NEXT:  %0 = CreateScopeInst %S{foo#1#2()#3}
 // CHECK-NEXT:  %1 = BinaryOperatorInst '+', "value" : string, 2 : number
 // CHECK-NEXT:  %2 = ReturnInst %1 : string
diff --git a/test/hermes/regress-arguments-callee.js b/test/hermes/regress-arguments-callee.js
new file mode 100644
index 0000000000..34f1afd4e2
--- /dev/null
+++ b/test/hermes/regress-arguments-callee.js
@@ -0,0 +1,133 @@
+/**
+ * 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 %s
+
+var f;
+var funcp;
+var argumentsp;
+var callee = _ => "callee";
+
+function calleeDirectly(a) {
+    return a.callee;
+}
+
+function calleeIndirectly(a) {
+    return a[callee()];
+}
+
+print("directArgumentsCallee");
+(function directArgumentsCallee(param) {
+    try {
+        funcp = arguments.callee;
+    } catch (e) {
+    }
+    let f = () => {
+        return param;
+    };
+    return f;
+})();
+f = funcp({ a: 1.1 });
+f();
+
+print("indirectArgumentsCallee");
+(function indirectArgumentsCallee(param) {
+    try {
+        funcp = arguments.callee;
+    } catch (e) {
+    }
+    let f = () => {
+        return param;
+    };
+    return f;
+})();
+f = funcp({ a: 1.1 });
+f();
+
+print("passignArgumentsToAnotherFunction_directArgumentsCallee");
+(function passignArgumentsToAnotherFunction_DirectArgumentsCallee(param) {
+    try {
+        funcp = calleeDirectly(arguments);
+    } catch (e) {
+    }
+    let f = () => {
+        return param;
+    };
+    return f;
+})();
+f = funcp({ a: 1.1 });
+f();
+
+print("passignArgumentsToAnotherFunction_indirectArgumentsCallee");
+(function passignArgumentsToAnotherFunction_indirectArgumentsCallee(param) {
+    try {
+        funcp = calleeIndirectly(arguments);
+    } catch (e) {
+    }
+    let f = () => {
+        return param;
+    };
+    return f;
+})();
+f = funcp({ a: 1.1 });
+f();
+
+print("arrow_directArgumentsCallee");
+(function arrow_directArgumentsCallee(param) {
+    try {
+        funcp = (_ => arguments.callee)();
+    } catch (e) {
+    }
+    let f = () => {
+        return param;
+    };
+    return f;
+})();
+f = funcp({ a: 1.1 });
+f();
+
+print("arrow_indirectArgumentsCallee");
+(function arrow_indirectArgumentsCallee(param) {
+    try {
+        funcp = (_ => arguments[callee()])();
+    } catch (e) {
+    }
+    let f = () => {
+        return param;
+    };
+    return f;
+})();
+f = funcp({ a: 1.1 });
+f();
+
+print("capture_directArgumentsCallee");
+(function capture_directArgumentsCallee(param) {
+    try {
+        argumentsp = arguments;
+    } catch (e) {
+    }
+    let f = () => {
+        return param;
+    };
+    return f;
+})();
+f = argumentsp.callee({ a: 1.1 });
+f();
+
+print("capture_indirectArgumentsCallee");
+(function capture_indirectArgumentsCallee(param) {
+    try {
+        argumentsp = arguments;
+    } catch (e) {
+    }
+    let f = () => {
+        return param;
+    };
+    return f;
+})();
+f = argumentsp[callee()]({ a: 1.1 });
+f();

Vulnerable code: // CHECK:function foo#1#2(p1 : number)#3 : string
Fixed code:   // Non-strict functions can be aliased in multiple ways (
  // arguments.callee, Function.prototype.caller etc). Do not try to identify
  // the callsites for those.
  if (!F->isStrictMode()) {
    return false;
  }

// CHECK:function foo#1#2(p1)#3 : string
/**
 * 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 %s

var f;
var funcp;
var argumentsp;
var callee = _ => "callee";

function calleeDirectly(a) {
    return a.callee;
}

function calleeIndirectly(a) {
    return a[callee()];
}

print("directArgumentsCallee");
(function directArgumentsCallee(param) {
    try {
        funcp = arguments.callee;
    } catch (e) {
    }
    let f = () => {
        return param;
    };
    return f;
})();
f = funcp({ a: 1.1 });
f();

print("indirectArgumentsCallee");
(function indirectArgumentsCallee(param) {
    try {
        funcp = arguments.callee;
    } catch (e) {
    }
    let f = () => {
        return param;
    };
    return f;
})();
f = funcp({ a: 1.1 });
f();

print("passignArgumentsToAnotherFunction_directArgumentsCallee");
(function passignArgumentsToAnotherFunction_DirectArgumentsCallee(param) {
    try {
        funcp = calleeDirectly(arguments);
    } catch (e) {
    }
    let f = () => {
        return param;
    };
    return f;
})();
f = funcp({ a: 1.1 });
f();

print("passignArgumentsToAnotherFunction_indirectArgumentsCallee");
(function passignArgumentsToAnotherFunction_indirectArgumentsCallee(param) {
    try {
        funcp = calleeIndirectly(arguments);
    } catch (e) {
    }
    let f = () => {
        return param;
    };
    return f;
})();
f = funcp({ a: 1.1 });
f();

print("arrow_directArgumentsCallee");
(function arrow_directArgumentsCallee(param) {
    try {
        funcp = (_ => arguments.callee)();
    } catch (e) {
    }
    let f = () => {
        return param;
    };
    return f;
})();
f = funcp({ a: 1.1 });
f();

print("arrow_indirectArgumentsCallee");
(function arrow_indirectArgumentsCallee(param) {
    try {
        funcp = (_ => arguments[callee()])();
    } catch (e) {
    }
    let f = () => {
        return param;
    };
    return f;
})();
f = funcp({ a: 1.1 });
f();

print("capture_directArgumentsCallee");
(function capture_directArgumentsCallee(param) {
    try {
        argumentsp = arguments;
    } catch (e) {
    }
    let f = () => {
        return param;
    };
    return f;
})();
f = argumentsp.callee({ a: 1.1 });
f();

print("capture_indirectArgumentsCallee");
(function capture_indirectArgumentsCallee(param) {
    try {
        argumentsp = arguments;
    } catch (e) {
    }
    let f = () => {
        return param;
    };
    return f;
})();
f = argumentsp[callee()]({ a: 1.1 });
f();