tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 7557fe41b5d2e7c7441f49fe88cef9ff5b5b58ae
parent c08c3ea0df5664f6e1dae83b7b3d31c8f0176bf4
Author: Jan de Mooij <jdemooij@mozilla.com>
Date:   Fri,  7 Nov 2025 06:54:20 +0000

Bug 1982378 part 2 - Fix Debugger.Script.getOffset{Metadata,Location} return value for prologue bytecode ops. r=arai

If the offset was in the prologue, `getOffsetMetadata` and `getOffsetLocation`
were returning information for the first 'main' op instead. This could result in
confusing stepping behavior in DevTools if the first main op is a valid stepping
location, because the debugger would pause in the prologue.

This patch adds a `SkipPrologueOps` argument to `BytecodeRangeWithPosition` and requires
callers to specify whether they want to skip prologue ops.

Differential Revision: https://phabricator.services.mozilla.com/D271579

Diffstat:
Mjs/src/debugger/Script.cpp | 36++++++++++++++++++++++++------------
Ajs/src/jit-test/tests/debug/Script-prologue-offsets.js | 48++++++++++++++++++++++++++++++++++++++++++++++++
Mjs/src/vm/BytecodeUtil-inl.h | 13+++++++++----
Mjs/src/vm/BytecodeUtil.cpp | 4++--
4 files changed, 83 insertions(+), 18 deletions(-)

