Severity: 🟠 High (robustness / self-DoS)
Location
src/reactive/internals.ts:54-60 (scheduleObserver), src/reactive/core.ts:48-56 (Signal.set), src/reactive/effect.ts:81-108 (observer — no re-entrancy guard).
Description
When batchDepth === 0, scheduleObserver invokes the observer synchronously inside Signal.set. The effect observer has no "currently running" guard, so an effect that writes to a signal it also reads (with a changing value) re-enters itself until the stack overflows:
effect(() => { count.value = count.value + 1; });
// read subscribes observer → write calls set → Object.is false →
// scheduleObserver runs observer synchronously → writes again → …
// → RangeError: Maximum call stack size exceeded
Wrapping in batch() does not help: during flushObservers, batchDepth is already 0, so the re-write recurses synchronously. Mutually-recursive effects/computeds hit the same path. Mainstream reactive libraries (Vue, Solid) detect this cycle and warn/bail rather than crashing the tab.
Impact
A single natural authoring mistake crashes the whole runtime (uncatchable RangeError unwinds the render). No untrusted input required, but the failure mode is a hard denial of service of the page.
Suggested fix
Add a re-entrancy / max-depth guard: track a per-observer isRunning flag (or an activeObservers set) and, when an observer is scheduled while already executing, either defer to a microtask or bail with a "cyclic dependency detected" warning (Vue-style depth counter). A max-depth counter on synchronous cascades is the minimal safe fix.
Filed as part of a full-codebase security & correctness audit.
Severity: 🟠 High (robustness / self-DoS)
Location
src/reactive/internals.ts:54-60(scheduleObserver),src/reactive/core.ts:48-56(Signal.set),src/reactive/effect.ts:81-108(observer — no re-entrancy guard).Description
When
batchDepth === 0,scheduleObserverinvokes the observer synchronously insideSignal.set. The effect observer has no "currently running" guard, so an effect that writes to a signal it also reads (with a changing value) re-enters itself until the stack overflows:Wrapping in
batch()does not help: duringflushObservers,batchDepthis already 0, so the re-write recurses synchronously. Mutually-recursive effects/computeds hit the same path. Mainstream reactive libraries (Vue, Solid) detect this cycle and warn/bail rather than crashing the tab.Impact
A single natural authoring mistake crashes the whole runtime (uncatchable
RangeErrorunwinds the render). No untrusted input required, but the failure mode is a hard denial of service of the page.Suggested fix
Add a re-entrancy / max-depth guard: track a per-observer
isRunningflag (or anactiveObserversset) and, when an observer is scheduled while already executing, either defer to a microtask or bail with a "cyclic dependency detected" warning (Vue-style depth counter). A max-depth counter on synchronous cascades is the minimal safe fix.Filed as part of a full-codebase security & correctness audit.