mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Summary: Constructing a `JSError` currently catches all exceptions that occur, regardless of their type, and embeds the information from them in the newly created `JSError`. This means that the type of exceptions can be silently changed. This results in a bug if Hermes is built with `HERMESVM_EXCEPTION_ON_OOM` enabled. The OOM exception will get converted to a `JSError`, which in turn can get converted into a regular JS exception. The regular JS exception can then be caught by JS and ignored. This leads to the VM continuing to execute in a bad state. I considered three ways to fix this: 1. Add a new type of exception to JSI that is intentionally meant to be ignored, and the Hermes OOM exception then subclasses it. 2. Propagate all exceptions that happen when constructing `JSError`. 3. Propagate all exceptions except `JSIException`s when constructing `JSError`. The first is technically the most surgical, but it adds complexity to JSI, and will require some extra machinery to implement since we wouldn't be able to throw it directly from inside the VM. I'm also not comfortable with the idea of JSI suppressing exceptions that are completely unrelated to JSI. The second is the simplest, and seems to match what JS would do if `throw new Error()` itself threw, but there is some possibility that it is a change in behaviour for existing code. So the third approach, which is implemented in this diff, tries to compromise between the two. Hermes can only ever throw `JSIException`s from regular JS operations, so there should be no change for existing code (except code that uses exceptions on OOM, which is what we're trying to fix). Any other exception gets passed through to the caller. Changelog: [Internal][Fixed] - Fixed handling of Hermes OOM exceptions in JSI. Reviewed By: jpporto Differential Revision: D41831616 fbshipit-source-id: 42e0dde1c4acc016ab19533941c58fc1797ba1c2
529 lines
15 KiB
C++
529 lines
15 KiB
C++
/*
|
|
* 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.
|
|
*/
|
|
|
|
#include <cassert>
|
|
#include <cmath>
|
|
#include <cstdlib>
|
|
#include <stdexcept>
|
|
|
|
#include <jsi/instrumentation.h>
|
|
#include <jsi/jsi.h>
|
|
|
|
namespace facebook {
|
|
namespace jsi {
|
|
|
|
namespace {
|
|
|
|
// This is used for generating short exception strings.
|
|
std::string kindToString(const Value& v, Runtime* rt = nullptr) {
|
|
if (v.isUndefined()) {
|
|
return "undefined";
|
|
} else if (v.isNull()) {
|
|
return "null";
|
|
} else if (v.isBool()) {
|
|
return v.getBool() ? "true" : "false";
|
|
} else if (v.isNumber()) {
|
|
return "a number";
|
|
} else if (v.isString()) {
|
|
return "a string";
|
|
} else if (v.isSymbol()) {
|
|
return "a symbol";
|
|
} else if (v.isBigInt()) {
|
|
return "a bigint";
|
|
} else {
|
|
assert(v.isObject() && "Expecting object.");
|
|
return rt != nullptr && v.getObject(*rt).isFunction(*rt) ? "a function"
|
|
: "an object";
|
|
}
|
|
}
|
|
|
|
// getPropertyAsFunction() will try to create a JSError. If the
|
|
// failure is in building a JSError, this will lead to infinite
|
|
// recursion. This function is used in place of getPropertyAsFunction
|
|
// when building JSError, to avoid that infinite recursion.
|
|
Value callGlobalFunction(Runtime& runtime, const char* name, const Value& arg) {
|
|
Value v = runtime.global().getProperty(runtime, name);
|
|
if (!v.isObject()) {
|
|
throw JSINativeException(
|
|
std::string("callGlobalFunction: JS global property '") + name +
|
|
"' is " + kindToString(v, &runtime) + ", expected a Function");
|
|
}
|
|
Object o = v.getObject(runtime);
|
|
if (!o.isFunction(runtime)) {
|
|
throw JSINativeException(
|
|
std::string("callGlobalFunction: JS global property '") + name +
|
|
"' is a non-callable Object, expected a Function");
|
|
}
|
|
Function f = std::move(o).getFunction(runtime);
|
|
return f.call(runtime, arg);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Buffer::~Buffer() = default;
|
|
|
|
MutableBuffer::~MutableBuffer() = default;
|
|
|
|
PreparedJavaScript::~PreparedJavaScript() = default;
|
|
|
|
Value HostObject::get(Runtime&, const PropNameID&) {
|
|
return Value();
|
|
}
|
|
|
|
void HostObject::set(Runtime& rt, const PropNameID& name, const Value&) {
|
|
std::string msg("TypeError: Cannot assign to property '");
|
|
msg += name.utf8(rt);
|
|
msg += "' on HostObject with default setter";
|
|
throw JSError(rt, msg);
|
|
}
|
|
|
|
HostObject::~HostObject() {}
|
|
|
|
NativeState::~NativeState() {}
|
|
|
|
Runtime::~Runtime() {}
|
|
|
|
Instrumentation& Runtime::instrumentation() {
|
|
class NoInstrumentation : public Instrumentation {
|
|
std::string getRecordedGCStats() override {
|
|
return "";
|
|
}
|
|
|
|
std::unordered_map<std::string, int64_t> getHeapInfo(bool) override {
|
|
return std::unordered_map<std::string, int64_t>{};
|
|
}
|
|
|
|
void collectGarbage(std::string) override {}
|
|
|
|
void startTrackingHeapObjectStackTraces(
|
|
std::function<void(
|
|
uint64_t,
|
|
std::chrono::microseconds,
|
|
std::vector<HeapStatsUpdate>)>) override {}
|
|
void stopTrackingHeapObjectStackTraces() override {}
|
|
|
|
void startHeapSampling(size_t) override {}
|
|
void stopHeapSampling(std::ostream&) override {}
|
|
|
|
void createSnapshotToFile(const std::string&) override {
|
|
throw JSINativeException(
|
|
"Default instrumentation cannot create a heap snapshot");
|
|
}
|
|
|
|
void createSnapshotToStream(std::ostream&) override {
|
|
throw JSINativeException(
|
|
"Default instrumentation cannot create a heap snapshot");
|
|
}
|
|
|
|
std::string flushAndDisableBridgeTrafficTrace() override {
|
|
std::abort();
|
|
}
|
|
|
|
void writeBasicBlockProfileTraceToFile(const std::string&) const override {
|
|
std::abort();
|
|
}
|
|
|
|
void dumpProfilerSymbolsToFile(const std::string&) const override {
|
|
std::abort();
|
|
}
|
|
};
|
|
|
|
static NoInstrumentation sharedInstance;
|
|
return sharedInstance;
|
|
}
|
|
|
|
Value Runtime::createValueFromJsonUtf8(const uint8_t* json, size_t length) {
|
|
Function parseJson = global()
|
|
.getPropertyAsObject(*this, "JSON")
|
|
.getPropertyAsFunction(*this, "parse");
|
|
return parseJson.call(*this, String::createFromUtf8(*this, json, length));
|
|
}
|
|
|
|
Pointer& Pointer::operator=(Pointer&& other) {
|
|
if (ptr_) {
|
|
ptr_->invalidate();
|
|
}
|
|
ptr_ = other.ptr_;
|
|
other.ptr_ = nullptr;
|
|
return *this;
|
|
}
|
|
|
|
Object Object::getPropertyAsObject(Runtime& runtime, const char* name) const {
|
|
Value v = getProperty(runtime, name);
|
|
|
|
if (!v.isObject()) {
|
|
throw JSError(
|
|
runtime,
|
|
std::string("getPropertyAsObject: property '") + name + "' is " +
|
|
kindToString(v, &runtime) + ", expected an Object");
|
|
}
|
|
|
|
return v.getObject(runtime);
|
|
}
|
|
|
|
Function Object::getPropertyAsFunction(Runtime& runtime, const char* name)
|
|
const {
|
|
Object obj = getPropertyAsObject(runtime, name);
|
|
if (!obj.isFunction(runtime)) {
|
|
throw JSError(
|
|
runtime,
|
|
std::string("getPropertyAsFunction: property '") + name + "' is " +
|
|
kindToString(std::move(obj), &runtime) + ", expected a Function");
|
|
};
|
|
|
|
return std::move(obj).getFunction(runtime);
|
|
}
|
|
|
|
Array Object::asArray(Runtime& runtime) const& {
|
|
if (!isArray(runtime)) {
|
|
throw JSError(
|
|
runtime,
|
|
"Object is " + kindToString(Value(runtime, *this), &runtime) +
|
|
", expected an array");
|
|
}
|
|
return getArray(runtime);
|
|
}
|
|
|
|
Array Object::asArray(Runtime& runtime) && {
|
|
if (!isArray(runtime)) {
|
|
throw JSError(
|
|
runtime,
|
|
"Object is " + kindToString(Value(runtime, *this), &runtime) +
|
|
", expected an array");
|
|
}
|
|
return std::move(*this).getArray(runtime);
|
|
}
|
|
|
|
Function Object::asFunction(Runtime& runtime) const& {
|
|
if (!isFunction(runtime)) {
|
|
throw JSError(
|
|
runtime,
|
|
"Object is " + kindToString(Value(runtime, *this), &runtime) +
|
|
", expected a function");
|
|
}
|
|
return getFunction(runtime);
|
|
}
|
|
|
|
Function Object::asFunction(Runtime& runtime) && {
|
|
if (!isFunction(runtime)) {
|
|
throw JSError(
|
|
runtime,
|
|
"Object is " + kindToString(Value(runtime, *this), &runtime) +
|
|
", expected a function");
|
|
}
|
|
return std::move(*this).getFunction(runtime);
|
|
}
|
|
|
|
Value::Value(Value&& other) : Value(other.kind_) {
|
|
if (kind_ == BooleanKind) {
|
|
data_.boolean = other.data_.boolean;
|
|
} else if (kind_ == NumberKind) {
|
|
data_.number = other.data_.number;
|
|
} else if (kind_ >= PointerKind) {
|
|
new (&data_.pointer) Pointer(std::move(other.data_.pointer));
|
|
}
|
|
// when the other's dtor runs, nothing will happen.
|
|
other.kind_ = UndefinedKind;
|
|
}
|
|
|
|
Value::Value(Runtime& runtime, const Value& other) : Value(other.kind_) {
|
|
// data_ is uninitialized, so use placement new to create non-POD
|
|
// types in it. Any other kind of initialization will call a dtor
|
|
// first, which is incorrect.
|
|
if (kind_ == BooleanKind) {
|
|
data_.boolean = other.data_.boolean;
|
|
} else if (kind_ == NumberKind) {
|
|
data_.number = other.data_.number;
|
|
} else if (kind_ == SymbolKind) {
|
|
new (&data_.pointer) Pointer(runtime.cloneSymbol(other.data_.pointer.ptr_));
|
|
} else if (kind_ == BigIntKind) {
|
|
new (&data_.pointer) Pointer(runtime.cloneBigInt(other.data_.pointer.ptr_));
|
|
} else if (kind_ == StringKind) {
|
|
new (&data_.pointer) Pointer(runtime.cloneString(other.data_.pointer.ptr_));
|
|
} else if (kind_ >= ObjectKind) {
|
|
new (&data_.pointer) Pointer(runtime.cloneObject(other.data_.pointer.ptr_));
|
|
}
|
|
}
|
|
|
|
Value::~Value() {
|
|
if (kind_ >= PointerKind) {
|
|
data_.pointer.~Pointer();
|
|
}
|
|
}
|
|
|
|
bool Value::strictEquals(Runtime& runtime, const Value& a, const Value& b) {
|
|
if (a.kind_ != b.kind_) {
|
|
return false;
|
|
}
|
|
switch (a.kind_) {
|
|
case UndefinedKind:
|
|
case NullKind:
|
|
return true;
|
|
case BooleanKind:
|
|
return a.data_.boolean == b.data_.boolean;
|
|
case NumberKind:
|
|
return a.data_.number == b.data_.number;
|
|
case SymbolKind:
|
|
return runtime.strictEquals(
|
|
static_cast<const Symbol&>(a.data_.pointer),
|
|
static_cast<const Symbol&>(b.data_.pointer));
|
|
case BigIntKind:
|
|
return runtime.strictEquals(
|
|
static_cast<const BigInt&>(a.data_.pointer),
|
|
static_cast<const BigInt&>(b.data_.pointer));
|
|
case StringKind:
|
|
return runtime.strictEquals(
|
|
static_cast<const String&>(a.data_.pointer),
|
|
static_cast<const String&>(b.data_.pointer));
|
|
case ObjectKind:
|
|
return runtime.strictEquals(
|
|
static_cast<const Object&>(a.data_.pointer),
|
|
static_cast<const Object&>(b.data_.pointer));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Value::asBool() const {
|
|
if (!isBool()) {
|
|
throw JSINativeException(
|
|
"Value is " + kindToString(*this) + ", expected a boolean");
|
|
}
|
|
|
|
return getBool();
|
|
}
|
|
|
|
double Value::asNumber() const {
|
|
if (!isNumber()) {
|
|
throw JSINativeException(
|
|
"Value is " + kindToString(*this) + ", expected a number");
|
|
}
|
|
|
|
return getNumber();
|
|
}
|
|
|
|
Object Value::asObject(Runtime& rt) const& {
|
|
if (!isObject()) {
|
|
throw JSError(
|
|
rt, "Value is " + kindToString(*this, &rt) + ", expected an Object");
|
|
}
|
|
|
|
return getObject(rt);
|
|
}
|
|
|
|
Object Value::asObject(Runtime& rt) && {
|
|
if (!isObject()) {
|
|
throw JSError(
|
|
rt, "Value is " + kindToString(*this, &rt) + ", expected an Object");
|
|
}
|
|
auto ptr = data_.pointer.ptr_;
|
|
data_.pointer.ptr_ = nullptr;
|
|
return static_cast<Object>(ptr);
|
|
}
|
|
|
|
Symbol Value::asSymbol(Runtime& rt) const& {
|
|
if (!isSymbol()) {
|
|
throw JSError(
|
|
rt, "Value is " + kindToString(*this, &rt) + ", expected a Symbol");
|
|
}
|
|
|
|
return getSymbol(rt);
|
|
}
|
|
|
|
Symbol Value::asSymbol(Runtime& rt) && {
|
|
if (!isSymbol()) {
|
|
throw JSError(
|
|
rt, "Value is " + kindToString(*this, &rt) + ", expected a Symbol");
|
|
}
|
|
|
|
return std::move(*this).getSymbol(rt);
|
|
}
|
|
|
|
BigInt Value::asBigInt(Runtime& rt) const& {
|
|
if (!isBigInt()) {
|
|
throw JSError(
|
|
rt, "Value is " + kindToString(*this, &rt) + ", expected a BigInt");
|
|
}
|
|
|
|
return getBigInt(rt);
|
|
}
|
|
|
|
BigInt Value::asBigInt(Runtime& rt) && {
|
|
if (!isBigInt()) {
|
|
throw JSError(
|
|
rt, "Value is " + kindToString(*this, &rt) + ", expected a BigInt");
|
|
}
|
|
|
|
return std::move(*this).getBigInt(rt);
|
|
}
|
|
|
|
String Value::asString(Runtime& rt) const& {
|
|
if (!isString()) {
|
|
throw JSError(
|
|
rt, "Value is " + kindToString(*this, &rt) + ", expected a String");
|
|
}
|
|
|
|
return getString(rt);
|
|
}
|
|
|
|
String Value::asString(Runtime& rt) && {
|
|
if (!isString()) {
|
|
throw JSError(
|
|
rt, "Value is " + kindToString(*this, &rt) + ", expected a String");
|
|
}
|
|
|
|
return std::move(*this).getString(rt);
|
|
}
|
|
|
|
String Value::toString(Runtime& runtime) const {
|
|
Function toString = runtime.global().getPropertyAsFunction(runtime, "String");
|
|
return toString.call(runtime, *this).getString(runtime);
|
|
}
|
|
|
|
uint64_t BigInt::asUint64(Runtime& runtime) const {
|
|
if (!isUint64(runtime)) {
|
|
throw JSError(runtime, "Lossy truncation in BigInt64::asUint64");
|
|
}
|
|
return getUint64(runtime);
|
|
}
|
|
|
|
int64_t BigInt::asInt64(Runtime& runtime) const {
|
|
if (!isInt64(runtime)) {
|
|
throw JSError(runtime, "Lossy truncation in BigInt64::asInt64");
|
|
}
|
|
return getInt64(runtime);
|
|
}
|
|
|
|
Array Array::createWithElements(
|
|
Runtime& rt,
|
|
std::initializer_list<Value> elements) {
|
|
Array result(rt, elements.size());
|
|
size_t index = 0;
|
|
for (const auto& element : elements) {
|
|
result.setValueAtIndex(rt, index++, element);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::vector<PropNameID> HostObject::getPropertyNames(Runtime&) {
|
|
return {};
|
|
}
|
|
|
|
Runtime::ScopeState* Runtime::pushScope() {
|
|
return nullptr;
|
|
}
|
|
|
|
void Runtime::popScope(ScopeState*) {}
|
|
|
|
JSError::JSError(Runtime& rt, Value&& value) {
|
|
setValue(rt, std::move(value));
|
|
}
|
|
|
|
JSError::JSError(Runtime& rt, std::string msg) : message_(std::move(msg)) {
|
|
try {
|
|
setValue(
|
|
rt,
|
|
callGlobalFunction(rt, "Error", String::createFromUtf8(rt, message_)));
|
|
} catch (const JSIException& ex) {
|
|
message_ = std::string(ex.what()) + " (while raising " + message_ + ")";
|
|
setValue(rt, String::createFromUtf8(rt, message_));
|
|
}
|
|
}
|
|
|
|
JSError::JSError(Runtime& rt, std::string msg, std::string stack)
|
|
: message_(std::move(msg)), stack_(std::move(stack)) {
|
|
try {
|
|
Object e(rt);
|
|
e.setProperty(rt, "message", String::createFromUtf8(rt, message_));
|
|
e.setProperty(rt, "stack", String::createFromUtf8(rt, stack_));
|
|
setValue(rt, std::move(e));
|
|
} catch (const JSIException& ex) {
|
|
setValue(rt, String::createFromUtf8(rt, ex.what()));
|
|
}
|
|
}
|
|
|
|
JSError::JSError(std::string what, Runtime& rt, Value&& value)
|
|
: JSIException(std::move(what)) {
|
|
setValue(rt, std::move(value));
|
|
}
|
|
|
|
void JSError::setValue(Runtime& rt, Value&& value) {
|
|
value_ = std::make_shared<Value>(std::move(value));
|
|
|
|
if ((message_.empty() || stack_.empty()) && value_->isObject()) {
|
|
auto obj = value_->getObject(rt);
|
|
|
|
if (message_.empty()) {
|
|
try {
|
|
Value message = obj.getProperty(rt, "message");
|
|
if (!message.isUndefined() && !message.isString()) {
|
|
message = callGlobalFunction(rt, "String", message);
|
|
}
|
|
if (message.isString()) {
|
|
message_ = message.getString(rt).utf8(rt);
|
|
} else if (!message.isUndefined()) {
|
|
message_ = "String(e.message) is a " + kindToString(message, &rt);
|
|
}
|
|
} catch (const JSIException& ex) {
|
|
message_ = std::string("[Exception while creating message string: ") +
|
|
ex.what() + "]";
|
|
}
|
|
}
|
|
|
|
if (stack_.empty()) {
|
|
try {
|
|
Value stack = obj.getProperty(rt, "stack");
|
|
if (!stack.isUndefined() && !stack.isString()) {
|
|
stack = callGlobalFunction(rt, "String", stack);
|
|
}
|
|
if (stack.isString()) {
|
|
stack_ = stack.getString(rt).utf8(rt);
|
|
} else if (!stack.isUndefined()) {
|
|
stack_ = "String(e.stack) is a " + kindToString(stack, &rt);
|
|
}
|
|
} catch (const JSIException& ex) {
|
|
message_ = std::string("[Exception while creating stack string: ") +
|
|
ex.what() + "]";
|
|
}
|
|
}
|
|
}
|
|
|
|
if (message_.empty()) {
|
|
try {
|
|
if (value_->isString()) {
|
|
message_ = value_->getString(rt).utf8(rt);
|
|
} else {
|
|
Value message = callGlobalFunction(rt, "String", *value_);
|
|
if (message.isString()) {
|
|
message_ = message.getString(rt).utf8(rt);
|
|
} else {
|
|
message_ = "String(e) is a " + kindToString(message, &rt);
|
|
}
|
|
}
|
|
} catch (const JSIException& ex) {
|
|
message_ = std::string("[Exception while creating message string: ") +
|
|
ex.what() + "]";
|
|
}
|
|
}
|
|
|
|
if (stack_.empty()) {
|
|
stack_ = "no stack";
|
|
}
|
|
|
|
if (what_.empty()) {
|
|
what_ = message_ + "\n\n" + stack_;
|
|
}
|
|
}
|
|
|
|
JSIException::~JSIException() {}
|
|
|
|
JSINativeException::~JSINativeException() {}
|
|
|
|
JSError::~JSError() {}
|
|
|
|
} // namespace jsi
|
|
} // namespace facebook
|