Skip to content

[JSC] Do not inline a function that has debug hooks - #837

Open
robobun wants to merge 1 commit into
robobun/aa32a760/unlinked-code-block-per-modefrom
robobun/aa32a760/no-inlining-of-debug-hooks
Open

robobun wants to merge 1 commit into
robobun/aa32a760/unlinked-code-block-per-modefrom
robobun/aa32a760/no-inlining-of-debug-hooks

Conversation

@robobun

@robobun robobun commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

Based on #830, which is based on #836 (the third commit here).

Problem

  • A breakpoint or a debugger statement in a function is skipped when the function runs inlined in DFG or FTL code: 8,507 of 10,000 calls in the test.
  • ByteCodeParser::inliningCost (dfg/DFGByteCodeParser.cpp:2021) refuses to inline only when the caller has the debugger's mark. The callee is not checked.
  • Two callers have no mark: code of a global object without a debugger, and a generator body that is suspended when the debugger attaches.

Fix

  • inliningCost also refuses a callee whose bytecode has debug hooks. A private builtin has the mark but no hooks, and stays inlinable.
  • Correct because an inlined op_debug is a nop, and CodeBlock::addBreakpoint jettisons only the optimized code of its own function.
  • Verified: JSTests/stress/dfg-does-not-inline-a-function-that-has-debug-hooks.js (3 callers, all fail without the change).

Background

Downsides

  • Code of a global object without a debugger runs a call to a debugged function as a real call, only while a debugger is attached to the callee's global object.
  • None found without a debugger: no bytecode has debug hooks then. The new check is one load and one branch for each inlining candidate, at compile time.
Notes

jsc shell, Release with assertions, --useConcurrentJIT=0. The callee has a debugger statement, which keeps it from tiering up by itself, as a breakpoint does. $vm.dfgTrue() in the callee is true only when it runs inlined. Calls that reached the statement in the callee's own frame, of 10,000:

caller main e8a8efc09c #830 this PR
both global objects linked it, the debugged one generated it first, the other one runs it 10,000 1,493 10,000
the other global object generated it first 1,493 1,493 10,000
a generator body that is suspended when the debugger attaches 2,166 2,151 10,000

The first row is 10,000 on main because main hands the other global object the debugged one's marked bytecode. #830 stops that, and this PR puts the guard where it belongs.

Upstream JavaScriptCore has the same guard and the same gap.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline nit, I also checked that every DFG/FTL inlining path reaches the new refusal: both attemptToInlineCall (DFGByteCodeParser.cpp:2440) and handleVarargsInlining (DFGByteCodeParser.cpp:2508) go through inliningCost, and the new check sits after the baseline CodeBlock null check, so no inlining route skips it and there is no null dereference. The private-builtin exclusion matches BytecodeGenerator::shouldEmitDebugHooks (BytecodeGenerator.h:1097), which never emits op_debug for private builtins.

Extended reasoning...

The change adds one USE(BUN_JSC_ADDITIONS)-guarded refusal in ByteCodeParser::inliningCost so a callee whose bytecode carries debug hooks is never inlined, plus a new jsc-shell stress test; it touches no security-sensitive surface. The check can only make the DFG/FTL more conservative (a real call instead of an inlined one) and only while a debugger is attached, but it is core JIT inlining policy whose correctness rests on the mark semantics introduced by the stacked PRs this one builds on, so a human maintainer look is still worthwhile.

Comment thread JSTests/stress/dfg-does-not-inline-a-function-that-has-debug-hooks.js Outdated
@github-actions

github-actions Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Preview build of a8110c4: autobuild-preview-pr-837-a8110c45

@robobun
robobun force-pushed the robobun/aa32a760/unlinked-code-block-per-mode branch from 11b5867 to 71da639 Compare October 18, 2026 19:02
@robobun
robobun force-pushed the robobun/aa32a760/no-inlining-of-debug-hooks branch from f940adf to 756d5a2 Compare October 18, 2026 19:02

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

@robobun
robobun force-pushed the robobun/aa32a760/unlinked-code-block-per-mode branch from 71da639 to c19a150 Compare October 20, 2026 06:45
@robobun
robobun force-pushed the robobun/aa32a760/no-inlining-of-debug-hooks branch from 756d5a2 to a664b85 Compare October 20, 2026 06:45

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I re-reviewed the latest push and found no bugs; the earlier testLoopCount nit is addressed. Since this changes the DFG inlining gate in code covered by the JSC reviewers in CODEOWNERS and depends on the base PR's debugger-mark guarantees, a human look is still worthwhile.

What was reviewed:

  • The new gate in inliningCost mirrors BytecodeGenerator::shouldEmitDebugHooks (Debugger mode and not a private builtin), so it refuses exactly the callees that carry op_debug; excluding private builtins is consistent with that.
  • FTL path: targetCodeBlock and the baseline block share the executable's UnlinkedCodeBlock, so checking the baseline block's mark covers both tiers; the check only ever removes inlining, so no new JIT behavior is introduced.
  • The test's $vm.enableDebuggerModeWhenIdle, createGlobalObject and testLoopCount all exist in this fork's shell, and the single runDefault mode pins its options.
