Skip to content

Commit 664be0e

Browse files
committed
Skip breakpoints on read-only bytecode pages instead of aborting
The debugger installs breakpoints by overwriting an opcode in the bytecode stream with Debugger, which requires the page to be writable. The patcher uses mprotect to make the page RW; on failure it called hermes_fatal and aborted the process. If the bytecode lives in a read-only segment (e.g. statically linked into the binary as a const array, ending up in __DATA_CONST / __TEXT_CONST on macOS), mprotect is rejected by the OS (EACCES under hardened runtime) and the abort takes down the whole process. This is reachable in practice because step / restoration / on-load breakpoints install at offset 0 of every CodeBlock about to execute, so stepping into such a CodeBlock from anywhere kills the runtime. Make the patch best-effort: - CodeBlock::installBreakpointAtOffset and the static makeWritable helper now return bool. On failure no state is modified. - Debugger::installBreakpoint returns BreakpointLocation* (nullptr on failure), rolling back the partial breakpointLocations_ entry it just inserted. - doSetNonUserBreakpoint and setOnLoadBreakpoint silently skip when installBreakpoint fails. Step / restoration / on-load breakpoints are best-effort hints; the only loss is that stepping cannot pause at the entry of a CodeBlock whose page cannot be patched. - setUserBreakpoint now returns bool so callers can propagate failure to their clients (e.g. CDP could surface a proper error back to DevTools instead of aborting). - The four call sites that re-install a previously-installed breakpoint (the page is known writable since the original install succeeded) assert the result.
1 parent c42f418 commit 664be0e

4 files changed

Lines changed: 91 additions & 37 deletions

File tree

include/hermes/VM/CodeBlock.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,11 @@ class CodeBlock final : private llvh::TrailingObjects<
371371
/// at location \p offset.
372372
/// Requires that there's a breakpoint registered at \p offset.
373373
/// Increments the user count of the associated runtime module.
374-
void installBreakpointAtOffset(uint32_t offset);
374+
/// \return true on success; false if the bytecode page cannot be made
375+
/// writable (e.g. statically embedded bytecode in a read-only segment
376+
/// that the OS refuses to remap, such as macOS __DATA_CONST under
377+
/// hardened runtime). On failure, no state is modified.
378+
LLVM_NODISCARD bool installBreakpointAtOffset(uint32_t offset);
375379

376380
/// Uninstalls the debugger instruction from the opcode stream
377381
/// at location \p offset, replacing it with \p opCode.

include/hermes/VM/Debugger/Debugger.h

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -473,8 +473,13 @@ class Debugger {
473473
/// for the given codeBlock and offset, else creates one.
474474
/// Used by other functions which should be called to set breakpoints.
475475
/// Installs a breakpoint at that location, doesn't modify it.
476-
/// \return the location at which the breakpoint was installed.
477-
BreakpointLocation &installBreakpoint(CodeBlock *codeBlock, uint32_t offset);
476+
/// \return a pointer to the location at which the breakpoint was installed,
477+
/// or nullptr if the bytecode page cannot be made writable (e.g.
478+
/// statically embedded bytecode in a read-only segment that the OS
479+
/// refuses to remap). On failure, no state is modified.
480+
LLVM_NODISCARD BreakpointLocation *installBreakpoint(
481+
CodeBlock *codeBlock,
482+
uint32_t offset);
478483

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

495504
/// Should not be called directly except from \p setStepBreakpoint() or \p

lib/VM/CodeBlock.cpp

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -332,8 +332,10 @@ uint32_t CodeBlock::getVirtualOffset() const {
332332
#ifdef HERMES_ENABLE_DEBUGGER
333333

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

339341
// Align the address to page size before setting the pagesize.
@@ -343,14 +345,11 @@ static void makeWritable(void *address, size_t length) {
343345
size_t totalLength =
344346
static_cast<char *>(endAddress) - static_cast<char *>(alignedAddress);
345347

346-
bool success = oscompat::vm_protect(
348+
return oscompat::vm_protect(
347349
alignedAddress, totalLength, oscompat::ProtectMode::ReadWrite);
348-
if (!success) {
349-
hermes_fatal("mprotect failed before modifying breakpoint");
350-
}
351350
}
352351

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

365-
makeWritable(address, sizeof(inst::DebuggerInst));
364+
if (!makeWritable(address, sizeof(inst::DebuggerInst))) {
365+
return false;
366+
}
366367
*address = debuggerOpcode;
367368
++numInstalledBreakpoints_;
369+
return true;
368370
}
369371

370372
void CodeBlock::uninstallBreakpointAtOffset(

lib/VM/Debugger/Debugger.cpp

Lines changed: 64 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,12 @@ ExecutionStatus Debugger::debuggerLoop(
421421
}
422422
breakAtPossibleNextInstructions(state);
423423
if (breakpointOpt) {
424-
state.codeBlock->installBreakpointAtOffset(state.offset);
424+
// Re-installing a previously-installed breakpoint: the page is
425+
// already known writable.
426+
bool ok =
427+
state.codeBlock->installBreakpointAtOffset(state.offset);
428+
(void)ok;
429+
assert(ok && "re-installing existing breakpoint cannot fail");
425430
}
426431
if (stepMode == StepMode::Into) {
427432
// Stepping in could enter another code block,
@@ -767,18 +772,26 @@ Debugger::getBreakpointLocation(CodeBlock *codeBlock, uint32_t offset) const {
767772
}
768773

769774
auto Debugger::installBreakpoint(CodeBlock *codeBlock, uint32_t offset)
770-
-> BreakpointLocation & {
775+
-> BreakpointLocation * {
771776
auto opcodes = codeBlock->getOpcodeArray();
772777
assert(offset < opcodes.size() && "invalid offset to set breakpoint");
773-
auto &location =
774-
breakpointLocations_
775-
.try_emplace(codeBlock->getOffsetPtr(offset), opcodes[offset])
776-
.first->second;
778+
auto *opcodePtr = codeBlock->getOffsetPtr(offset);
779+
auto emplaceResult =
780+
breakpointLocations_.try_emplace(opcodePtr, opcodes[offset]);
781+
auto &location = emplaceResult.first->second;
777782
if (location.count() == 0) {
778783
// count used to be 0, so patch this in now that the count > 0.
779-
codeBlock->installBreakpointAtOffset(offset);
784+
if (!codeBlock->installBreakpointAtOffset(offset)) {
785+
// Patching failed (e.g. statically embedded bytecode in a read-only
786+
// segment that the OS refuses to remap). Roll back the entry we just
787+
// inserted so state stays consistent, and signal failure to caller.
788+
if (emplaceResult.second) {
789+
breakpointLocations_.erase(emplaceResult.first);
790+
}
791+
return nullptr;
792+
}
780793
}
781-
return location;
794+
return &location;
782795
}
783796

784797
void Debugger::uninstallBreakpoint(
@@ -796,28 +809,39 @@ void Debugger::uninstallBreakpoint(
796809
}
797810
}
798811

799-
void Debugger::setUserBreakpoint(
812+
bool Debugger::setUserBreakpoint(
800813
CodeBlock *codeBlock,
801814
uint32_t offset,
802815
BreakpointID id) {
803-
BreakpointLocation &location = installBreakpoint(codeBlock, offset);
804-
location.userBreakpointIDs.insert(id);
816+
BreakpointLocation *location = installBreakpoint(codeBlock, offset);
817+
if (!location) {
818+
return false;
819+
}
820+
location->userBreakpointIDs.insert(id);
821+
return true;
805822
}
806823

807824
void Debugger::doSetNonUserBreakpoint(
808825
CodeBlock *codeBlock,
809826
uint32_t offset,
810827
uint32_t callStackDepth,
811828
bool isStepBreakpoint) {
812-
BreakpointLocation &location = installBreakpoint(codeBlock, offset);
829+
BreakpointLocation *location = installBreakpoint(codeBlock, offset);
830+
if (!location) {
831+
// Best-effort: step/restoration breakpoints are an optimization for
832+
// pause-on-entry while stepping. If the target lives in a read-only
833+
// bytecode segment we cannot patch, just skip it -- stepping will simply
834+
// not pause at the entry of that code block.
835+
return;
836+
}
813837
std::vector<Breakpoint> &breakpoints =
814838
isStepBreakpoint ? tempBreakpoints_ : restorationBreakpoints_;
815-
if (location.callStackDepths.count(callStackDepth) == 0) {
816-
location.callStackDepths.insert(callStackDepth);
839+
if (location->callStackDepths.count(callStackDepth) == 0) {
840+
location->callStackDepths.insert(callStackDepth);
817841
}
818842

819-
if ((isStepBreakpoint && !location.hasStepBreakpoint) ||
820-
(!isStepBreakpoint && !location.hasRestorationBreakpoint)) {
843+
if ((isStepBreakpoint && !location->hasStepBreakpoint) ||
844+
(!isStepBreakpoint && !location->hasRestorationBreakpoint)) {
821845
// Leave the resolved location empty for now,
822846
// let the caller fill it in lazily.
823847
Breakpoint breakpoint{};
@@ -828,9 +852,9 @@ void Debugger::doSetNonUserBreakpoint(
828852
}
829853

830854
if (isStepBreakpoint) {
831-
location.hasStepBreakpoint = true;
855+
location->hasStepBreakpoint = true;
832856
} else {
833-
location.hasRestorationBreakpoint = true;
857+
location->hasRestorationBreakpoint = true;
834858
}
835859
}
836860

@@ -843,17 +867,22 @@ void Debugger::setStepBreakpoint(
843867
}
844868

845869
void Debugger::setOnLoadBreakpoint(CodeBlock *codeBlock, uint32_t offset) {
846-
BreakpointLocation &location = installBreakpoint(codeBlock, offset);
870+
BreakpointLocation *location = installBreakpoint(codeBlock, offset);
871+
if (!location) {
872+
// Best-effort: pauseOnScriptLoad cannot pause inside a code block whose
873+
// bytecode page is not writable. Skip.
874+
return;
875+
}
847876
// Leave the resolved location empty for now,
848877
// let the caller fill it in lazily.
849878
Breakpoint breakpoint{};
850879
breakpoint.codeBlock = codeBlock;
851880
breakpoint.offset = offset;
852881
breakpoint.enabled = true;
853-
assert(!location.onLoad && "can't set duplicate on-load breakpoint");
854-
location.onLoad = true;
882+
assert(!location->onLoad && "can't set duplicate on-load breakpoint");
883+
location->onLoad = true;
855884
tempBreakpoints_.push_back(breakpoint);
856-
assert(location.count() && "invalid count following set breakpoint");
885+
assert(location->count() && "invalid count following set breakpoint");
857886
}
858887

859888
void Debugger::unsetUserBreakpoint(
@@ -1017,8 +1046,12 @@ void Debugger::setRestorationBreakpoint(
10171046

10181047
bool Debugger::restoreBreakpointIfAny() {
10191048
if (breakpointToRestore_.first != nullptr) {
1020-
breakpointToRestore_.first->installBreakpointAtOffset(
1049+
// Re-installing a previously-installed breakpoint: the page is already
1050+
// known writable.
1051+
bool ok = breakpointToRestore_.first->installBreakpointAtOffset(
10211052
breakpointToRestore_.second);
1053+
(void)ok;
1054+
assert(ok && "re-installing existing breakpoint cannot fail");
10221055
breakpointToRestore_ = {nullptr, 0};
10231056
return true;
10241057
}
@@ -1046,7 +1079,10 @@ ExecutionStatus Debugger::stepInstruction(InterpreterState &state) {
10461079
// Temporarily uninstall the breakpoint so we can run the real instruction.
10471080
uninstallBreakpoint(codeBlock, offset, locationOpt->opCode);
10481081
status = runtime_.stepFunction(newState);
1049-
codeBlock->installBreakpointAtOffset(offset);
1082+
// Re-install: page already known writable.
1083+
bool ok = codeBlock->installBreakpointAtOffset(offset);
1084+
(void)ok;
1085+
assert(ok && "re-installing existing breakpoint cannot fail");
10501086
} else {
10511087
status = runtime_.stepFunction(newState);
10521088
}
@@ -1092,7 +1128,10 @@ ExecutionStatus Debugger::processInstUnderDebuggerOpCode(
10921128
runtime_.setCurrentIP(ip);
10931129
ExecutionStatus status = runtime_.stepFunction(newState);
10941130
runtime_.invalidateCurrentIP();
1095-
codeBlock->installBreakpointAtOffset(offset);
1131+
// Re-install: page already known writable.
1132+
bool ok = codeBlock->installBreakpointAtOffset(offset);
1133+
(void)ok;
1134+
assert(ok && "re-installing existing breakpoint cannot fail");
10961135
if (status == ExecutionStatus::EXCEPTION) {
10971136
return status;
10981137
}

0 commit comments

Comments
 (0)