Skip to content

CM-4: sync-streams.wast:145 overfits wasmtime's scheduler — upstream test-defect filing kit #43

Closed

Description

@lann

Decision (operator, 2026-08-10)

Source+trace verification (exams/wasmtime-exclusivity/wasmtime-actual-semantics.md, PR pending) showed the CM-4 premise was wrong: wasmtime does NOT release its instance-entry gate at resolution. wasmtime = hold rule (gate spans the whole core invocation, like definitions.py's exclusive_thread) + deferred entry decision (an async-lowered caller learns STARTING only after the instance's runnable work is exhausted; ready gate-holders run to exit first) + FIFO queue. deltic currently ships release-at-resolution + eager start — more permissive than wasmtime (admits same-instance execution during a resolved task's mid-frame park; the IROH-1 RefCell collision is reachable here, unreachable under wasmtime). Under the standing 'wasmtime-becomes-the-spec' assumption, deltic migrates to the wasmtime model.

Target semantics (order-robust formulation)

  1. Gate lifetime: exclusiveThread held from entry to invocation exit (initial invocation and each callback invocation), NOT released at resolution mid-frame. Reverts the release-at-BLOCK-for-resolved-holders rule.
  2. Entry decision: an async-lowered guest→guest call reports STARTING only if the callee is still unstarted after the instance's runnable work has been drained to quiescence (not FIFO-pop-one — must stay green under DELTIC_SCHED_SEED shuffles, unlike wasmtime's own FIFO-dependent pass).
  3. DeliverEvent parity: event delivery to a parked-between-invocations callback task is gated the same way as entry (wasmtime concurrent.rs:765).

Affected code (from the release-rule annotations)

  • runtime/src/task/mod.ts:337-340 (resolved-holder release at block; needsExclusive/enter/exit)
  • runtime/src/exec/boundary.ts:1611-1660 (held-guarded callback loop → returns to always-holding invariant), :627, :1170-1184
  • runtime/src/jspi/bridge.ts:540-577 (UPSTREAM DIVERGENCE block — release-at-BLOCK for resolved tasks)
  • lower trampolines / scheduler: deferred entry evaluation (design: where the drain runs; JSPI suspendability classification of async-lowered call trampolines; plain-path impact must stay zero for sync-only components per contracts/intrinsics.md)
  • harness/src/xfail.ts:321 comment (references the old model)