Extended reasoning...

The change adds one USE(BUN_JSC_ADDITIONS)-guarded early return in ByteCodeParser::inliningCost (Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp) refusing to inline a callee whose bytecode was compiled with debugging opcodes unless it is a private builtin, plus a new stress test. It touches no security-sensitive surface; the effect is strictly to inline less when a debugger is attached. Approval was withheld because /Source/JavaScriptCore and /JSTests are covered by CODEOWNERS entries and the fix's correctness rests on the debugger mark being reliable, which the author attributes to a base PR not in this diff. The latest commit addressed the only prior feedback (iteration count now comes from testLoopCount), and the bug hunt ran dry with no findings.

robobun added a commit to oven-sh/bun that referenced this pull request Oct 20, 2026
…the profilers and a client that disconnects

The preview has oven-sh/WebKit#836, oven-sh/WebKit#830 and oven-sh/WebKit#837 on top of the WebKit commit that main pins.

New tests: a vm.Script that runs twice in one global object, with a debugger, the type profiler and the control
flow profiler turned on while its first generator is suspended. A generator and an async function that start under
a profiler and resume after the profiler is turned off, and after the inspector client is gone.
@robobun
robobun force-pushed the robobun/aa32a760/unlinked-code-block-per-mode branch from c19a150 to 93c3757 Compare October 21, 2026 18:49
@robobun
robobun force-pushed the robobun/aa32a760/no-inlining-of-debug-hooks branch from a664b85 to d14e779 Compare October 21, 2026 18:49

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

robobun added a commit to oven-sh/bun that referenced this pull request Oct 21, 2026
…bKit main

WebKit main (e8a8efc09c) is one commit past the WebKit commit that main pins (7d0c8a7d09): oven-sh/WebKit#862.
The DFG turns an inlined op_debug into nothing (ByteCodeParser, op_debug), on the grounds that a breakpoint
jettisons every optimized CodeBlock that contains it. It does not: CodeBlock::addBreakpoint jettisons the optimized
code of the function the breakpoint is in, not the code of a caller that has inlined that function. The one guard,
in ByteCodeParser::inliningCost, looks at the caller: code with the debugger's mark inlines nothing. A caller
without the mark can call a function that has debug hooks:

- code of a global object without a debugger that calls a function of a global object with one,
- the body of a generator or an async function that is suspended when the debugger attaches, which keeps the code
  it started in.

In both, a breakpoint or a `debugger` statement in the callee was skipped for every call that ran inlined: 8,507
of 10,000 calls in the test.

inliningCost now also refuses a callee whose code has debug hooks. A private builtin has the mark and no hooks,
and stays inlinable.

JSTests/stress/dfg-does-not-inline-a-function-that-has-debug-hooks.js has the three callers.
@robobun
robobun force-pushed the robobun/aa32a760/unlinked-code-block-per-mode branch from 93c3757 to fcb227a Compare October 22, 2026 01:17
@robobun
robobun force-pushed the robobun/aa32a760/no-inlining-of-debug-hooks branch from d14e779 to a8110c4 Compare October 22, 2026 01:17

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed the latest push and found no bugs; the earlier testLoopCount nit is addressed. Since this changes DFG/FTL inlining policy under the JavaScriptCore codeowner paths, a human look is still worthwhile.

What was reviewed:

  • The new guard in ByteCodeParser::inliningCost(): wasCompiledWithDebuggingOpcodes() && !isPrivateBuiltinFunction() is the same predicate as BytecodeGenerator::shouldEmitDebugHooks(), so it refuses exactly the callees whose bytecode contains op_debug, and it runs before the targetCodeBlock/FTL selection so both tiers are covered.
  • Checked the ordering relative to the existing m_hasDebuggerEnabled and null-codeBlock checks — the callee CodeBlock is non-null when dereferenced.
  • The test now takes its count from testLoopCount, fails only by throwing, and the $vm helpers it uses (dfgTrue, enableDebuggerModeWhenIdle) exist in this fork.
Extended reasoning...

The change adds a USE(BUN_JSC_ADDITIONS)-guarded 12-line check to Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp that refuses to inline a callee compiled with debugger opcodes unless it is a private builtin, plus a new JSTests/stress test covering two cross-realm callers and a suspended generator. No security-sensitive surface is touched; this is JIT inlining policy interacting with the debugger. The decision to defer rather than approve rests on the files being covered by .github/CODEOWNERS (/Source/JavaScriptCore and /JSTests) and on the change altering DFG/FTL inlining behaviour, where the test's reliance on the callee not tiering up on its own is an empirical property best confirmed by a maintainer and CI. The new commit since the last review addressed the one prior nit (testLoopCount), and no outstanding CHANGES_REQUESTED review exists in the timeline.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant