GHSA-38f7-945m-qr2g: Effect `AsyncLocalStorage` context lost/contaminated inside Effect fibers under concurrent load with RPC
Versions`effect`: 3.19.15`@effect/rpc`: 0.72.1`@effect/platform`: 0.94.2Node.js: v22.20.0Vercel runtime with Fluid computeNext.js: 16 (App Router)`@clerk/nextjs`: 6.xRoot causeEffect's `MixedScheduler` batches fiber continuations and drains them inside a **single** microtask or timer callback. The `AsyncLocalStorage` context active during that callback belongs to whichever request first triggered the scheduler's drain cycle — **not** the request that owns the fiber being resumed.Detailed mechanism1. Scheduler batching (`effect/src/Scheduler.ts`, `MixedScheduler`)`scheduleTask` only calls `starve()` when `running` is `false`. Subsequent tasks accumulate in `this.tasks` until `starveInternal` drains them all. The `Promise.then()` (or `setTimeout`) callback inherits the ALS context from whichever call site created it — i.e., whichever request's fiber first set `running = true`.**Result:** Under concurrent load, fiber continuations from Request A and Request B execute inside the same `starveInternal` call, sharing a single ALS context. If Request A triggered `starve()`, then Request B's fiber reads Request A's ALS context.2. `toWebHandlerRuntime` does not propagate ALS (`@effect/platform/src/HttpApp.ts:211-240`)Effect's own `Context` (containing `HttpServerRequest`) is correctly set per-request. But the **Node.js ALS context** — which frameworks like Next.js, Clerk, and OpenTelemetry rely on — is not captured at fork time or restored when the fiber's continuations execute.3. The dangerous pattern this enablesThe `async () => auth()` thunk executes when the fiber continuation is scheduled by `MixedScheduler`. At that point, the ALS context belongs to an arbitrary concurrent request.Reproduction scenarioMinimal reproductionImpact| Symptom | Severity |
|---------|----------|
| `auth()` returns wrong user's session | **Critical** — authentication bypass |
| `cookies()` / `headers()` from Next.js read wrong request | **High** — data leakage |
| OpenTelemetry trace context crosses requests | **Medium** — incorrect traces |
| Works locally, fails in production | Hard to diagnose — only manifests under concurrent load |WorkaroundCapture ALS-dependent values **before** entering the Effect runtime and pass them via Effect's own context system:Suggested fix (for Effect maintainers)Option A: Propagate ALS context through the schedulerCapture the `AsyncLocalStorage` snapshot when a fiber continuation is scheduled, and restore it when the continuation executes:`AsyncLocalStorage.snapshot()` (Node.js 20.5+) returns a function that, when called, restores the ALS context from the point of capture. This ensures each fiber continuation runs with its originating request's ALS context.**Trade-off:** Adds one closure allocation per scheduled task. Could be opt-in via a `FiberRef` or scheduler option.Option B: Capture ALS at `runFork` and restore per fiber stepWhen `Runtime.runFork` is called, capture the ALS snapshot and associate it with the fiber. Before each fiber step (in the fiber runtime's `evaluateEffect` loop), restore the snapshot.**Trade-off:** More invasive but provides correct ALS propagation for the fiber's entire lifetime, including across `flatMap` chains and `Effect.tryPromise` thunks.Option C: Document the limitation and provide a `context` injection APIIf ALS propagation is intentionally not supported, document this prominently and provide a first-class API for `toWebHandler` to accept per-request context. The existing `context?: Context.Context<never>` parameter on the handler function partially addresses this, but it requires callers to know about the issue and manually extract values before entering Effect.RelatedNode.js `AsyncLocalStorage` docs: https://nodejs.org/api/async_context.html`AsyncLocalStorage.snapshot()`: https://nodejs.org/api/async_context.html#static-method-asynclocalstoragesnapshotNext.js uses ALS for `cookies()`, `headers()`, `auth()` in App RouterSimilar issue pattern in other fiber-based runtimes (e.g., ZIO has `FiberRef` propagation for this)POC replica of my setupUsed util functionsThe actual effect that was run within the RPC context that the bug was found
CVSS v3.1: 7.4