Skip to content

Commit 8409289

Browse files
feat(tw-verify): #10 — enforce L13 module-isolation on emitted wasm (typed-wasm contract widening) (#280)
First Stage-E widening slice (the agent-scoped doable-now, high-value level). Emitted-wasm enforcement was L7(aliasing)+L10(linearity) only. This adds **L13 module isolation, negative form**: - `Tw_verify.ModuleNotIsolated` + `verify_module_isolation`: a module that owns its own linear memory yet also imports a memory or table has a cross-module shared-state channel outside the declared function-import boundary. AffineScript codegen always emits a private memory and never imports one, so a violation is a codegen/isolation regression — exactly the class tw_verify catches on emitted wasm. - Wired into `verify_from_module`, gated behind the same `affinescript.ownership` presence so the "no section ⇒ Ok" contract is preserved. `pp_error` case added. - **Carrier-free**: reads only the standard wasm import/memory sections — NO ownership-section wire-format change, so the multi-producer ABI (ephapax + typed-wasm) is untouched (no unilateral ABI change, per the ECOSYSTEM contract). Tests: `test/test_tw_isolation.ml` (5 cases — clean module Ok; imported memory + own memory ⇒ ModuleNotIsolated; imported table ⇒ flagged; pure-consumer shim not in scope; no-ownership-section ⇒ Ok contract). `dune test --force` 295/295. Zero regression. ECOSYSTEM/TECH-DEBT coverage truthed. L1–6/L14–16 need a new region/capability carrier section = a multi-producer ABI proposal (filed for typed-wasm; explicitly NOT implemented unilaterally). The Rust-verifier mirror (typed-wasm crates/typed-wasm-verify) and INT-12 C5.1 are tracked cross-repo follow-ups. Refs #234 #235. Stage-E typed-wasm convergence widening (CONV-04). Not Closes — Stage E continues; owner closes per ISSUE-CLOSURE.
1 parent 4df2e01 commit 8409289

5 files changed

Lines changed: 163 additions & 6 deletions

File tree

docs/ECOSYSTEM.adoc

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,16 @@ The contract is *narrower than older prose claimed* and is exactly this:
123123
remain the spec of record until the cross-compat suite is supplemented
124124
with real AffineScript-emitted fixtures (typed-wasm "C5.1", deferred).
125125
. *Coverage today.* Emitted-wasm enforcement is *Level 7 (aliasing) +
126-
Level 10 (linearity) only*. L1–6 and L13–16 (module isolation, session
127-
protocols, resource capabilities, agent choreography) are proven at the
128-
spec level but *not* enforced on AffineScript-emitted wasm. Widening this
129-
is Stage E.
126+
Level 10 (linearity)*, plus *Level 13 (module isolation, negative
127+
form)* — `Tw_verify.verify_module_isolation` rejects a module that
128+
owns linear memory yet also imports a memory/table (carrier-free; no
129+
ownership-section ABI change). L1–6, L14–16 (region schema, session
130+
protocols, resource capabilities, agent choreography) remain proven
131+
at the spec level but *not* enforced on AffineScript-emitted wasm:
132+
they require a *new carrier section* (region/type-schema or
133+
capability data), which is a multi-producer ABI change coordinated
134+
with `hyperpolymath/typed-wasm` + `ephapax` — not made unilaterally
135+
here. Further widening + INT-12 is the rest of Stage E.
130136
. *Other producers.* `ephapax` (`src/ephapax-wasm`) already emits the same
131137
`affinescript.ownership` section and exposes the verifier via
132138
`ephapax compile --verify-ownership`. Any change to the section format is

docs/TECH-DEBT.adoc

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,11 @@ structural-conservative recogniser) |S2 |open #234
225225
|CONV-03 |#225/#160 convergence ABI matured to "shared with Ephapax" |S1
226226
|partial (PR3a/b/c merged)
227227
|CONV-04 |Widen emitted-wasm enforcement beyond L7+L10 toward L1–6/L13–16 |S2
228-
|planned (Stage E)
228+
|L13 (module isolation, negative form) DONE —
229+
`Tw_verify.verify_module_isolation` (carrier-free, no ABI change).
230+
L1–6/L14–16 need a new carrier section = multi-producer ABI proposal
231+
(filed for typed-wasm; NOT unilateral). Rust-verifier mirror + C5.1 =
232+
cross-repo follow-ups
229233
|CONV-05 |INT-12: AffineScript-emitted fixtures into typed-wasm
230234
`crates/typed-wasm-verify/tests/cross_compat.rs` (typed-wasm "C5.1") |S1
231235
|planned — coordinate Refs `hyperpolymath/typed-wasm`

lib/tw_verify.ml

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,17 @@ type ownership_error =
5454
| ExclBorrowAliased of { func_idx : int; param_idx : int; count : int }
5555
(** Level 7: ExclBorrow parameter was loaded [count] times — aliasing violation.
5656
An exclusive borrow must not have multiple simultaneous references. *)
57+
| ModuleNotIsolated of { reason : string }
58+
(** Level 13 (module isolation — the typed-wasm contract widening,
59+
issue #234/#10). The emitted module owns its own linear memory
60+
yet ALSO imports a memory or table: a cross-module shared-state
61+
channel outside the declared function-import boundary.
62+
AffineScript codegen always emits a private memory and never
63+
imports one, so this is the negative L13 property and a violation
64+
is a codegen/isolation regression — exactly the class
65+
[tw_verify] exists to catch on emitted wasm. Additive: NO
66+
ownership-section wire-format change, so the multi-producer ABI
67+
(ephapax + typed-wasm) is untouched. *)
5768

5869
(* ============================================================================
5970
Per-path use-range analysis
@@ -222,6 +233,40 @@ let parse_ownership_section_payload
222233
(func_idx, param_kinds, ret_kind)
223234
)
224235

236+
(** Level 13 — module isolation (negative form). A well-isolated
237+
module owns its private linear memory and reaches other modules
238+
ONLY through the declared function-import boundary. If the module
239+
has its own memory yet also imports a memory or a table, that
240+
imported entity is a shared-state channel outside the boundary —
241+
not isolated. (A module with no own memory that imports one is a
242+
pure consumer shim, not in scope here; AffineScript codegen never
243+
emits that shape.) Carrier-free: read only the standard wasm
244+
import/memory sections, so no multi-producer ABI change. *)
245+
let verify_module_isolation
246+
(wasm_mod : Wasm.wasm_module)
247+
: ownership_error list =
248+
if wasm_mod.Wasm.mems = [] then []
249+
else
250+
List.filter_map
251+
(fun (im : Wasm.import) ->
252+
match im.Wasm.i_desc with
253+
| Wasm.ImportMemory ->
254+
Some (ModuleNotIsolated {
255+
reason =
256+
Printf.sprintf
257+
"module owns linear memory yet imports memory '%s.%s' \
258+
(cross-module shared memory breaks L13 isolation)"
259+
im.Wasm.i_module im.Wasm.i_name })
260+
| Wasm.ImportTable ->
261+
Some (ModuleNotIsolated {
262+
reason =
263+
Printf.sprintf
264+
"module owns linear memory yet imports table '%s.%s' \
265+
(externally-backed table breaks L13 isolation)"
266+
im.Wasm.i_module im.Wasm.i_name })
267+
| Wasm.ImportFunc _ -> None)
268+
wasm_mod.Wasm.imports
269+
225270
(** Verify a Wasm module using the embedded [affinescript.ownership] custom
226271
section. This is the primary entry point for the pipeline and the CLI.
227272
@@ -236,7 +281,12 @@ let verify_from_module
236281
Ok ()
237282
| Some payload ->
238283
let annots = parse_ownership_section_payload payload in
239-
let errors = verify_module wasm_mod annots in
284+
(* L7/L10 (ownership) + L13 (module isolation). The isolation check
285+
is gated behind the same ownership-section presence so the
286+
"no section ⇒ Ok" contract is preserved. *)
287+
let errors =
288+
verify_module wasm_mod annots @ verify_module_isolation wasm_mod
289+
in
240290
if errors = [] then Ok () else Error errors
241291

242292
(* ============================================================================
@@ -266,6 +316,8 @@ let pp_error (fmt : Format.formatter) (err : ownership_error) : unit =
266316
"Level 7 violation: function %d, param %d — ExclBorrow (mut) param \
267317
aliased (%d simultaneous references; at most 1 permitted)"
268318
func_idx param_idx count
319+
| ModuleNotIsolated { reason } ->
320+
Format.fprintf fmt "Level 13 violation: %s" reason
269321

270322
(** Format a full verification report. Prints "OK" if no errors. *)
271323
let pp_report (fmt : Format.formatter) (errs : ownership_error list) : unit =

test/test_main.ml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ let () =
1212
("Examples", Test_golden.example_tests);
1313
("Effects (#59)", Test_effects.tests);
1414
("Effect-sites (#234 S2a)", Test_effect_sites.tests);
15+
("TW L13 isolation (#10)", Test_tw_isolation.tests);
1516
] @ Test_e2e.tests @ Test_stdlib_aot.tests)

test/test_tw_isolation.ml

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
(* SPDX-License-Identifier: PMPL-1.0-or-later *)
2+
(* SPDX-FileCopyrightText: 2026 hyperpolymath *)
3+
4+
(* #10 / typed-wasm contract widening: L13 module-isolation (negative
5+
form) enforced on emitted wasm by [Tw_verify.verify_module_isolation]
6+
/ [verify_from_module]. Additive — no ownership-section ABI change. *)
7+
8+
open Affinescript
9+
10+
let mem () : Wasm.memory = { Wasm.mem_type = { Wasm.lim_min = 1; lim_max = None } }
11+
12+
let imp m n d : Wasm.import = { Wasm.i_module = m; i_name = n; i_desc = d }
13+
14+
(* count=0 ownership payload (u32le 0) — present so verify_from_module
15+
does not short-circuit on "no ownership section ⇒ Ok". *)
16+
let own_section = ("affinescript.ownership", Bytes.make 4 '\000')
17+
18+
let is_isolation_err = function
19+
| Tw_verify.ModuleNotIsolated _ -> true
20+
| _ -> false
21+
22+
let test_clean_module_ok () =
23+
let m =
24+
{ (Wasm.empty_module ()) with
25+
Wasm.mems = [ mem () ];
26+
imports = [ imp "Other" "callee" (Wasm.ImportFunc 0) ];
27+
custom_sections = [ own_section ] }
28+
in
29+
match Tw_verify.verify_from_module m with
30+
| Ok () -> ()
31+
| Error errs ->
32+
Alcotest.failf "clean isolated module flagged: %d errs" (List.length errs)
33+
34+
let test_imported_memory_flagged () =
35+
let m =
36+
{ (Wasm.empty_module ()) with
37+
Wasm.mems = [ mem () ];
38+
imports = [ imp "Host" "memory" Wasm.ImportMemory ];
39+
custom_sections = [ own_section ] }
40+
in
41+
match Tw_verify.verify_from_module m with
42+
| Ok () -> Alcotest.fail "imported memory + own memory must violate L13"
43+
| Error errs ->
44+
Alcotest.(check bool) "ModuleNotIsolated reported" true
45+
(List.exists is_isolation_err errs)
46+
47+
let test_imported_table_flagged () =
48+
let m =
49+
{ (Wasm.empty_module ()) with
50+
Wasm.mems = [ mem () ];
51+
imports = [ imp "Host" "tbl" Wasm.ImportTable ];
52+
custom_sections = [ own_section ] }
53+
in
54+
Alcotest.(check bool) "imported table flagged" true
55+
(List.exists is_isolation_err (Tw_verify.verify_module_isolation m))
56+
57+
let test_no_own_memory_not_in_scope () =
58+
(* A pure consumer shim (no own memory) importing a memory is not the
59+
negative-L13 shape AffineScript emits; not flagged. *)
60+
let m =
61+
{ (Wasm.empty_module ()) with
62+
Wasm.mems = [];
63+
imports = [ imp "Host" "memory" Wasm.ImportMemory ];
64+
custom_sections = [ own_section ] }
65+
in
66+
Alcotest.(check int) "no isolation errors" 0
67+
(List.length (Tw_verify.verify_module_isolation m))
68+
69+
let test_no_ownership_section_contract () =
70+
(* Even a would-be violation: no ownership section ⇒ Ok (the
71+
pre-existing contract is preserved; isolation is gated behind it). *)
72+
let m =
73+
{ (Wasm.empty_module ()) with
74+
Wasm.mems = [ mem () ];
75+
imports = [ imp "Host" "memory" Wasm.ImportMemory ];
76+
custom_sections = [] }
77+
in
78+
match Tw_verify.verify_from_module m with
79+
| Ok () -> ()
80+
| Error _ -> Alcotest.fail "no ownership section must remain Ok"
81+
82+
let tests =
83+
[
84+
Alcotest.test_case "clean isolated module is Ok" `Quick
85+
test_clean_module_ok;
86+
Alcotest.test_case "imported memory + own memory violates L13" `Quick
87+
test_imported_memory_flagged;
88+
Alcotest.test_case "imported table violates L13" `Quick
89+
test_imported_table_flagged;
90+
Alcotest.test_case "no-own-memory consumer not in scope" `Quick
91+
test_no_own_memory_not_in_scope;
92+
Alcotest.test_case "no ownership section ⇒ Ok (contract)" `Quick
93+
test_no_ownership_section_contract;
94+
]

0 commit comments

Comments
 (0)