Skip to content

Commit 953cd5b

Browse files
committed
Issue #5 resolution: CM-4 review + wasmtime determinism evidence; IROH-1 sharpened
Spec-history review (operator prompt: is this wasmtime lagging a recent spec change?): NO - both sides are ancient. The exclusivity model dates to >=2025-08-20 (#553); sync-streams.wast's contrary assertion to 2025-09-05 (file birth); the in-window commits #650/#656 (May 2026) renamed exclusive->exclusive_thread and refined the reentrance definition without touching release points. ~11-month-old inconsistency. Structural finding: nobody upstream can see it. Spec CI runs only run_tests.py (the wast suite never executes against the reference); wasmtime runs the suite via its tests/component-model submodule (e8d8005, bumped 2026-07-24) with an exception ledger sync-streams is not on. Each CI green against its own half. Determinism (the operator's easier path, replacing the planned custom wast): wasmtime's pass on the arbiter is deterministic BY CONSTRUCTION (D.run never yields between rendezvous and the set call - no race exists at the entry-gate check) and BY MEASUREMENT (50/50 identical passes, wasmtime 49.0.0-dev; the 47.0.1 release CLI cannot parse post-#655 suite syntax). The official test already contains the full IROH-1 shape - the interloper is admitted, runs, and clobbers shared memory (address 16) while the resolved task sits parked mid-frame - so no custom experiment was needed: exams/wasmtime-exclusivity/ holds RESULTS.md + run.sh. IROH-1 rewritten with the sharpened boundary: PRE-resolution blocks are safe everywhere; POST-resolution blocks admit same-instance interleaving per the suite-pinned semantics wasmtime and deltic both implement; wasmtime's green endpoint row is survivorship under one deterministic schedule, not a guarantee. CM-4 gains the review + evidence appendix.
1 parent 6a73409 commit 953cd5b

4 files changed

Lines changed: 177 additions & 11 deletions

File tree

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# wasmtime exclusivity check — is the sync-streams pass deterministic?
2+
3+
Evidence backing `upstream-component-model-repo-findings.md` CM-4's
4+
2026-08-09 review and the IROH-1 consumer finding. Question posed by the
5+
operator: wasmtime runs the official CM test suite — is *its* pass on the
6+
CM-4 arbiter (`test/async/sync-streams.wast`) itself scheduling accident?
7+
8+
**Answer: no — deterministic semantics.** A custom counter-experiment wast
9+
was planned and then dropped as redundant: the official test already
10+
contains the full IROH-1 shape, and wasmtime's pass on it is deterministic
11+
by construction and by measurement.
12+
13+
## What the official test already demonstrates
14+
15+
`test/async/sync-streams.wast` (spec repo, e8d8005 vintage and current):
16+
17+
1. `$C.get` calls `task.return` (RESOLVED) and then parks **mid-frame** in a
18+
synchronous `stream.write` (first to the rendezvous).
19+
2. `$D.run` — sync-lowered, so it regains control at `get`'s *resolution*,
20+
while `get`'s thread is still parked — rendezvous-reads, then calls
21+
`$C.set` **without ever yielding in between** (straight-line core wasm).
22+
3. Line 145 asserts `set` reports **STARTED**: the same-instance task was
23+
admitted, and its body *executed* (it runs its own `stream.read` and
24+
memory traffic — clobbering address 16, which `get`'s parked frame also
25+
uses — before blocking). Under `definitions.py`'s `exclusive_thread`
26+
lifetime, `set` would be gated (STARTING) and the guest traps
27+
`unreachable` at line 146.
28+
29+
That is: entry admitted + interloper body run + shared state mutated, all
30+
while a resolved task sits parked mid-frame — the IROH-1 collision shape,
31+
in the official corpus, asserted as the *expected* behavior.
32+
33+
## Why the pass is deterministic (structure)
34+
35+
Between the rendezvous read and the `set` call, `$D.run` executes
36+
straight-line wasm with no suspension point; execution is single-threaded
37+
and cooperative. The machine state at the entry-gate check is therefore a
38+
pure function of the program, and STARTED-vs-gated is decided solely by the
39+
gating rule (wasmtime: `ConcurrentInstanceState.do_not_enter`, scoped to
40+
the sync-call span, ending at resolution). No race exists at the arbiter.
41+
42+
## Measurement
43+
44+
wasmtime 49.0.0-dev (`3ebfbe5af`, 2026-08-07 — a current-main dev release;
45+
the 47.0.1 release CLI cannot parse the post-#655 suite syntax):
46+
47+
```sh
48+
./run.sh # fetches nothing; expects `wasmtime` ≥ the 2026-07 suite syntax on PATH
49+
```
50+
51+
Result: **50/50 identical silent passes** (exit 0) of
52+
`third_party/component-model/test/async/sync-streams.wast` with
53+
`-W component-model-async=y -W component-model-more-async-builtins=y`.
54+
55+
deltic's own lane passes the same file deterministically (green under
56+
`DELTIC_SCHED_SEED=1` and `=4242`; see `harness/src/xfail.ts`'s
57+
sync-streams note and CI).
58+
59+
## CI provenance (who actually runs this suite)
60+
61+
- The spec repo's CI runs **only** `design/mvp/canonical-abi/run_tests.py`
62+
— the wast suite is never executed against the reference.
63+
- wasmtime vendors the spec repo as the `tests/component-model` submodule
64+
(bumped to e8d8005 on 2026-07-24, commit b6cb744) and runs the suite in
65+
CI with an explicit exception ledger; `sync-streams.wast` is not on it.
66+
67+
So the reference↔suite contradiction (CM-4) has no upstream detector: each
68+
CI is green against its own half.
69+
70+
## The one true "scheduling accident" in the story
71+
72+
Downstream only: the *semantics* (suite-pinned, wasmtime-implemented,
73+
deltic-implemented) **admit** same-instance task execution during a
74+
resolved task's mid-frame block; whether an admitted interleaving lands on
75+
a *colliding* point (polymorph-iroh's `RefCell` window, IROH-1) is a
76+
scheduler choice — deterministic per host, guaranteed by nothing. wasmtime's
77+
endpoint-matrix green and deltic's ~90% trap rate are the same semantics
78+
under different deterministic schedules.

exams/wasmtime-exclusivity/run.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env sh
2+
# Determinism check for wasmtime on the CM-4 arbiter (see RESULTS.md).
3+
# Requires a `wasmtime` new enough to parse the post-#655 suite syntax
4+
# (a 2026-08+ dev release works; the 47.0.1 release CLI does not).
5+
set -eu
6+
cd "$(dirname "$0")/../.."
7+
WAST=third_party/component-model/test/async/sync-streams.wast
8+
WASMTIME="${WASMTIME:-wasmtime}"
9+
"$WASMTIME" --version
10+
N="${N:-50}"
11+
i=1
12+
while [ "$i" -le "$N" ]; do
13+
"$WASMTIME" wast -W component-model-async=y -W component-model-more-async-builtins=y "$WAST"
14+
i=$((i + 1))
15+
done
16+
echo "$N/$N passes: deterministic"

upstream-component-model-repo-findings.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,48 @@ Scope the reference's entry gate to the sync-call-in-progress span (release
212212
on resolution + block), or document the divergence and mark the suite test
213213
as the normative source.
214214

215+
### 2026-08-09 review: not a recent-spec-change lag, and structurally invisible upstream
216+
217+
Reviewed on operator prompt (3-month spec-history window + wasmtime CI
218+
provenance + determinism check; full transcripts in
219+
`exams/wasmtime-exclusivity/RESULTS.md`):
220+
221+
- **Both sides are ancient.** The exclusivity model (`exclusive`, then
222+
`exclusive_thread`) with the hold-for-the-activation lifetime dates to
223+
≥ 2025-08-20 (#553); `sync-streams.wast`'s contrary assertion dates to
224+
the file's birth, 2025-09-05 (9b5aa62). The in-window commits — #650
225+
(2026-05-21, `exclusive: Task``exclusive_thread: Thread` + the
226+
entering-set reentrance definition) and #656 (2026-05-29, cooperative
227+
thread built-ins) — refined granularity without touching the release
228+
points. This is a ~11-month-old inconsistency, not wasmtime lagging a
229+
recent change (nor the reverse).
230+
- **No CI cross-checks the two.** The spec repo's CI runs only
231+
`run_tests.py` (definitions.py's own unit tests); the wast suite is
232+
never executed against the reference. wasmtime runs the suite via a
233+
`tests/component-model` submodule (currently e8d8005, bumped 2026-07-24)
234+
with an explicit exception ledger for known misalignments (e.g.
235+
`post-return.wast` pending #680 alignment) — `sync-streams.wast` is not
236+
on it. So: wasmtime CI green, spec CI green, and the reference↔suite
237+
contradiction has no detector by construction.
238+
- **wasmtime's pass is deterministic, not scheduling accident.**
239+
Structurally: `$D.run` never yields between the first rendezvous and the
240+
`$C.set` call, so the entry-gate check occurs at a fully deterministic
241+
machine state — the STARTED-vs-gated outcome is a pure function of the
242+
gating rule. Empirically: 50/50 identical passes under wasmtime
243+
49.0.0-dev (3ebfbe5af, 2026-08-07) with
244+
`-W component-model-async=y -W component-model-more-async-builtins=y`.
245+
(Vintage note: the wasmtime 47.0.1 *release* CLI cannot even parse the
246+
current suite text — its bundled wast crate predates the 2026-07 #655
247+
syntax adherence pass — so any 47-era corroboration must use the crate
248+
APIs or a dev build.)
249+
250+
Net: the filing should present this as an internal spec-repo inconsistency
251+
(reference vs its own test corpus) that only external implementations can
252+
currently observe, propose the gate-scoping fix (or normative-source
253+
ruling), and suggest the structural fix — run the wast suite against the
254+
reference (or at least flag reference-affecting suite assertions) in the
255+
spec repo's own CI.
256+
215257
---
216258

217259
## NOTE-1: several official async tests assume the deterministic profile

upstream-consumer-findings.md

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ filing is the operator's (foreign repos).
99

1010
---
1111

12-
## IROH-1 — endpoint holds a `RefCell` borrow across a `block_on` yield (DRAFT)
12+
## IROH-1 — endpoint holds a `RefCell` borrow across a post-resolution `block_on` (DRAFT)
1313

1414
**Repo:** polymorph-iroh. **Where:** `endpoint/src/endpoint_impl.rs:13`
1515
(claim: "the `RefCell` borrows never cross an await") vs the actual path:
@@ -21,9 +21,10 @@ State::drain() # under shared.borrow_mut()
2121
-> wit_bindgen::block_on(polymorph:webcrypto/signature#signing-key.sign)
2222
```
2323

24-
`block_on` on an async import is a yield point (callback-ABI activation
25-
returns to the host and resumes later), so other tasks run while the
26-
borrow is live. Every other endpoint task parks in `wait_until`
24+
`block_on` on an async import blocks the thread mid-frame (sync
25+
`waitable-set.wait` under the callback ABI); whether other same-instance
26+
tasks may run during that window is the load-bearing question — see the
27+
sharpened semantics below. Every other endpoint task parks in `wait_until`
2728
(`endpoint_impl.rs:939`) whose first act is `shared.borrow_mut()`
2829
`RefCell already borrowed``unreachable` trap.
2930

@@ -36,16 +37,45 @@ handshake. The 5 ms bounded-polling cadence (their jco workaround)
3637
re-arms `wait_until` on the same timescale as the signing window, making
3738
the collision near-certain on any host that interleaves there.
3839

39-
**Why jco/wasmtime legs didn't surface it:** scheduling luck, not
40-
absence — holding a `RefCell` borrow across a yield is illegal on any
41-
conforming host. wasmtime's interleaving choices happen not to run the
42-
poller inside that window (their matrix row is green); deltic's
43-
do (see https://github.com/lann/deltic/issues/5 open question on the `bridge.ts` exclusivity
44-
divergence, which widens — but does not create — the window).
40+
**The precise semantics (sharpened 2026-08-09; see
41+
`upstream-component-model-repo-findings.md` CM-4 and
42+
`exams/wasmtime-exclusivity/RESULTS.md`):** the boundary is
43+
**resolution**.
44+
45+
- *Before* `task.return`, a callback task's instance-entry gate holds
46+
across mid-frame blocks on every implementation surveyed (deltic,
47+
wasmtime, and `definitions.py` alike) — borrows held across a
48+
pre-resolution `block_on` are safe.
49+
- *After* `task.return`, the semantics the official suite pins
50+
(`test/async/sync-streams.wast:208` — the interloper task is admitted,
51+
its body runs, and it touches shared instance memory while the resolved
52+
task sits parked mid-frame; wasmtime runs this suite in CI via its
53+
`tests/component-model` submodule and passes it **deterministically**,
54+
50/50 measured on wasmtime 49.0.0-dev) **admit other same-instance
55+
tasks running while the resolved task's thread is blocked mid-frame**.
56+
wasmtime's own gate (`ConcurrentInstanceState.do_not_enter`) ends at
57+
resolution. `definitions.py` as written disagrees (its
58+
`exclusive_thread` is held for the whole activation) — that
59+
contradiction is CM-4, a separate filing against the spec repo; the
60+
suite + wasmtime are the operative semantics today, and deltic
61+
implements them.
62+
63+
The endpoint's pump does its `block_on(sign)` **after** `bind` resolved,
64+
inside that admitted window, with the `RefCell` borrow live.
65+
66+
**Why the wasmtime leg is green anyway — deterministic schedule, not a
67+
guarantee:** wasmtime admits the same interleaving (its own suite asserts
68+
it); its particular deterministic scheduler simply never chooses the
69+
parked poller inside the signing window on this workload. deltic's
70+
deterministic scheduler does, ~90% of the time. Same semantics, different
71+
schedules — "works on wasmtime" is survivorship, and any scheduler change
72+
(wasmtime's included) can flip it.
4573

4674
**Proposed fix (guest-side):** scope the borrow inside `drain`'s inner
4775
steps, or move signing out of the borrowed region (take what `sign`
48-
needs, release, sign, re-borrow).
76+
needs, release, sign, re-borrow). General rule: treat every
77+
**post-resolution** mid-frame block as a potential same-instance
78+
interleaving point.
4979

5080
**Workaround in-tree:** the exam retries scenarios 2–4 (observed 8/20
5181
attempts trip it); residual all-attempts-fail probability < 1%.

0 commit comments

Comments
 (0)