Skip to content

Restore the stack pointer when a JS measure function throws - #2032

Open
greekr4 wants to merge 1 commit into
react:mainfrom
greekr4:fix-measure-exception-stack
Open

greekr4 wants to merge 1 commit into
react:mainfrom
greekr4:fix-measure-exception-stack

Conversation

@greekr4

@greekr4 greekr4 commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

If a measure function throws during calculateLayout(), the exception unwinds the WebAssembly frames without restoring Emscripten's stack pointer (emscripten#21350), so every throw leaks stack in proportion to the layout depth. The stack is 64 KB and sits directly above static data. Once enough has leaked, later calls fail with RuntimeError: memory access out of bounds (usually in YGNodeNew), and the module can't recover without being reloaded. Freeing nodes in finally doesn't prevent this, because the leak is on the stack, not the heap.

On main built from source, it takes 156 throws with a single measured node, and 18 when the measured node is 20 levels deep.

Changes

  • calculateLayout() saves the stack pointer before calling into Wasm. If an exception comes back, it restores the pointer and rethrows, so callers still get the original error. The nodes whose layout was interrupted stay dirty and are measured again on the next pass.
  • stackSave and stackRestore are added to EXPORTED_RUNTIME_METHODS.

#2033 handles the same problem for dirtied functions.

Test Plan

New tests in YGMeasureTest.test.ts:

  • measure_func_exception_propagates_to_caller
  • layout_works_after_repeated_measure_func_exceptions: 1,000 throws from a node 20 levels deep, then a normal layout returns the expected size. On main it fails with memory access out of bounds.
  • node_is_measured_again_after_its_measure_func_threw

Checks run with Node 20 and emsdk 4.0.23 on an Apple M2:

  • In javascript/: yarn test passes 38 suites and 576 tests (573 before this change). yarn lint and yarn tsc are clean, and yarn pack succeeds.
  • yarn benchmark, three runs each: 3.7–5.3 ms per case on main, 3.2–5.3 ms with this change.
  • At the repo root: yarn format-check-javascript and yarn format-check-cpp pass. I moved my local, untracked javascript/.emsdk aside first, because Prettier would otherwise check it. I didn't run the Kotlin and Python checks, which need a local JDK; this change doesn't touch those files.

An exception thrown by a measure function unwinds the WebAssembly
frames of calculateLayout without restoring Emscripten's stack
pointer, so every throw leaks stack in proportion to the layout depth.
After enough throws the stack overwrites static data and later calls
fail with "memory access out of bounds", usually in YGNodeNew.

Save the stack pointer before calling into the layout and restore it if
an exception propagates, then rethrow. stackSave and stackRestore are
added to EXPORTED_RUNTIME_METHODS.
@meta-cla meta-cla Bot added the CLA Signed label Sep 30, 2026
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Sep 30, 2026

This branch has not been deployed

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

Labels

CLA Signed Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant