[GH-ISSUE #733] Crash: stale insert/remove on destroyed parent triggers Yoga wasm Out of bounds call_indirect #200

Open
opened 2026-03-02 23:45:13 +03:00 by kerem · 0 comments
Owner

Originally created by @GreyElaina on GitHub (Feb 24, 2026).
Original GitHub issue: https://github.com/anomalyco/opentui/issues/733

Summary

@opentui/solid can call _insertNode -> parent.add(...) on a parent whose Yoga node was already freed. In that state, Yoga wasm traps with:

  • RuntimeError: Out of bounds call_indirect (evaluating 'd.apply(null, p)')

This looks like a missing destroyed-parent guard in core/reconciler paths.

Environment

  • @opentui/core: 0.1.81
  • @opentui/solid: 0.1.81
  • yoga-layout: 3.2.1
  • Bun: 1.3.9
  • OS: macOS 15.6 (Darwin arm64)

Deterministic minimal repro

import { insertNode, testRender } from "@opentui/solid"

function App() {
  return (
    <box id="root-shell">
      <box id="parent" />
      <box id="child" />
    </box>
  )
}

const setup = await testRender(App, { width: 40, height: 10 })
await setup.renderOnce()

const root = setup.renderer.root
const parent = root.findDescendantById("parent")
const child = root.findDescendantById("child")
if (!parent || !child) throw new Error("expected nodes")

parent.destroy()

// Simulate stale reconciler write shape
insertNode(parent as never, child as never)

Run:

bun -r @opentui/solid/preload ./repro-solid-insert-node-after-destroy.tsx

Observed stack:

RuntimeError: Out of bounds call_indirect (evaluating 'd.apply(null, p)')
  at yoga-wasm-base64-esm.js:33:52
  at add (.../@opentui/src/Renderable.ts:1146:19)
  at _insertNode (.../@opentui/solid/index.js:455:15)

Real-world evidence (non-synthetic)

In a real app flow (fork panel close + async updates), instrumentation showed:

  • parent Yoga node freed first,
  • then _insertNode -> add on that same parent,
  • then _removeNode -> remove on that same parent.

Example from trace:

free parentId=604
insertChild parentId=604 parentFreed=true
removeChild parentId=604 parentFreed=true

And mapped metadata:

parentMeta={"renderableId":"box-222","ctor":"BoxRenderable","destroyed":true,...}
stackTop includes: _insertNode -> Renderable.add

Expected behavior

Operations targeting destroyed parents should be safely ignored (or warned in dev), not crash the process in Yoga wasm.

Proposed fix

  1. Core guard in Renderable:

    • add(...): early return if this._isDestroyed
    • insertBefore(...): early return if this._isDestroyed
    • remove(...): early return if this._isDestroyed
  2. Solid guard in reconciler:

    • _insertNode(...): no-op if parent is destroyed
    • _removeNode(...): no-op if parent is destroyed

These are defensive checks: healthy flows should be unaffected; stale work becomes benign instead of fatal.

Additional context

This seems in the same family as prior lifecycle/reconciler race fixes, but I could not find an existing issue that exactly covers destroyed-parent Yoga calls in solid + core.

Originally created by @GreyElaina on GitHub (Feb 24, 2026). Original GitHub issue: https://github.com/anomalyco/opentui/issues/733 ## Summary `@opentui/solid` can call `_insertNode -> parent.add(...)` on a parent whose Yoga node was already freed. In that state, Yoga wasm traps with: - `RuntimeError: Out of bounds call_indirect (evaluating 'd.apply(null, p)')` This looks like a missing destroyed-parent guard in core/reconciler paths. ## Environment - `@opentui/core`: `0.1.81` - `@opentui/solid`: `0.1.81` - `yoga-layout`: `3.2.1` - Bun: `1.3.9` - OS: macOS 15.6 (Darwin arm64) ## Deterministic minimal repro ```tsx import { insertNode, testRender } from "@opentui/solid" function App() { return ( <box id="root-shell"> <box id="parent" /> <box id="child" /> </box> ) } const setup = await testRender(App, { width: 40, height: 10 }) await setup.renderOnce() const root = setup.renderer.root const parent = root.findDescendantById("parent") const child = root.findDescendantById("child") if (!parent || !child) throw new Error("expected nodes") parent.destroy() // Simulate stale reconciler write shape insertNode(parent as never, child as never) ``` Run: ```bash bun -r @opentui/solid/preload ./repro-solid-insert-node-after-destroy.tsx ``` Observed stack: ```text RuntimeError: Out of bounds call_indirect (evaluating 'd.apply(null, p)') at yoga-wasm-base64-esm.js:33:52 at add (.../@opentui/src/Renderable.ts:1146:19) at _insertNode (.../@opentui/solid/index.js:455:15) ``` ## Real-world evidence (non-synthetic) In a real app flow (fork panel close + async updates), instrumentation showed: - parent Yoga node freed first, - then `_insertNode -> add` on that same parent, - then `_removeNode -> remove` on that same parent. Example from trace: ```text free parentId=604 insertChild parentId=604 parentFreed=true removeChild parentId=604 parentFreed=true ``` And mapped metadata: ```text parentMeta={"renderableId":"box-222","ctor":"BoxRenderable","destroyed":true,...} stackTop includes: _insertNode -> Renderable.add ``` ## Expected behavior Operations targeting destroyed parents should be safely ignored (or warned in dev), not crash the process in Yoga wasm. ## Proposed fix 1. Core guard in `Renderable`: - `add(...)`: early return if `this._isDestroyed` - `insertBefore(...)`: early return if `this._isDestroyed` - `remove(...)`: early return if `this._isDestroyed` 2. Solid guard in reconciler: - `_insertNode(...)`: no-op if parent is destroyed - `_removeNode(...)`: no-op if parent is destroyed These are defensive checks: healthy flows should be unaffected; stale work becomes benign instead of fatal. ## Additional context This seems in the same family as prior lifecycle/reconciler race fixes, but I could not find an existing issue that exactly covers destroyed-parent Yoga calls in `solid + core`.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/opentui#200
No description provided.