Skip to content
This repository was archived by the owner on Aug 12, 2026. It is now read-only.

Commit 137d8f5

Browse files
committed
own<T> transfer works: the host was freeing the handle before wasmtime read it
The prediction recorded before looking was that the guest was at fault, on the reasoning that the host writes identical bytes for own and borrow -- kind 21 and a pointer -- so it had no way to differ. That was wrong, and reading the built component said so: consume takes (own 8) and [constructor]counter returns (own 8), the same exported type. The component was correct all along. transfer! called resource_any_delete during lowering. func_call reads the pointer the argument val holds, so the host freed it before wasmtime read it, and wasmtime called that "mismatched resource types" -- an error about the wrong thing entirely, which is why the guest looked guilty. Deletes are now collected per call and run after it, in the same finally that releases the in-call flag. One atom per instance suffices because 0014 D forbids concurrent calls into a store. So 0016 is complete: a handle is opaque and AutoCloseable, close is try { drop } finally { delete } and idempotent, lowering an own transfers and leaves with-open able to exit cleanly over the body's value, lowering into a borrow leaves the handle live, and the instance closes handles the caller never destructured before deleting the store their context points into. cljwit.host now marshals every 0012 row a component can express today. What is left -- map, list<T,N>, stream/future, error-context -- is not in a shipped release. The error text is what made this a bounded question rather than a guess. Before ok! read wasmtime's message it was "call ... failed". 176 assertions. Claude-Session: https://claude.ai/code/session_01XF5Hfq4Ca2N2XYEzWQQuHt
1 parent ec54a22 commit 137d8f5

4 files changed

Lines changed: 52 additions & 48 deletions

File tree

‎doc/design/0016-own-handles.md‎

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
# 0016 — `own<T>` handles, in both directions
22

3-
**Status:** accepted for A, B and D, which are implemented and tested.
4-
**C is open** — lowering an `own` is rejected by wasmtime with `mismatched
5-
resource types` and nobody knows why yet; see the end. · 2026-07-30 · rewritten after an adversarial review built
3+
**Status:** accepted · 2026-07-30 · A, B, C and D are implemented and tested
4+
against a guest that really implements a resource · rewritten after an adversarial review built
65
the guest this note said it was missing. The file was `0016-resource-handles.md`
76
until that review showed `borrow` is not a question this note can ask.
87

@@ -150,32 +149,29 @@ instead of it."
150149
the result val. Measured true today, and the failure is a silent resource
151150
leak, so it needs re-checking on upgrade.
152151

153-
## C does not work, and the reason is not yet known
152+
## What C cost, and what the error was really about
154153

155-
Implemented and measured 2026-07-30. A handle that came from *this instance's
156-
own* `[constructor]counter`, and that works as a `borrow` across repeated
157-
`bump` calls, is refused when lowered into an `own` parameter:
154+
Lowering an `own` failed for a while with
158155

159156
```
160157
call local:res/bag@0.1.0#consume: mismatched resource types
161158
```
162159

163-
What has been ruled out: **the clone is not the cause** — skipping it entirely
164-
gives the identical message. What has not been separated is whether the fault
165-
is in the guest's `[resource-new]` / `[resource-rep]` / `[resource-drop]`
166-
imports resolving to a different resource type than the one in the exported
167-
interface, or in what the host writes into the val. A C control that does the
168-
same round trip would separate them, exactly as it did for `0013`.
169-
170-
The test asserts the failure and its message, so the day this changes a test
171-
says so rather than a comment going stale. Everything else in this note is
172-
implemented: lifting an `own`, lowering into a `borrow`, `close` as
173-
`try { drop } finally { delete }`, and the instance closing what it still
174-
holds.
175-
176-
**A side effect worth having**: chasing this added wasmtime's own error text to
177-
every `ok!`. `call … failed` became `call …: mismatched resource types`, which
178-
is what turned this from a guess into a bounded question.
160+
for a handle that came from *this instance's own* constructor and worked fine
161+
as a `borrow`. The prediction recorded before looking was that the guest was at
162+
fault — the host writes identical bytes for `own` and `borrow`, so it seemed to
163+
have no way to differ. **That was wrong**, and reading the built component
164+
showed why: `consume` takes `(own 8)` and `[constructor]counter` returns
165+
`(own 8)`, the same exported type. The component was correct.
166+
167+
The fault was `transfer!` calling `resource_any_delete` **during lowering**.
168+
`func_call` reads the pointer the argument val holds, so the host freed it
169+
before wasmtime read it, and wasmtime reported a type mismatch — an error about
170+
the wrong thing entirely. Deleting after the call fixes it; `0016` C now says
171+
so explicitly, because "delete, never drop" was true and still not enough.
172+
173+
**The error text is what made this a bounded question.** Before `ok!` read
174+
wasmtime's message, this was `call … failed`.
179175

180176
## Notes for whoever writes the guest
181177

‎doc/status.md‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,10 @@ _Short by design, and printed at every session start — so findings live in
1313
`close` as `try { drop } finally { delete }`, lowering an `own` transfers,
1414
the instance closes outstanding handles before deleting its store) and
1515
`borrow` is out of scope because it **cannot appear in a return position**.
16-
**A, B and D are implemented**: lifting an `own`, lowering into a `borrow`,
17-
`close` as `try { drop } finally { delete }`, and the instance closing
18-
handles the caller never destructured before deleting its store.
19-
**C is open** — lowering an `own` is refused with `mismatched resource
20-
types` for a handle from this instance's own constructor that works fine as
21-
a borrow. The clone is ruled out; a C control would separate guest from
22-
host, as it did for `0013`.
16+
**Done.** `cljwit.host` marshals every `0012` row that a component can
17+
express today. What is left of `0012` is `map`, `list<T,N>`,
18+
`stream`/`future` and `error-context`, none of which are in a shipped
19+
release.
2320
2. **Host imports.** `0014` names them as its most-likely-to-fire falsifier,
2421
WASI requires them, and nothing has touched them. They also invalidate
2522
`0014` E's argument-buffer reuse, which is safe today only because no guest

