Skip to content

Commit 149492f

Browse files
committed
v4/go-gate: resolve type-assertion and indexed-slot callback callees to the formal slot
1 parent 6bf4467 commit 149492f

4 files changed

Lines changed: 673 additions & 116 deletions

File tree

.agents/sow/current/SOW-0025-20260811-pure-go-exact-v4-port.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -944,6 +944,64 @@ wall). Real module scan rc=0 across the 5 OS configs; go test ./...
944944
both tag sets, -race, vet (module and gate), gofmt, and the
945945
import-graph boundary check over the 11 GOOS/GOARCH pairs all green.
946946

947+
Level-1 round continuation (2026-08-17, Jason P2 on type-assertion
948+
and indexed-slot callee escapes): Jason returned FAIL at HEAD 6bf4467
949+
with three P2 gate escapes, all probe-verified silent at HEAD and all
950+
reproduced by the lead:
951+
952+
1. P2-1 type-assertion callee defeats the holder fence: s.cb = fn with
953+
cb any, then s.cb.(T)(out, out) with owned buffers - the counter-check
954+
was wired only for Ident/Selector callees, so the assertion callee
955+
matched neither the flow records nor the rules counter-check; the
956+
identity-func roundtrip cast(s.cb.(T))(out, out) (cast := func(f F) F
957+
{ return f }) was equally silent.
958+
2. P2-2 type-assertion-to-local breaks the alias chain: f := s.cb.(T);
959+
f(out, out) - noteCallbackInvokes accepted RHS recordings only as
960+
*ast.Ident and paramAliasedFuncVar required an Ident initializer, so
961+
the assertion result was neither a scanned callback nor a policed
962+
holding.
963+
3. P2-3 indexed slot holders are not recorded: arr[0] = fn, map
964+
m["cb"] = fn, slice literals hs := []func{fn}, and slice fields
965+
s.hs = []func{fn} invoked as arr[0](out, out) etc. - slot recording
966+
handled only Ident/Selector LHS and callee resolution only
967+
Ident/Selector functions.
968+
969+
Fixed with one shared callee-resolution authority (slotOfExpr in
970+
pageflow.go, with its rules-side counterpart callbackSlotOf in
971+
rules.go): a func-typed expression resolves to the callback formal
972+
slot through the formal itself, identity/assertion aliases
973+
(f := fn.(T), f := s.cb.(T), var box any = fn; f := box.(T)),
974+
struct-field holders, indexed container slots (keyed by root object,
975+
selector path, and constant index; non-constant indices fail closed
976+
with a catch-all key), and type assertions of any of these. The
977+
flow pass records type-assert RHSs in the identity and param-alias
978+
passes, seeds indexAliases from indexed assignments AND array/slice/
979+
map composite literals ([]func{fn} seeds hs[0]), records return
980+
wrappers (id := func(f F) F { return f }) as returnAliases so
981+
id(x)(out, out) resolves the callee to the callback bound to x, and
982+
the composition and rootSlot resolve through the same authority.
983+
The rules counter-check now fires for TypeAssertExpr, IndexExpr/
984+
IndexListExpr, and call-typed callees, and forwardsCallbackFormal and
985+
paramAliasedFuncVar resolve the new shapes (paramAliasedFuncVar also
986+
accepts flow-recorded any-holder aliases, so a box-asserted callback
987+
forwarded to a helper is scanned instead of over-rejected).
988+
989+
All nine Jason liar shapes now fail (type-assert callee, array slot,
990+
map slot, assert-to-local, slice literal, slice field, identity-
991+
return roundtrip, box-assert forwarded, field-assert forwarded),
992+
per-OS as expected, while the honest twin (assertion alias forwarded
993+
through a helper with mapped views, box- or field-derived) stays
994+
clean; helper-internal direct holder invocations with traced params
995+
stay over-rejected exactly like the pre-existing alias/field classes
996+
(verified identical to the 6bf4467 binary on the mapped honest
997+
controls). Pinned durably as P311 (nine liar shapes) and P312
998+
(honest assertion-alias forwarding twins). Battery: 620 -> 622 cases
999+
(528 rejections, 94 benign acceptances), zero misses under
1000+
--self-test-jobs 24 (1:47 wall). Real module scan rc=0 across the 5
1001+
OS configs; go test ./... both tag sets, -race, vet (module and
1002+
gate), gofmt, and the import-graph boundary check over the 11
1003+
GOOS/GOARCH pairs all green.
1004+
9471005
## Review Process (user decision, 2026-08-12)
9481006

9491007
1. Implement the milestone work, always long-term-best and minimal-complete.

v4/go-gate/battery_page.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1177,4 +1177,9 @@ var batteryPageCases = []batteryCase{
11771177
{name: "P310 benign: mint-before-invocation helper and mapped cross-helper alias chain pass", desc: "CopyPage(src, dst, fn) implementations that (a) mint the mapped views BEFORE invoking the callback formal inside a helper (position-faithful single-assignment locals), or (b) alias the formal and forward it through a two-helper chain with mapped views: the store contract is satisfied, and the position-stability guard and the alias composition must not over-reject the honest forwarding", expectFail: false, ops: []batteryOp{
11781178
batteryOp{kind: "create", path: "internal/reader/gatemut_lin_ok.go", content: "package reader\n\nimport \"github.com/firehol/iprange/v4/go/internal/tree\"\n\n// P310a: mint BEFORE the callback invocation inside the helper stays\n// honest: the mapped view is position-faithful, so the exemption must\n// not fail the store forwarding.\ntype honestLin310 struct{ r *ImmutableReader }\n\nfunc (s honestLin310) TargetTxn() uint64 { return 0 }\nfunc (s honestLin310) PageLimit() uint64 { return 0 }\nfunc (s honestLin310) Inspect(uint32, func([]byte) error) error { return nil }\nfunc (s honestLin310) Allocate() (uint32, error) { return 0, nil }\nfunc (s honestLin310) Update(uint32, func([]byte) error) error { return nil }\nfunc (s honestLin310) DiscardPrivate(uint32) error { return nil }\n\nfunc (s honestLin310) CopyPage(source, destination uint32, fn func(src, output []byte) error) error {\n\treturn launderMintFirstLin310(fn, &s)\n}\n\nfunc launderMintFirstLin310(fn func(src, output []byte) error, s *honestLin310) error {\n\tp, perr := s.r.page(0)\n\tif perr != nil {\n\t\treturn perr\n\t}\n\tv := p\n\terr := fn(v, v)\n\t_ = v\n\treturn err\n}\n\n// P310b: the same cross-helper alias chain with mapped views stays\n// honest: the composition resolves the alias and the traced byte\n// parameters are mapped at the call site.\ntype honestChainLin310 struct{ r *ImmutableReader }\n\nfunc (s honestChainLin310) TargetTxn() uint64 { return 0 }\nfunc (s honestChainLin310) PageLimit() uint64 { return 0 }\nfunc (s honestChainLin310) Inspect(uint32, func([]byte) error) error { return nil }\nfunc (s honestChainLin310) Allocate() (uint32, error) { return 0, nil }\nfunc (s honestChainLin310) Update(uint32, func([]byte) error) error { return nil }\nfunc (s honestChainLin310) DiscardPrivate(uint32) error { return nil }\n\nfunc (s honestChainLin310) CopyPage(source, destination uint32, fn func(src, output []byte) error) error {\n\tx, err := s.r.page(source)\n\tif err != nil {\n\t\treturn err\n\t}\n\ty, err := s.r.page(destination)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn h1OkLin310(fn, x, y)\n}\n\nfunc h1OkLin310(fn func(src, output []byte) error, x, y []byte) error {\n\tcb := fn\n\treturn h2OkLin310(cb, x, y)\n}\n\nfunc h2OkLin310(fn func(src, output []byte) error, a, b []byte) error { return fn(a, b) }\n\nfunc honestProbe310(x *ImmutableReader, src, dst uint32) error {\n\tvar st1 tree.Store = honestLin310{r: x}\n\tvar st2 tree.Store = honestChainLin310{r: x}\n\t_ = st1.CopyPage(src, dst, func(a, b []byte) error { copy(b, a); return nil })\n\treturn st2.CopyPage(src, dst, func(a, b []byte) error { copy(b, a); return nil })\n}"},
11791179
}},
1180-
}
1180+
{name: "P311: type-assertion callees, assertion aliases, and indexed slot holders launder the store callback", desc: "CopyPage(src, dst, fn) implementations invoking the callback through a type assertion of an any-hosting field (s.cb = fn; s.cb.(T)(out, out)), an assertion alias (f := s.cb.(T); f(out, out), or var box any = fn; f := box.(T)), an indexed container slot (arr[0] = fn; arr[0](out, out); m[\"cb\"] = fn; m[\"cb\"](out, out); hs := []func{fn}; hs[0](out, out); s.hs[0]), or a return wrapper (id := func(f F) F { return f }; id(s.cb.(T))(out, out)): the callee resolution and the store-callback counter-check previously covered only Ident and field-selection callees, so these clean owned buffers laundered into the callback with zero diagnostics even though the honest mapped twins were rejected; the fences now resolve assertions, indexed slots, and func-valued wrapper results to the formal slot", expectFail: true, ops: []batteryOp{
1181+
batteryOp{kind: "create", path: "internal/reader/gatemut_jason.go", content: "package reader\n\nimport \"github.com/firehol/iprange/v4/go/internal/tree\"\n\ntype cbSig = func(src, output []byte) error\n\n// J1: any-hosting field, type-assert callee, owned buffers.\ntype liarJ1 struct{ cb any }\n\nfunc (s *liarJ1) TargetTxn() uint64 { return 0 }\nfunc (s *liarJ1) PageLimit() uint64 { return 0 }\nfunc (s *liarJ1) Inspect(uint32, func([]byte) error) error { return nil }\nfunc (s *liarJ1) Allocate() (uint32, error) { return 0, nil }\nfunc (s *liarJ1) DiscardPrivate(uint32) error { return nil }\nfunc (s *liarJ1) Update(uint32, func([]byte) error) error { return nil }\nfunc (s *liarJ1) CopyPage(source, destination uint32, fn cbSig) error {\n\ts.cb = fn\n\tout := make([]byte, 4096)\n\treturn s.cb.(cbSig)(out, out)\n}\n\n// J2: array slot holder, owned buffers.\ntype liarJ2 struct{}\n\nfunc (s *liarJ2) TargetTxn() uint64 { return 0 }\nfunc (s *liarJ2) PageLimit() uint64 { return 0 }\nfunc (s *liarJ2) Inspect(uint32, func([]byte) error) error { return nil }\nfunc (s *liarJ2) Allocate() (uint32, error) { return 0, nil }\nfunc (s *liarJ2) DiscardPrivate(uint32) error { return nil }\nfunc (s *liarJ2) Update(uint32, func([]byte) error) error { return nil }\nfunc (s *liarJ2) CopyPage(source, destination uint32, fn cbSig) error {\n\tvar arr [1]cbSig\n\tarr[0] = fn\n\tout := make([]byte, 4096)\n\treturn arr[0](out, out)\n}\n\n// J3: map slot holder, owned buffers.\ntype liarJ3 struct{}\n\nfunc (s *liarJ3) TargetTxn() uint64 { return 0 }\nfunc (s *liarJ3) PageLimit() uint64 { return 0 }\nfunc (s *liarJ3) Inspect(uint32, func([]byte) error) error { return nil }\nfunc (s *liarJ3) Allocate() (uint32, error) { return 0, nil }\nfunc (s *liarJ3) DiscardPrivate(uint32) error { return nil }\nfunc (s *liarJ3) Update(uint32, func([]byte) error) error { return nil }\nfunc (s *liarJ3) CopyPage(source, destination uint32, fn cbSig) error {\n\tm := map[string]cbSig{}\n\tm[\"cb\"] = fn\n\tout := make([]byte, 4096)\n\treturn m[\"cb\"](out, out)\n}\n\n// J4: assertion alias of a field holder, owned buffers.\ntype liarJ4 struct{ cb any }\n\nfunc (s *liarJ4) TargetTxn() uint64 { return 0 }\nfunc (s *liarJ4) PageLimit() uint64 { return 0 }\nfunc (s *liarJ4) Inspect(uint32, func([]byte) error) error { return nil }\nfunc (s *liarJ4) Allocate() (uint32, error) { return 0, nil }\nfunc (s *liarJ4) DiscardPrivate(uint32) error { return nil }\nfunc (s *liarJ4) Update(uint32, func([]byte) error) error { return nil }\nfunc (s *liarJ4) CopyPage(source, destination uint32, fn cbSig) error {\n\ts.cb = fn\n\tf := s.cb.(cbSig)\n\tout := make([]byte, 4096)\n\treturn f(out, out)\n}\n\n// J5: slice-literal element holder, owned buffers.\ntype liarJ5 struct{}\n\nfunc (s *liarJ5) TargetTxn() uint64 { return 0 }\nfunc (s *liarJ5) PageLimit() uint64 { return 0 }\nfunc (s *liarJ5) Inspect(uint32, func([]byte) error) error { return nil }\nfunc (s *liarJ5) Allocate() (uint32, error) { return 0, nil }\nfunc (s *liarJ5) DiscardPrivate(uint32) error { return nil }\nfunc (s *liarJ5) Update(uint32, func([]byte) error) error { return nil }\nfunc (s *liarJ5) CopyPage(source, destination uint32, fn cbSig) error {\n\ths := []cbSig{fn}\n\tout := make([]byte, 4096)\n\treturn hs[0](out, out)\n}\n\n// J6: struct slice-field element holder, owned buffers.\ntype liarJ6 struct{ hs []cbSig }\n\nfunc (s *liarJ6) TargetTxn() uint64 { return 0 }\nfunc (s *liarJ6) PageLimit() uint64 { return 0 }\nfunc (s *liarJ6) Inspect(uint32, func([]byte) error) error { return nil }\nfunc (s *liarJ6) Allocate() (uint32, error) { return 0, nil }\nfunc (s *liarJ6) DiscardPrivate(uint32) error { return nil }\nfunc (s *liarJ6) Update(uint32, func([]byte) error) error { return nil }\nfunc (s *liarJ6) CopyPage(source, destination uint32, fn cbSig) error {\n\ts.hs = []cbSig{fn}\n\tout := make([]byte, 4096)\n\treturn s.hs[0](out, out)\n}\n\n// J7: identity-return wrapper roundtrip, owned buffers.\ntype liarJ7 struct{ cb any }\n\nfunc (s *liarJ7) TargetTxn() uint64 { return 0 }\nfunc (s *liarJ7) PageLimit() uint64 { return 0 }\nfunc (s *liarJ7) Inspect(uint32, func([]byte) error) error { return nil }\nfunc (s *liarJ7) Allocate() (uint32, error) { return 0, nil }\nfunc (s *liarJ7) DiscardPrivate(uint32) error { return nil }\nfunc (s *liarJ7) Update(uint32, func([]byte) error) error { return nil }\nfunc (s *liarJ7) CopyPage(source, destination uint32, fn cbSig) error {\n\ts.cb = fn\n\tout := make([]byte, 4096)\n\tcast := func(sig cbSig) cbSig { return sig }\n\treturn cast(s.cb.(cbSig))(out, out)\n}\n\n// J8: box-assertion alias forwarded to a helper, owned buffers.\ntype liarJ8 struct{}\n\nfunc (s *liarJ8) TargetTxn() uint64 { return 0 }\nfunc (s *liarJ8) PageLimit() uint64 { return 0 }\nfunc (s *liarJ8) Inspect(uint32, func([]byte) error) error { return nil }\nfunc (s *liarJ8) Allocate() (uint32, error) { return 0, nil }\nfunc (s *liarJ8) DiscardPrivate(uint32) error { return nil }\nfunc (s *liarJ8) Update(uint32, func([]byte) error) error { return nil }\nfunc (s *liarJ8) CopyPage(source, destination uint32, fn cbSig) error {\n\tvar box any = fn\n\tf := box.(cbSig)\n\tout := make([]byte, 4096)\n\treturn runJ311(f, out, out)\n}\n\n// J9: field-assertion alias forwarded to a helper, owned buffers.\ntype liarJ9 struct{ cb any }\n\nfunc (s *liarJ9) TargetTxn() uint64 { return 0 }\nfunc (s *liarJ9) PageLimit() uint64 { return 0 }\nfunc (s *liarJ9) Inspect(uint32, func([]byte) error) error { return nil }\nfunc (s *liarJ9) Allocate() (uint32, error) { return 0, nil }\nfunc (s *liarJ9) DiscardPrivate(uint32) error { return nil }\nfunc (s *liarJ9) Update(uint32, func([]byte) error) error { return nil }\nfunc (s *liarJ9) CopyPage(source, destination uint32, fn cbSig) error {\n\ts.cb = fn\n\tf := s.cb.(cbSig)\n\tout := make([]byte, 4096)\n\treturn runJ311(f, out, out)\n}\n\nfunc runJ311(fn cbSig, a, b []byte) error { return fn(a, b) }\n\nfunc liarProbeJ311(page []byte) error {\n\tvar st1 tree.Store = &liarJ1{}\n\tvar st2 tree.Store = &liarJ2{}\n\tvar st3 tree.Store = &liarJ3{}\n\tvar st4 tree.Store = &liarJ4{}\n\tvar st5 tree.Store = &liarJ5{}\n\tvar st6 tree.Store = &liarJ6{}\n\tvar st7 tree.Store = &liarJ7{}\n\tvar st8 tree.Store = &liarJ8{}\n\tvar st9 tree.Store = &liarJ9{}\n\t_ = st1.CopyPage(0, 1, func(src, output []byte) error { copy(output, page); return nil })\n\t_ = st2.CopyPage(0, 1, func(src, output []byte) error { copy(output, page); return nil })\n\t_ = st3.CopyPage(0, 1, func(src, output []byte) error { copy(output, page); return nil })\n\t_ = st4.CopyPage(0, 1, func(src, output []byte) error { copy(output, page); return nil })\n\t_ = st5.CopyPage(0, 1, func(src, output []byte) error { copy(output, page); return nil })\n\t_ = st6.CopyPage(0, 1, func(src, output []byte) error { copy(output, page); return nil })\n\t_ = st7.CopyPage(0, 1, func(src, output []byte) error { copy(output, page); return nil })\n\t_ = st8.CopyPage(0, 1, func(src, output []byte) error { copy(output, page); return nil })\n\treturn st9.CopyPage(0, 1, func(src, output []byte) error { copy(output, page); return nil })\n}\n"},
1182+
}},
1183+
{name: "P312 benign: assertion aliases forwarded through a helper with mapped views pass", desc: "CopyPage(src, dst, fn) implementations sourcing mapped views from s.r.page and forwarding them through an assertion alias of the formal (var box any = fn; f := box.(T); runJ312(f, x, y), or f := s.cb.(T) on an any field holding the formal): the store contract is satisfied through the indirection, and the assertion/non-interface-holder recordings must not over-reject the honest forwarding", expectFail: false, ops: []batteryOp{
1184+
batteryOp{kind: "create", path: "internal/reader/gatemut_jason_ok.go", content: "package reader\n\nimport \"github.com/firehol/iprange/v4/go/internal/tree\"\n\ntype cbSig = func(src, output []byte) error\n\n// K1: box-assertion alias of the formal forwarded to a helper with\n// mapped views.\ntype honestK1 struct{ r *ImmutableReader }\n\nfunc (s honestK1) TargetTxn() uint64 { return 0 }\nfunc (s honestK1) PageLimit() uint64 { return 0 }\nfunc (s honestK1) Inspect(uint32, func([]byte) error) error { return nil }\nfunc (s honestK1) Allocate() (uint32, error) { return 0, nil }\nfunc (s honestK1) DiscardPrivate(uint32) error { return nil }\nfunc (s honestK1) Update(uint32, func([]byte) error) error { return nil }\nfunc (s honestK1) CopyPage(source, destination uint32, fn cbSig) error {\n\tx, err := s.r.page(source)\n\tif err != nil {\n\t\treturn err\n\t}\n\ty, err := s.r.page(destination)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar box any = fn\n\tf := box.(cbSig)\n\treturn runJ312(f, x, y)\n}\n\n// K2: field-assertion alias (any field holding the formal) forwarded\n// to a helper with mapped views.\ntype honestK2 struct{ r *ImmutableReader; cb any }\n\nfunc (s *honestK2) TargetTxn() uint64 { return 0 }\nfunc (s *honestK2) PageLimit() uint64 { return 0 }\nfunc (s *honestK2) Inspect(uint32, func([]byte) error) error { return nil }\nfunc (s *honestK2) Allocate() (uint32, error) { return 0, nil }\nfunc (s *honestK2) DiscardPrivate(uint32) error { return nil }\nfunc (s *honestK2) Update(uint32, func([]byte) error) error { return nil }\nfunc (s *honestK2) CopyPage(source, destination uint32, fn cbSig) error {\n\tx, err := s.r.page(source)\n\tif err != nil {\n\t\treturn err\n\t}\n\ty, err := s.r.page(destination)\n\tif err != nil {\n\t\treturn err\n\t}\n\ts.cb = fn\n\tf := s.cb.(cbSig)\n\treturn runJ312(f, x, y)\n}\n\nfunc runJ312(fn cbSig, a, b []byte) error { return fn(a, b) }\n\nfunc honestProbeJ312(x *ImmutableReader, src, dst uint32) error {\n\tvar st1 tree.Store = honestK1{r: x}\n\tvar st2 tree.Store = &honestK2{r: x}\n\t_ = st1.CopyPage(src, dst, func(a, b []byte) error { copy(b, a); return nil })\n\treturn st2.CopyPage(src, dst, func(a, b []byte) error { copy(b, a); return nil })\n}\n"},
1185+
}}}

0 commit comments

Comments
 (0)