Skip to content

fix(vm): reserve jump-table entry 0 for try/finally fallthrough#5381

Open
tkshsbcue wants to merge 1 commit into
boa-dev:mainfrom
tkshsbcue:fix-finally-fallthrough-untaken-return
Open

fix(vm): reserve jump-table entry 0 for try/finally fallthrough#5381
tkshsbcue wants to merge 1 commit into
boa-dev:mainfrom
tkshsbcue:fix-finally-fallthrough-untaken-return

Conversation

@tkshsbcue
Copy link
Copy Markdown
Contributor

The jump table emitted at the end of a try/finally block uses the finally_throw_index register to dispatch to a break/continue/return that was pending when control transferred into the finally. The index register is initialised to 0 and only updated by HandleFinally, so 0 represents "no jump record was selected — fall through past the table".

#4852 made JumpTable index its address array directly and dropped the explicit default address. The same PR also removed the +1 offset that HandleFinally used to apply to its index, so the initial value 0 now collides with the first registered jump record. Any return, break, or continue syntactically present (but not executed) inside a try block whose finally runs is then taken as soon as the finally completes, even though control should fall through to the code after the try statement.

A minimal reproducer (which silently breaks React 19's dispatchSetStateInternal):

function g(x) {
    try { if (x) return -1; } catch (e) {} finally {}
    return 42;
}
g(0); // returned `undefined`, should be `42`

Restore the +1 offset in HandleFinally and size the emitted jump table at N + 1 entries: entry 0 is patched to point past all of the jump-record handlers, and entries 1..=N continue to dispatch to the registered records. A jump emitted right after the table skips the record handlers in the fallthrough case so that the new entry 0 target is only reachable through the table.
Fixes #5369 as well!

The jump table emitted at the end of a `try/finally` block uses the
`finally_throw_index` register to dispatch to a `break`/`continue`/`return`
that was pending when control transferred into the finally. The index
register is initialised to `0` and only updated by `HandleFinally`, so
`0` represents "no jump record was selected — fall through past the
table".

boa-dev#4852 made `JumpTable` index its address array directly and dropped the
explicit default address. The same PR also removed the `+1` offset that
`HandleFinally` used to apply to its index, so the initial value `0`
now collides with the first registered jump record. Any `return`,
`break`, or `continue` syntactically present (but not executed) inside
a `try` block whose `finally` runs is then taken as soon as the finally
completes, even though control should fall through to the code after
the `try` statement.

A minimal reproducer (which silently breaks React 19's
`dispatchSetStateInternal`):

    function g(x) {
        try { if (x) return -1; } catch (e) {} finally {}
        return 42;
    }
    g(0); // returned `undefined`, should be `42`

Restore the `+1` offset in `HandleFinally` and size the emitted jump
table at `N + 1` entries: entry `0` is patched to point past all of the
jump-record handlers, and entries `1..=N` continue to dispatch to the
registered records. A jump emitted right after the table skips the
record handlers in the fallthrough case so that the new entry `0`
target is only reachable through the table.

Fixes boa-dev#5369.
@tkshsbcue tkshsbcue requested a review from a team as a code owner May 21, 2026 11:20
@github-actions github-actions Bot added Waiting On Review Waiting on reviews from the maintainers C-Tests Issues and PRs related to the tests. C-VM Issues and PRs related to the Boa Virtual Machine. and removed Waiting On Review Waiting on reviews from the maintainers labels May 21, 2026
@github-actions github-actions Bot added this to the v1.0.0 milestone May 21, 2026
@github-actions
Copy link
Copy Markdown

Test262 conformance changes

Test result main count PR count difference
Total 53,125 53,125 0
Passed 51,071 51,071 0
Ignored 1,482 1,482 0
Failed 572 572 0
Panics 0 0 0
Conformance 96.13% 96.13% 0.00%

Tested main commit: 8f5ef6542d641fd22320e51234e914b59e623717
Tested PR commit: 1fdef75378315be5058ad638d12444b84dc2077e
Compare commits: 8f5ef65...1fdef75

@codecov
Copy link
Copy Markdown

codecov Bot commented May 21, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 60.04%. Comparing base (6ddc2b4) to head (1fdef75).
⚠️ Report is 967 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff             @@
##             main    #5381       +/-   ##
===========================================
+ Coverage   47.24%   60.04%   +12.80%     
===========================================
  Files         476      566       +90     
  Lines       46892    63027    +16135     
===========================================
+ Hits        22154    37846    +15692     
- Misses      24738    25181      +443     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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

Labels

C-Tests Issues and PRs related to the tests. C-VM Issues and PRs related to the Boa Virtual Machine.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Empty finally block causes function to return undefined when try block contains a conditional return

1 participant