From dbf1cc0381c305ebbfa232fc068ada8e069e58fa Mon Sep 17 00:00:00 2001 From: TKman <102001532+greekr4@users.noreply.github.com> Date: Wed, 30 Sep 2026 18:59:38 +0900 Subject: [PATCH] Restore the stack pointer when a JS measure function throws 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. --- javascript/CMakeLists.txt | 2 +- javascript/src/wrapAssembly.ts | 11 +++- javascript/tests/YGMeasureTest.test.ts | 77 ++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 2 deletions(-) diff --git a/javascript/CMakeLists.txt b/javascript/CMakeLists.txt index ea3eefd6c1..5b89810435 100644 --- a/javascript/CMakeLists.txt +++ b/javascript/CMakeLists.txt @@ -49,7 +49,7 @@ add_link_options( # both bundlders and Node. "SHELL:-s SINGLE_FILE=1" "SHELL:-s ENVIRONMENT='web'" - "SHELL:-s EXPORTED_RUNTIME_METHODS=['HEAPF32','HEAP32']" + "SHELL:-s EXPORTED_RUNTIME_METHODS=['HEAPF32','HEAP32','stackSave','stackRestore']" "SHELL:-Wl,--export-dynamic") add_library(yogaObjLib OBJECT ${YOGA_SOURCES} ${BRIDGE_SOURCES}) diff --git a/javascript/src/wrapAssembly.ts b/javascript/src/wrapAssembly.ts index f6689a2ca9..596ecaf7e7 100644 --- a/javascript/src/wrapAssembly.ts +++ b/javascript/src/wrapAssembly.ts @@ -939,7 +939,16 @@ export default function wrapAssembly(lib: any): Yoga { height: number | 'auto' | undefined = NaN, direction: Direction = Direction.LTR, ): void { - lib._YGNodeCalculateLayout(this._ptr, width, height, direction); + // An exception thrown by a measure function unwinds the WebAssembly + // frames of the layout without restoring the stack pointer. Restore it, + // or repeated exceptions overflow the stack into static memory. + const stack = lib.stackSave(); + try { + lib._YGNodeCalculateLayout(this._ptr, width, height, direction); + } catch (e) { + lib.stackRestore(stack); + throw e; + } } // --- Layout getters --- diff --git a/javascript/tests/YGMeasureTest.test.ts b/javascript/tests/YGMeasureTest.test.ts index fe651f01ae..27b7533b03 100644 --- a/javascript/tests/YGMeasureTest.test.ts +++ b/javascript/tests/YGMeasureTest.test.ts @@ -64,3 +64,80 @@ test('dont_fail_with_incomplete_measure_dimensions', () => { expect(node3.getComputedWidth()).toBe(100); expect(node3.getComputedHeight()).toBe(0); }); + +function createTreeWithThrowingMeasureFunc(depth: number) { + const root = Yoga.Node.create(); + let parent = root; + for (let i = 0; i < depth; i++) { + const child = Yoga.Node.create(); + parent.insertChild(child, 0); + parent = child; + } + + const leaf = Yoga.Node.create(); + leaf.setMeasureFunc(() => { + throw new Error('measure failed'); + }); + parent.insertChild(leaf, 0); + + return root; +} + +test('measure_func_exception_propagates_to_caller', () => { + const root = createTreeWithThrowingMeasureFunc(0); + + expect(() => + root.calculateLayout(undefined, undefined, Yoga.DIRECTION_LTR), + ).toThrow('measure failed'); +}); + +test('layout_works_after_repeated_measure_func_exceptions', () => { + // Without restoring the stack pointer, this breaks after about 18 throws. + for (let i = 0; i < 1000; i++) { + const root = createTreeWithThrowingMeasureFunc(20); + expect(() => + root.calculateLayout(undefined, undefined, Yoga.DIRECTION_LTR), + ).toThrow('measure failed'); + } + + const root = Yoga.Node.create(); + root.setWidth(100); + root.setHeight(100); + root.setAlignItems(Yoga.ALIGN_FLEX_START); + + const root_child0 = Yoga.Node.create(); + root_child0.setMeasureFunc(() => ({width: 10, height: 10})); + root.insertChild(root_child0, 0); + root.calculateLayout(undefined, undefined, Yoga.DIRECTION_LTR); + + expect(root_child0.getComputedWidth()).toBe(10); + expect(root_child0.getComputedHeight()).toBe(10); +}); + +test('node_is_measured_again_after_its_measure_func_threw', () => { + let shouldThrow = true; + + const root = Yoga.Node.create(); + root.setWidth(100); + root.setHeight(100); + root.setAlignItems(Yoga.ALIGN_FLEX_START); + + const root_child0 = Yoga.Node.create(); + root_child0.setMeasureFunc(() => { + if (shouldThrow) { + throw new Error('measure failed'); + } + return {width: 10, height: 20}; + }); + root.insertChild(root_child0, 0); + + expect(() => + root.calculateLayout(undefined, undefined, Yoga.DIRECTION_LTR), + ).toThrow('measure failed'); + + shouldThrow = false; + root.calculateLayout(undefined, undefined, Yoga.DIRECTION_LTR); + + expect(root_child0.getComputedWidth()).toBe(10); + expect(root_child0.getComputedHeight()).toBe(20); +});