diff --git a/js/src/debugger/Script.cpp b/js/src/debugger/Script.cpp @@ -921,7 +921,8 @@ class DebuggerScript::GetPossibleBreakpointsMatcher { return false; } - for (BytecodeRangeWithPosition r(cx_, script); !r.empty(); r.popFront()) { + for (BytecodeRangeWithPosition r(cx_, script, SkipPrologueOps::Yes); + !r.empty(); r.popFront()) { if (!r.frontIsBreakablePoint()) { continue; } @@ -1023,10 +1024,14 @@ class DebuggerScript::GetOffsetMetadataMatcher { return false; } - BytecodeRangeWithPosition r(cx_, script); + // Use SkipPrologueOps::No to ensure we return isBreakpoint = false and + // isStepStart = false for prologue ops, instead of the metadata for the + // first 'main' op. + BytecodeRangeWithPosition r(cx_, script, SkipPrologueOps::No); while (!r.empty() && r.frontOffset() < offset_) { r.popFront(); } + MOZ_ASSERT(r.frontOffset() == offset_); RootedValue value(cx_, NumberValue(r.frontLineNumber())); if (!DefineDataProperty(cx_, result_, cx_->names().lineNumber, value)) { @@ -1215,7 +1220,8 @@ class FlowGraphSummary { uint32_t prevLineno = script->lineno(); uint32_t prevColumn = 1; JSOp prevOp = JSOp::Nop; - for (BytecodeRangeWithPosition r(cx, script); !r.empty(); r.popFront()) { + for (BytecodeRangeWithPosition r(cx, script, SkipPrologueOps::Yes); + !r.empty(); r.popFront()) { uint32_t lineno = prevLineno; uint32_t column = prevColumn; JSOp op = r.frontOpcode(); @@ -1336,12 +1342,14 @@ class DebuggerScript::GetOffsetLocationMatcher { return false; } - BytecodeRangeWithPosition r(cx_, script); + // Use SkipPrologueOps::No to ensure we return isEntryPoint = false for + // prologue ops, instead of the value for the first 'main' op. + BytecodeRangeWithPosition r(cx_, script, SkipPrologueOps::No); while (!r.empty() && r.frontOffset() < offset_) { r.popFront(); } + MOZ_ASSERT(r.frontOffset() == offset_); - size_t offset = r.frontOffset(); bool isEntryPoint = r.frontIsEntryPoint(); // Line numbers are only correctly defined on entry points. Thus looks @@ -1378,9 +1386,9 @@ class DebuggerScript::GetOffsetLocationMatcher { } // The same entry point test that is used by getAllColumnOffsets. - isEntryPoint = (isEntryPoint && !flowData[offset].hasNoEdges() && - (flowData[offset].lineno() != r.frontLineNumber() || - flowData[offset].columnOrSentinel() != + isEntryPoint = (isEntryPoint && !flowData[offset_].hasNoEdges() && + (flowData[offset_].lineno() != r.frontLineNumber() || + flowData[offset_].columnOrSentinel() != r.frontColumnNumber().oneOriginValue())); value.setBoolean(isEntryPoint); if (!DefineDataProperty(cx_, result_, cx_->names().isEntryPoint, value)) { @@ -1772,7 +1780,8 @@ bool DebuggerScript::CallData::getAllOffsets() { if (!result) { return false; } - for (BytecodeRangeWithPosition r(cx, script); !r.empty(); r.popFront()) { + for (BytecodeRangeWithPosition r(cx, script, SkipPrologueOps::Yes); + !r.empty(); r.popFront()) { if (!r.frontIsEntryPoint()) { continue; } @@ -1881,7 +1890,8 @@ class DebuggerScript::GetAllColumnOffsetsMatcher { return false; } - for (BytecodeRangeWithPosition r(cx_, script); !r.empty(); r.popFront()) { + for (BytecodeRangeWithPosition r(cx_, script, SkipPrologueOps::Yes); + !r.empty(); r.popFront()) { uint32_t lineno = r.frontLineNumber(); JS::LimitedColumnNumberOneOrigin column = r.frontColumnNumber(); size_t offset = r.frontOffset(); @@ -1964,7 +1974,8 @@ class DebuggerScript::GetLineOffsetsMatcher { } // Second pass: build the result array. - for (BytecodeRangeWithPosition r(cx_, script); !r.empty(); r.popFront()) { + for (BytecodeRangeWithPosition r(cx_, script, SkipPrologueOps::Yes); + !r.empty(); r.popFront()) { if (!r.frontIsEntryPoint()) { continue; } @@ -2424,7 +2435,8 @@ bool DebuggerScript::CallData::getOffsetsCoverage() { RootedValue countValue(cx); // Iterate linearly over the bytecode. - for (BytecodeRangeWithPosition r(cx, script); !r.empty(); r.popFront()) { + for (BytecodeRangeWithPosition r(cx, script, SkipPrologueOps::Yes); + !r.empty(); r.popFront()) { size_t offset = r.frontOffset(); // The beginning of each non-branching sequences of instruction set the diff --git a/js/src/jit-test/tests/debug/Script-prologue-offsets.js b/js/src/jit-test/tests/debug/Script-prologue-offsets.js @@ -0,0 +1,48 @@ +let g = newGlobal({newCompartment: true}); +let dbg = new Debugger(g); +let count = 0; +dbg.onEnterFrame = function(frame) { + if (frame.type === "eval") { + return; + } + + // This test assumes the hello1 and hello2 functions have prologue bytecode. + assertEq(frame.script.mainOffset > 0, true); + + // Prologue bytecode ops are never marked as break/step points. + let prologueData = frame.script.getOffsetMetadata(0); + assertEq(prologueData.isBreakpoint, false); + assertEq(prologueData.isStepStart, false); + + // Prologue ops are also never marked as entry points. + let location = frame.script.getOffsetLocation(0); + assertEq(location.isEntryPoint, false); + if (count == 0) { + // hello1 + assertEq(location.lineNumber, 3); + assertEq(location.columnNumber, 5); + } else { + // hello2 + assertEq(location.lineNumber, 5); + assertEq(location.columnNumber, 18); + } + + // getPossibleBreakpointOffsets shouldn't return prologue offsets. + for (let offset of frame.script.getPossibleBreakpointOffsets()) { + assertEq(offset >= frame.script.mainOffset, true); + } + + count++; +}; +g.eval(` // 1 + function hello1(name) { // 2 + return [].some((r) => r === name); // 3 + } // 4 + function hello2(name, x=1) { // 5 + return [].some((r) => r === name); // 6 + } // 7 + hello1("world"); // 8 + hello2("world"); // 9 +`); + +assertEq(count, 2); diff --git a/js/src/vm/BytecodeUtil-inl.h b/js/src/vm/BytecodeUtil-inl.h @@ -108,6 +108,8 @@ class BytecodeRange { jsbytecode* end; }; +enum class SkipPrologueOps { No, Yes }; + class BytecodeRangeWithPosition : private BytecodeRange { public: using BytecodeRange::empty; @@ -115,7 +117,8 @@ class BytecodeRangeWithPosition : private BytecodeRange { using BytecodeRange::frontOpcode; using BytecodeRange::frontPC; - BytecodeRangeWithPosition(JSContext* cx, JSScript* script) + BytecodeRangeWithPosition(JSContext* cx, JSScript* script, + SkipPrologueOps skipPrologueOps) : BytecodeRange(cx, script), initialLine(script->lineno()), lineno(script->lineno()), @@ -130,10 +133,12 @@ class BytecodeRangeWithPosition : private BytecodeRange { snpc += sn->delta(); } updatePosition(); - while (frontPC() != mainPC) { - popFront(); + if (skipPrologueOps == SkipPrologueOps::Yes) { + while (frontPC() != mainPC) { + popFront(); + } + MOZ_ASSERT(entryPointState != EntryPointState::NotEntryPoint); } - MOZ_ASSERT(entryPointState != EntryPointState::NotEntryPoint); } void popFront() { diff --git a/js/src/vm/BytecodeUtil.cpp b/js/src/vm/BytecodeUtil.cpp @@ -2769,8 +2769,8 @@ static bool GetPCCountJSON(JSContext* cx, const ScriptAndCounts& sac, json.beginListProperty("opcodes"); uint64_t hits = 0; - for (BytecodeRangeWithPosition range(cx, script); !range.empty(); - range.popFront()) { + for (BytecodeRangeWithPosition range(cx, script, SkipPrologueOps::Yes); + !range.empty(); range.popFront()) { jsbytecode* pc = range.frontPC(); size_t offset = script->pcToOffset(pc); JSOp op = JSOp(*pc);