Skip to content

fix(signals): migrate height-adjust heap entries in markDisposal to keep zombie flag and queue in sync#2760

Open
spokodev wants to merge 1 commit into
solidjs:nextfrom
spokodev:fix/scheduler-zombie-heap-migration
Open

fix(signals): migrate height-adjust heap entries in markDisposal to keep zombie flag and queue in sync#2760
spokodev wants to merge 1 commit into
solidjs:nextfrom
spokodev:fix/scheduler-zombie-heap-migration

Conversation

@spokodev

Copy link
Copy Markdown

Fixes #2759

Problem

markDisposal migrates a zombified child's heap entry from dirtyQueue to zombieQueue only when the child has REACTIVE_IN_HEAP. A child that is in the heap for height adjustment only (REACTIVE_IN_HEAP_HEIGHT) is zombified but left physically linked in dirtyQueue.

When the pending disposal commits, disposeChildren picks the queue to delete from by the (now set) zombie flag, so deleteFromHeap patches zombieQueue's bucket while the node is linked in dirtyQueue. If the node was a bucket head, dirtyQueue._heap[height] keeps pointing at a disposed node whose in-heap bits are cleared and whose links are reset. The next runHeap that reaches that bucket calls adjustHeightdeleteFromHeap, which early-returns on the flag check without unlinking, so the loop re-reads the same bucket head forever — a permanent main-thread livelock. #2759 documents the full four-step mechanism with a standalone repro (<Show> remount after async data raises a condition memo's height).

Fix

In markDisposal, migrate height-adjust entries too, and pick the source queue from the pre-zombify flags. This preserves the invariant every deleteFromHeap call site relies on: a node's REACTIVE_ZOMBIE flag always agrees with the queue the node is physically linked in.

Test

The new test replicates the issue's repro at the signals level (a conditional subtree whose condition expression memo grows in height after async data lands, parked above the heights the unmount flush visits) and asserts the queue invariant directly — every node physically linked in a heap carries an in-heap flag — instead of letting the corrupted state livelock the worker. On next it fails with a disposed zombie node (flags = 97) still linked in dirtyQueue; with the fix both queues are clean and the remount sequence completes.

@solidjs/signals: 746/746 tests pass. solid: 415/415. @solidjs/web: 3/3.

The issue also notes two adjacent hazards (adjustHeight inserting subscribers into the running heap regardless of their zombie flag, and runHeap making no progress on an unremovable bucket head). This PR intentionally fixes only the demonstrated corruption path; the hazards are left as documented in #2759.

@changeset-bot

changeset-bot Bot commented Jun 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e141459

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 9 packages
Name Type
@solidjs/signals Patch
test-integration Patch
solid-js Patch
babel-preset-solid Patch
@solidjs/web Patch
@solidjs/html Patch
@solidjs/h Patch
@solidjs/universal Patch
@solidjs/element Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@spokodev spokodev marked this pull request as ready for review June 12, 2026 18:15
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