Gates

  • cargo build --workspace; cargo test -p translator-shim -p bindgen -p testgen
  • (cd runtime && deno task check && deno task test)
  • (cd harness && deno task conformance) — incl. sync-streams.wast green, plus DELTIC_SCHED_SEED=1 and =4242 reruns
  • (cd ct-runner && deno task test); wasi-shims, ports as affected
  • exams/iroh-endpoint: expected to flip from ~90% trap to deterministic green (the IROH-1 window closes) — rerun and update upstream-consumer-findings.md IROH-1
  • New targeted runtime test: pump/poke shape asserting STARTING while a resolved holder is parked-unready, admission only after its exit (the wasmtime-model expectations, NOT cm4-run-tests.patch's release-rule expectations)

Authorities

  • exams/wasmtime-exclusivity/wasmtime-actual-semantics.md (trace + line refs, both wasmtime vintages)
  • exams/wasmtime-exclusivity/trace-sync-streams-wasmtime-dev.log
  • third_party/component-model definitions.py (gate lifetime — now agreed with wasmtime)
  • architecture.md §1 corpus-exception clause (corrected wording in the same PR)

Scheduler-core: single-owner track (coder-hard at most) under close orchestrator review, per AGENTS.md.

Activity

  1. lann commented on Aug 10, 2026

    @lann
    CollaboratorAuthor

    Landed via PR #45 (pending checks). Gate results incl. the IROH-1 exam flip recorded in the PR body; IROH-1 marked RESOLVED-BY-HOST in upstream-consumer-findings.md.

  2. added a commit that references this issue on Aug 10, 2026
  3. lann commented on Aug 10, 2026

    @lann
    CollaboratorAuthor

    Final adjudication (operator, 2026-08-10): entry-status timing is not normative — the test overfits

    Closes out the investigation arc (#44 corrected the mechanism attribution; #45 migrated the runtime). The exams/wasmtime-exclusivity/ artifact kit is removed from the tree in the follow-up PR — the scope shrank to fit this issue. Everything of substance is distilled below; the full kit stays reachable at 4f3351f:exams/wasmtime-exclusivity/.

    The finding, final form

    test/async/sync-streams.wast:145 (spec repo) asserts that an async-lowered call to a same-instance callback-lifted export reports STARTED while the previous task is resolved (task.return delivered) but still parked mid-frame in a sync builtin — and ready to resume. That assertion is schedule-dependent, and scheduling is not normative:

    • Gate semantics are agreed by the reference, wasmtime, and the prose alike: the exclusivity gate is held for the whole core invocation (mid-frame parks included), released only between callback invocations at the event loop and at task exit. Evidence: definitions.py exclusive_thread (only release sites: event-loop wait :2187–2188, task exit :506–508); wasmtime ConcurrentInstanceState.do_not_enter — every site is an invocation bracket (enter/exit_instance concurrent.rs:2004–2021 [v47.0.3: :1998/:2008]; callback initial invocation :2652/:2662; sync-lift :2696/:2714 with the stackful exemption; per-callback invocation :942/:960; task_return never touches it); CanonicalABI.md:3740–3746.
    • What differs is a scheduler policy — when the new call's STARTING/STARTED status is decided:
      • reference: eagerly, inline at the call instant — canon_lower invokes the callee synchronously (definitions.py:2281), canon_lift creates and resumes the callee thread on the spot (:2211–2212), it parks at the gate (:484–486), and the caller packs whatever state was reached (:2306). Answer in the wast scenario: STARTING, under every schedule the reference can produce.
      • wasmtime: deferred — start_call (:3040–3160) queues the callee and suspends the caller until the first status event; a gated callee yields Status::Starting and parks pending (:1497–1522). The ready-to-resume gate holder was queued ahead, runs to invocation exit, releases the gate; only then is the new call's readiness evaluated. Answer: STARTED, deterministic under FIFO. Trace proof (archived trace-sync-streams-wasmtime-dev.log): $C.get is resumed, exited, and deleted before $C.set's readiness is ever evaluated.

    Two conforming scheduling policies over identical gate semantics give two different ABI-visible answers — so the assertion cannot be semantics. The test hard-asserts the outcome of the engine it was co-developed on (the wast corpus's documented runner is wasmtime, while the spec repo's CI runs only run_tests.py and never executes the wast corpus against the reference — the contradiction has no upstream detector by construction). Same class as NOTE-1 in upstream-component-model-repo-findings.md (tests assuming the deterministic profile), sharper instance: engine-policy overfit — the reference itself fails it deterministically.

    Nothing else is defective: definitions.py needs no change, CanonicalABI.md needs no change, test_callback_interleaving needs no change. Both previously sketched amendments (release-at-resolution, then deferred-entry normativization) are withdrawn.

    deltic disposition (landed in #45; unchanged by this adjudication)

    • Hold-lifetime gate = spec conformance.
    • Drain-to-quiescence entry decision (Store.hasRunnableWork + createAsyncStartCall, pinned by runtime/tests/entry_deferral_test.ts) = a deliberate non-normative scheduler policy: it satisfies sync-streams.wast as written, and — being drain-based rather than FIFO-order-based — it is robust under DELTIC_SCHED_SEED shuffles, unlike wasmtime's own formulation. It is legal under any upstream adjudication of the test, so there is no flip-back trigger.

    Proposed upstream fix (to file against WebAssembly/component-model)

    Make the :145 region schedule-agnostic. The test's real content is "the sibling call is eventually admitted once the holder exits, and completes correctly" — not "admitted before the caller regains control":

    1. accept STARTING or STARTED from the lower;
    2. on STARTING, waitable-set.wait for the SUBTASK event (STARTED/RETURNED), then proceed with the existing assertions;
    3. leave the rest of the file untouched.

    Secondary, structural: run the wast corpus against the reference in the spec repo's CI (or at least flag reference-affecting assertions), so reference↔corpus contradictions get a detector.

    Filing is tracked by #15. Repro, from the archived kit: verify-cm4.sh legs 0–1 reproduce the reference-side STARTING answer against pristine definitions.py (via cm4-run-tests.patch); the wast side needs a wasmtime dev build (wasmtime wast -W component-model-async=y -W component-model-more-async-builtins=y test/async/sync-streams.wast — 47-era release CLIs cannot parse the post-#655 suite syntax). Spec submodule at 73b7ad5; wasmtime refs against main as of 2026-08-10 and v47.0.3.

  4. changed the title [-]CM-4 correction: migrate entry semantics to wasmtime's hold + deferred-entry model[/-] [+]CM-4: sync-streams.wast:145 overfits wasmtime's scheduler — upstream test-defect filing kit[/+] on Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions