Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion API/hermes/hermes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,8 @@ class HermesRuntimeImpl final : public HermesRuntime,
private IHermesTestHelpers,
private InstallHermesFatalErrorHandler,
private jsi::Instrumentation,
public ISetEventLoopControl
public ISetEventLoopControl,
public ICancelAsyncTimeout
#ifdef JSI_UNSTABLE
,
public jsi::ISerialization,
Expand Down Expand Up @@ -1323,6 +1324,7 @@ class HermesRuntimeImpl final : public HermesRuntime,
void registerForProfiling() override;
void unregisterForProfiling() override;
void asyncTriggerTimeout() override;
bool asyncCancelTimeout() override;
void watchTimeLimit(uint32_t timeoutInMs) override;
void unwatchTimeLimit() override;
jsi::Value evaluateJavaScriptWithSourceMap(
Expand Down Expand Up @@ -1622,6 +1624,8 @@ jsi::ICast *HermesRuntimeImpl::castInterface(const jsi::UUID &interfaceUUID) {
return static_cast<IHermes *>(this);
} else if (interfaceUUID == IHermesSHUnit::uuid) {
return static_cast<IHermesSHUnit *>(this);
} else if (interfaceUUID == ICancelAsyncTimeout::uuid) {
return static_cast<ICancelAsyncTimeout *>(this);
}
#ifdef JSI_UNSTABLE
else if (interfaceUUID == ISerialization::uuid) {
Expand Down Expand Up @@ -1902,6 +1906,17 @@ void HermesRuntimeImpl::asyncTriggerTimeout() {
runtime_.triggerTimeoutAsyncBreak();
}

bool HermesRuntimeImpl::asyncCancelTimeout() {
// Unlike asyncTriggerTimeout(), this may only be called on the JS thread
// (see the IHermes contract), so NoMutatorScope is safe here. That
// contract is also what makes the clear below correct: on this thread the
// interpreter is either not running or suspended in the native frame that
// called us, so it cannot concurrently consume the request in an async
// break check.
vm::NoMutatorScope noMutatorScope{runtime_};
return runtime_.cancelTimeoutAsyncBreak();
}

void HermesRuntimeImpl::watchTimeLimit(uint32_t timeoutInMs) {
vm::NoMutatorScope noMutatorScope{runtime_};
auto &runtimeTimeLimitMonitor = runtime_.timeLimitMonitor;
Expand Down
16 changes: 16 additions & 0 deletions API/hermes_sandbox/HermesSandboxRuntime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

#include "external/hermes_sandbox_impl_compiled.h"
#include "hermes/ADT/ManagedChunkedList.h"
#include "jsi/hermes-interfaces.h"
#include "jsi/jsilib.h"

#include <atomic>
Expand Down Expand Up @@ -897,6 +898,7 @@ class NativeTable {
#define THROW_UNIMPLEMENTED() throwUnimplementedImpl(__func__)

class HermesSandboxRuntimeImpl : public facebook::hermes::HermesSandboxRuntime,
public facebook::hermes::ICancelAsyncTimeout,
public W2CHermesRAII {
class ManagedPointerHolder;

Expand Down Expand Up @@ -1743,6 +1745,20 @@ class HermesSandboxRuntimeImpl : public facebook::hermes::HermesSandboxRuntime,
asyncTimeout_.store(true, std::memory_order_relaxed);
}

bool asyncCancelTimeout() override {
// The flag lives on the host side and the sandboxed interpreter polls it
// through a host callback (see the testAndClearAsyncTimeout vtable use),
// so clearing it here fully cancels a not-yet-observed request.
return testAndClearAsyncTimeout();
}

ICast *castInterface(const UUID &interfaceUUID) override {
if (interfaceUUID == facebook::hermes::ICancelAsyncTimeout::uuid) {
return static_cast<facebook::hermes::ICancelAsyncTimeout *>(this);
}
return Runtime::castInterface(interfaceUUID);
}

/// Return true if an asynchronous timeout has been triggered.
bool testAsyncTimeout() {
return asyncTimeout_.load(std::memory_order_relaxed);
Expand Down
4 changes: 3 additions & 1 deletion API/hermes_sandbox/HermesSandboxRuntime.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ class HERMES_EXPORT HermesSandboxRuntime : public jsi::Runtime {
const std::string &sourceURL) = 0;

/// Asynchronously terminates the current execution. This can be called on
/// any thread.
/// any thread. A pending, not-yet-observed termination request can be
/// cancelled via the ICancelAsyncTimeout interface (obtained with
/// castInterface).
virtual void asyncTriggerTimeout() = 0;
};

Expand Down
32 changes: 31 additions & 1 deletion API/jsi/jsi/hermes-interfaces.h
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,8 @@ class JSI_EXPORT IHermes : public jsi::ICast {
/// the emitted code contains async break checks).

/// Asynchronously terminates the current execution. This can be called on
/// any thread.
/// any thread. A pending, not-yet-observed termination request can be
/// cancelled via the ICancelAsyncTimeout interface, where supported.
virtual void asyncTriggerTimeout() = 0;

/// Register this runtime for execution time limit monitoring, with a time
Expand Down Expand Up @@ -221,6 +222,35 @@ class JSI_EXPORT IHermes : public jsi::ICast {
~IHermes() = default;
};

/// Cancellation counterpart to IHermes::asyncTriggerTimeout(), for runtimes
/// that support it. Obtained via castInterface; a null result means the
/// runtime does not support cancellation.
class ICancelAsyncTimeout : public jsi::ICast {
public:
static constexpr jsi::UUID uuid{
0x46e249ba,
0xd059,
0x4900,
0x9f22,
0xbbbc43b52e0b};

/// Cancel a pending timeout previously requested with
/// IHermes::asyncTriggerTimeout() that has not yet been observed by
/// executing JS. \return true if a pending timeout was cancelled, false if
/// there was none (either none was triggered, or it already terminated an
/// execution). Unlike asyncTriggerTimeout(), this may only be called on
/// the thread that executes JS on this runtime -- either between
/// executions or from a native frame invoked by executing JS. Calling it
/// from another thread would race the interpreter's consumption of the
/// request. This is the analog of V8's Isolate::CancelTerminateExecution:
/// without it, a timeout that fires after the targeted execution completed
/// remains pending and terminates the next, unrelated execution.
virtual bool asyncCancelTimeout() = 0;

protected:
~ICancelAsyncTimeout() = default;
};

/// Interface for provide Hermes backend specific methods.
class IHermesSHUnit : public jsi::ICast {
public:
Expand Down
6 changes: 5 additions & 1 deletion include/hermes/VM/CodeBlock.h
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,11 @@ class CodeBlock final : private llvh::TrailingObjects<
/// at location \p offset.
/// Requires that there's a breakpoint registered at \p offset.
/// Increments the user count of the associated runtime module.
void installBreakpointAtOffset(uint32_t offset);
/// \return true on success; false if the bytecode page cannot be made
/// writable (e.g. statically embedded bytecode in a read-only segment
/// that the OS refuses to remap, such as macOS __DATA_CONST under
/// hardened runtime). On failure, no state is modified.
LLVM_NODISCARD bool installBreakpointAtOffset(uint32_t offset);

/// Uninstalls the debugger instruction from the opcode stream
/// at location \p offset, replacing it with \p opCode.
Expand Down
15 changes: 12 additions & 3 deletions include/hermes/VM/Debugger/Debugger.h
Original file line number Diff line number Diff line change
Expand Up @@ -473,8 +473,13 @@ class Debugger {
/// for the given codeBlock and offset, else creates one.
/// Used by other functions which should be called to set breakpoints.
/// Installs a breakpoint at that location, doesn't modify it.
/// \return the location at which the breakpoint was installed.
BreakpointLocation &installBreakpoint(CodeBlock *codeBlock, uint32_t offset);
/// \return a pointer to the location at which the breakpoint was installed,
/// or nullptr if the bytecode page cannot be made writable (e.g.
/// statically embedded bytecode in a read-only segment that the OS
/// refuses to remap). On failure, no state is modified.
LLVM_NODISCARD BreakpointLocation *installBreakpoint(
CodeBlock *codeBlock,
uint32_t offset);

/// Helper function to uninstall a breakpoint. Always use this function to
/// uninstall breakpoints. This takes care of the case when we purposely keep
Expand All @@ -489,7 +494,11 @@ class Debugger {
/// If the physical breakpoint isn't enabled yet, patches the debugger
/// instruction in.
/// Sets the breakpoint ID to \p id.
void
/// \return true on success; false if the bytecode page cannot be made
/// writable. On failure, no state is modified and the breakpoint is not
/// physically installed (the caller may still keep it in
/// userBreakpoints_ as a record).
bool
setUserBreakpoint(CodeBlock *codeBlock, uint32_t offset, BreakpointID id);

/// Should not be called directly except from \p setStepBreakpoint() or \p
Expand Down
17 changes: 15 additions & 2 deletions include/hermes/VM/Runtime.h
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,16 @@ class Runtime : public RuntimeBase, public HandleRootOwner {
triggerAsyncBreak(AsyncBreakReasonBits::Timeout);
}

/// Cancel a timeout async break requested via triggerTimeoutAsyncBreak()
/// that has not yet been observed by the interpreter. Unlike
/// triggerTimeoutAsyncBreak(), this may only be called on the thread that
/// executes JS -- either between executions or from a native frame invoked
/// by executing JS; see testAndClearAsyncBreakRequest(). \return whether a
/// request was pending.
bool cancelTimeoutAsyncBreak() {
return testAndClearTimeoutAsyncBreakRequest();
}

#ifdef HERMES_ENABLE_DEBUGGER
/// Encapsulates useful information about a stack frame, needed by the
/// debugger. It requres extra context and cannot be extracted from a
Expand Down Expand Up @@ -1511,8 +1521,11 @@ class Runtime : public RuntimeBase, public HandleRootOwner {
/// \p reasonBit request bit afterward.
uint8_t testAndClearAsyncBreakRequest(uint8_t reasonBits) {
/// Note that while the triggerTimeoutAsyncBreak() function may be called
/// from any thread, this one may only be called from within the Interpreter
/// loop.
/// from any thread, this one may only be called on the thread that
/// executes JS: either from within the Interpreter loop, or from a host
/// API while no JS is executing (e.g. HermesRuntime::asyncCancelTimeout).
/// Concurrent calls could both pass the fast path and then race the
/// fetch_and, making the loser observe oldFlag == 0.
uint8_t flag = asyncBreakRequestFlag_.load(std::memory_order_relaxed);
if (LLVM_LIKELY((flag & (uint8_t)reasonBits) == 0)) {
// Fast path.
Expand Down
18 changes: 10 additions & 8 deletions lib/VM/CodeBlock.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -324,8 +324,10 @@ uint32_t CodeBlock::getVirtualOffset() const {
#ifdef HERMES_ENABLE_DEBUGGER

/// Makes the page that \p address is in writable.
/// If it fails, aborts execution.
static void makeWritable(void *address, size_t length) {
/// \return true on success, false if the page cannot be made writable (e.g.
/// the bytecode lives in a read-only segment of the binary that the OS
/// refuses to remap, such as macOS __DATA_CONST under hardened runtime).
static bool makeWritable(void *address, size_t length) {
void *endAddress = static_cast<void *>(static_cast<char *>(address) + length);

// Align the address to page size before setting the pagesize.
Expand All @@ -335,14 +337,11 @@ static void makeWritable(void *address, size_t length) {
size_t totalLength =
static_cast<char *>(endAddress) - static_cast<char *>(alignedAddress);

bool success = oscompat::vm_protect(
return oscompat::vm_protect(
alignedAddress, totalLength, oscompat::ProtectMode::ReadWrite);
if (!success) {
hermes_fatal("mprotect failed before modifying breakpoint");
}
}

void CodeBlock::installBreakpointAtOffset(uint32_t offset) {
bool CodeBlock::installBreakpointAtOffset(uint32_t offset) {
auto opcodes = getOpcodeArray();
assert(offset < opcodes.size() && "patch offset out of bounds");
hbc::opcode_atom_t *address =
Expand All @@ -354,9 +353,12 @@ void CodeBlock::installBreakpointAtOffset(uint32_t offset) {
sizeof(inst::DebuggerInst) == 1,
"debugger instruction can only be a single opcode atom");

makeWritable(address, sizeof(inst::DebuggerInst));
if (!makeWritable(address, sizeof(inst::DebuggerInst))) {
return false;
}
*address = debuggerOpcode;
++numInstalledBreakpoints_;
return true;
}

void CodeBlock::uninstallBreakpointAtOffset(
Expand Down
Loading
Loading