‎src/cljwit/host.clj‎

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@
255255

256256
(def ^:private RESOURCE 21) ; wasmtime_component_val_t's kind for a handle
257257

258-
(deftype Handle [^MemorySegment ptr ^MemorySegment ctx api registry
258+
(deftype Handle [^MemorySegment ptr ^MemorySegment ctx api registry pending
259259
^AtomicBoolean closed]
260260
java.lang.AutoCloseable
261261
(close [this]
@@ -274,11 +274,16 @@
274274
(defn- transfer!
275275
"0016 C: lowering an `own` gives the resource to the guest. The host's handle
276276
is stale immediately — dropping it afterwards is `unknown handle index` —
277-
so it is deleted, never dropped."
277+
so it is deleted, never dropped.
278+
279+
**Deleted after the call, not during lowering.** `func_call` reads the
280+
pointer the argument val holds, so freeing it while lowering hands wasmtime
281+
a dangling `resource_any_t *` — which surfaces as `mismatched resource
282+
types`, an error about the wrong thing entirely."
278283
[^Handle h]
279284
(when (.compareAndSet ^AtomicBoolean (.-closed h) false true)
280285
(swap! (.-registry h) disj h)
281-
(invoke (:res-delete (.-api h)) (.-ptr h))))
286+
(swap! (.-pending h) conj (.-ptr h))))
282287

283288
(defn- live-ptr [^Handle h what]
284289
(when (.get ^AtomicBoolean (.-closed h))
@@ -447,7 +452,8 @@
447452
(fn [^MemorySegment seg]
448453
(let [p ^MemorySegment (.get seg ADDR (long UNION))
449454
c ^MemorySegment (invoke (:res-clone (:api rt)) p)
450-
h (->Handle c (:ctx rt) (:api rt) (:registry rt) (AtomicBoolean. false))]
455+
h (->Handle c (:ctx rt) (:api rt) (:registry rt) (:pending rt)
456+
(AtomicBoolean. false))]
451457
(swap! (:registry rt) conj h)
452458
h))
453459
(scalar-lift tree))
@@ -689,7 +695,12 @@
689695
;; grow RSS by less than the same number of scalar calls do.
690696
(let [out (when rlift (rlift res))]
691697
out))
692-
(finally (.set in-call false))))))
698+
(finally
699+
(let [ps @(:pending rt)]
700+
(when (seq ps)
701+
(reset! (:pending rt) [])
702+
(run! (fn [p] (invoke (:res-delete api) p)) ps)))
703+
(.set in-call false))))))
693704

694705
(defn instantiate
695706
"Instantiates a compiled component. Cheap — tens of microseconds — so a
@@ -713,7 +724,11 @@
713724
;; the instance keeps the list and closes what is left before deleting
714725
;; the store a handle's context points into.
715726
registry (atom #{})
716-
rt {:ctx ctx :api api :registry registry}
727+
;; Handles transferred by a call, deleted once the call has read them.
728+
;; One atom per instance is enough because 0014 D forbids concurrent
729+
;; calls into a store.
730+
pending (atom [])
731+
rt {:ctx ctx :api api :registry registry :pending pending}
717732
item-kind (fn [^MemorySegment it] (bit-and (long (.get it I8 (long 0))) 0xFF))
718733
;; Every export, flattened. A function inside an interface is keyed the
719734
;; way WIT spells it — "pkg:name/iface@ver#func" — and remembers the

‎test/cljwit/host_test.clj‎

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -309,18 +309,14 @@
309309
(is (= 102 (bump (second cs) 1)))
310310
(run! (fn [^java.lang.AutoCloseable c] (.close c)) cs)))
311311

312-
;; Last: the trap this provokes poisons the instance, and every
313-
;; later call then fails with "cannot enter component instance"
314-
;; (the note in res.wat).
315-
(testing "0016 C — lowering an own does not work yet"
316-
;; wasmtime answers "mismatched resource types" for a handle
317-
;; that came from this instance's own constructor and works
318-
;; fine as a borrow. Whether the fault is the guest's
319-
;; [resource-*] imports or what the host writes into the val
320-
;; is unresolved. Asserted so the day it changes a test says
321-
;; so, rather than a comment going stale.
322-
(let [e (is (thrown? clojure.lang.ExceptionInfo (consume (make 7))))]
323-
(is (re-find #"mismatched resource types" (ex-message e)))))))))
312+
(testing "0016 C — lowering an own transfers, and with-open still exits cleanly"
313+
(is (= 7 (with-open [^java.lang.AutoCloseable c (make 7)]
314+
(consume c)))
315+
"close after a transfer must not throw over the body's value")
316+
(let [^java.lang.AutoCloseable c (make 3)]
317+
(is (= 3 (consume c)))
318+
(is (thrown? clojure.lang.ExceptionInfo (bump c 1))
319+
"the handle is stale the moment ownership moves")))))))
324320
(finally (.delete ^File c))))))
325321

326322
(deftest an-instance-closes-handles-it-still-holds

0 commit comments

Comments
 (0)