launderrvictim.gno
10.23 Kb · 304 lines
1// Package launderrvictim is the /r/-DATA-DECLARED variant of the
2// launder-game victim. Its Immutable type is declared HERE (in /r/),
3// not in /p/launderpkg. This is the recommended inter-realm pattern:
4// realms declare their own logic data types.
5//
6// The hypothesis under test: with /r/-declared logic data, the
7// Attack H/I/J/K/L laundering shapes are structurally impossible.
8// Tests against this victim should all fail to mutate gImm.
9package launderrvictim
10
11import "gno.land/p/demo/tests/launderpkg"
12
13// Immutable is /r/-declared (the key difference from /r/laundervictim,
14// which uses /p/launderpkg.Immutable).
15type Immutable struct {
16 Field string
17}
18
19// Read is /r/launderrvictim-declared, so calling it borrow rule #1 borrows
20// m.Realm to launderrvictim.
21func (i *Immutable) Read() string { return i.Field }
22
23var gImm *Immutable
24
25func init() {
26 gImm = &Immutable{Field: "rdata-original"}
27}
28
29// GetImm hands out a pointer to gImm. Standard "victim exposes a
30// pointer to its state" antipattern — but with /r/-declared data,
31// the attacker should still be unable to write through it.
32func GetImm() *Immutable { return gImm }
33
34// ReadImm reads the current field for after-attack verification.
35func ReadImm() string { return gImm.Field }
36
37// UseAnyMutator boxes gImm as any and dispatches a /p/-declared
38// AnyMutator. This is the dangerous shape from Attack L: victim
39// boxes its own /r/-declared data through a /p/-defined interface
40// that the attacker can implement.
41func UseAnyMutator(m launderpkg.AnyMutator) {
42 m.Run(gImm)
43}
44
45// ApplyHook dispatches a caller-supplied callback on gImm. The
46// callback's parameter type is /r/launderrvictim-declared, so /p/
47// packages can't supply this hook — only /r/ realms can.
48func ApplyHook(h func(*Immutable)) {
49 h(gImm)
50}
51
52// --- /p/-type embedded / fielded inside /r/-declared types ---
53//
54// The three shapes below mix /r/-declared containers with /p/-typed
55// inner state. Even though the container types are /r/-declared,
56// the inner /p/-typed values inherit /p/'s methods — including
57// Apply-style higher-order methods that take /p/-typed callbacks.
58// /p/-attacker code can supply such callbacks. The attacker reaches
59// the inner /p/-value (read access works), then invokes Apply with
60// a /p/-declared function pointer. Inside Apply (borrow rule #2ed
61// to /r/launderrvictim), the callback runs without any borrow rule #1 or #2
62// shift (top-level /p/ fn), so the write commits under victim
63// authority.
64
65// WithEmbed embeds launderpkg.Immutable by VALUE (method promotion
66// gives WithEmbed an .Apply method).
67type WithEmbed struct {
68 launderpkg.Immutable
69}
70
71// WithPtr has a POINTER FIELD to launderpkg.Immutable.
72type WithPtr struct {
73 Inner *launderpkg.Immutable
74}
75
76// WithVal has a VALUE FIELD of launderpkg.Immutable (not embedded;
77// the field is named, no method promotion — but the value is still
78// addressable through c.Inner).
79type WithVal struct {
80 Inner launderpkg.Immutable
81}
82
83var (
84 gWithEmbed *WithEmbed
85 gWithPtr *WithPtr
86 gWithVal *WithVal
87)
88
89func init() {
90 gWithEmbed = &WithEmbed{Immutable: launderpkg.Immutable{Field: "embed-orig"}}
91 gWithPtr = &WithPtr{Inner: &launderpkg.Immutable{Field: "ptr-orig"}}
92 gWithVal = &WithVal{Inner: launderpkg.Immutable{Field: "val-orig"}}
93}
94
95func GetWithEmbed() *WithEmbed { return gWithEmbed }
96func GetWithPtr() *WithPtr { return gWithPtr }
97func GetWithVal() *WithVal { return gWithVal }
98
99func ReadEmbed() string { return gWithEmbed.Field }
100func ReadPtr() string { return gWithPtr.Inner.Field }
101func ReadVal() string { return gWithVal.Inner.Field }
102
103// --- Methods-less /p/-type inner state ---
104// launderpkg.Bare has no methods. These containers wrap Bare in
105// the three field shapes. The question: does readonly taint catch
106// a direct field write through the /r/-container's getter?
107
108type WithBareEmbed struct {
109 launderpkg.Bare
110}
111
112type WithBarePtr struct {
113 Inner *launderpkg.Bare
114}
115
116type WithBareVal struct {
117 Inner launderpkg.Bare
118}
119
120var (
121 gWithBareEmbed *WithBareEmbed
122 gWithBarePtr *WithBarePtr
123 gWithBareVal *WithBareVal
124)
125
126func init() {
127 gWithBareEmbed = &WithBareEmbed{Bare: launderpkg.Bare{Field: "bare-embed-orig"}}
128 gWithBarePtr = &WithBarePtr{Inner: &launderpkg.Bare{Field: "bare-ptr-orig"}}
129 gWithBareVal = &WithBareVal{Inner: launderpkg.Bare{Field: "bare-val-orig"}}
130}
131
132func GetWithBareEmbed() *WithBareEmbed { return gWithBareEmbed }
133func GetWithBarePtr() *WithBarePtr { return gWithBarePtr }
134func GetWithBareVal() *WithBareVal { return gWithBareVal }
135
136func ReadBareEmbed() string { return gWithBareEmbed.Field }
137func ReadBarePtr() string { return gWithBarePtr.Inner.Field }
138func ReadBareVal() string { return gWithBareVal.Inner.Field }
139
140// --- Composite containers holding /p/-typed elements ---
141// Slices, arrays, maps of methods-less /p/-Bare values and pointers.
142
143var (
144 gBareSlice []launderpkg.Bare
145 gBarePtrSlice []*launderpkg.Bare
146 gBareArr [3]launderpkg.Bare
147 gBarePtrArr [3]*launderpkg.Bare
148 gBareMap map[string]launderpkg.Bare
149 gBarePtrMap map[string]*launderpkg.Bare
150)
151
152func init() {
153 gBareSlice = []launderpkg.Bare{
154 {Field: "slice0"}, {Field: "slice1"},
155 }
156 gBarePtrSlice = []*launderpkg.Bare{
157 {Field: "ptrslice0"}, {Field: "ptrslice1"},
158 }
159 gBareArr = [3]launderpkg.Bare{
160 {Field: "arr0"}, {Field: "arr1"}, {Field: "arr2"},
161 }
162 gBarePtrArr = [3]*launderpkg.Bare{
163 {Field: "ptrarr0"}, {Field: "ptrarr1"}, {Field: "ptrarr2"},
164 }
165 gBareMap = map[string]launderpkg.Bare{
166 "a": {Field: "mapA"}, "b": {Field: "mapB"},
167 }
168 gBarePtrMap = map[string]*launderpkg.Bare{
169 "a": {Field: "ptrmapA"}, "b": {Field: "ptrmapB"},
170 }
171}
172
173func GetBareSlice() []launderpkg.Bare { return gBareSlice }
174func GetBarePtrSlice() []*launderpkg.Bare { return gBarePtrSlice }
175func GetBareArr() *[3]launderpkg.Bare { return &gBareArr }
176func GetBarePtrArr() *[3]*launderpkg.Bare { return &gBarePtrArr }
177func GetBareMap() map[string]launderpkg.Bare { return gBareMap }
178func GetBarePtrMap() map[string]*launderpkg.Bare { return gBarePtrMap }
179
180func ReadBareSlice0() string { return gBareSlice[0].Field }
181func ReadBareSlice0Then1() string { return gBareSlice[1].Field }
182func ReadBarePtrSlice0() string { return gBarePtrSlice[0].Field }
183func ReadBareArr0() string { return gBareArr[0].Field }
184func ReadBarePtrArr0() string { return gBarePtrArr[0].Field }
185func ReadBareMapA() string { return gBareMap["a"].Field }
186func ReadBarePtrMapA() string { return gBarePtrMap["a"].Field }
187
188// --- Panic/defer/recover helpers ---
189// These victim-side helpers expose scenarios where the m.Realm
190// borrow can interact with deferred calls, recover(), and panics
191// in unusual control-flow shapes.
192
193// DeferCallback installs h as a defer inside an /r/launderrvictim
194// frame, then returns. h runs at frame pop. The question: at the
195// time h is invoked, m.Realm has just been restored to caller's
196// realm by PopFrameAndReturn — but wait, defers run BEFORE
197// PopFrameAndReturn. So m.Realm should still be victim's. Does
198// the deferred h then run under victim authority?
199func DeferCallback(h func(*Immutable)) {
200 defer h(gImm)
201}
202
203// PanicAfterPushDefer pushes a defer and then panics, so the defer
204// runs as part of panic unwinding. Tests that m.Realm is correctly
205// borrowed when the defer body invokes a foreign function.
206func PanicAfterPushDefer(h func(*Immutable)) {
207 defer h(gImm)
208 panic("victim-induced panic")
209}
210
211// DeferApplyHook defers an ApplyHook call. The deferred ApplyHook
212// itself runs borrow rule #1 to /r/launderrvictim, and inside the
213// callback runs as borrow rule #1 of the attacker's realm — the standard
214// known-open Apply pattern, but now triggered via defer.
215func DeferApplyHook(h func(*Immutable)) {
216 defer ApplyHook(h)
217}
218
219// RecoverAndRetry: inside a victim method, defer a recover, write
220// something to gImm, then panic. After the recover, the function
221// returns normally. Tests that internal panic/recover doesn't leak
222// state.
223func RecoverAndRetry(h func(*Immutable)) (recovered any) {
224 defer func() {
225 recovered = recover()
226 }()
227 h(gImm)
228 return
229}
230
231// CallThenPanic invokes h synchronously and then panics. If h is
232// attacker-supplied and writes via captured pointer, this is just
233// a re-shape of ApplyHook.
234func CallThenPanic(h func(*Immutable)) {
235 h(gImm)
236 panic("victim panic after callback")
237}
238
239// CallPDeferApply: multi-level defer chain. Victim invokes a
240// /p/-method (DeferApply) on a victim-owned *launderpkg.Immutable;
241// the /p/-method defers the attacker callback. Three frames at
242// callback time: attacker.main → victim.CallPDeferApply →
243// /p/.DeferApply (deferred fn dispatches here).
244func CallPDeferApply(fn func(*launderpkg.Immutable)) {
245 gWithPtr.Inner.DeferApply(fn)
246}
247
248// --- Stored-hook plumbing ---
249//
250// Victim accepts caller-registered callbacks and dispatches them
251// LATER, from inside a /r/-victim method body. If the registered
252// callback is /p/-declared and writes through a captured /r/-stamped
253// pointer, the laundering shape is: stored callback rather than
254// callback-arg.
255
256type ImmHook func(*Immutable)
257
258var gHooks []ImmHook
259
260func RegisterHook(h ImmHook) { gHooks = append(gHooks, h) }
261func RunHooks() {
262 for _, h := range gHooks {
263 h(gImm)
264 }
265}
266
267type PlainHook func()
268
269var gPlainHooks []PlainHook
270
271func RegisterPlainHook(h PlainHook) { gPlainHooks = append(gPlainHooks, h) }
272func RunPlainHooks() {
273 for _, h := range gPlainHooks {
274 h()
275 }
276}
277
278func ClearHooks() {
279 gHooks = nil
280 gPlainHooks = nil
281}
282
283// MakeWriterClosure constructs a /r/-victim-declared closure that
284// captures gImm and writes through it. The closure body is /r/-victim-
285// declared, so borrow rule #1 fires at invocation → m.Realm = /r/-victim →
286// write commits with victim authority. Returning this closure to an
287// attacker is "consenting to write" by the victim.
288func MakeWriterClosure(value string) func() {
289 return func() {
290 gImm.Field = value
291 }
292}
293
294// MakeApplyTrampoline returns a closure that captures &gImm.Field
295// indirectly: it captures *Immutable, and dispatches a caller-supplied
296// callback fn on it. /r/-victim-declared body → borrow rule #1 → m.Realm =
297// /r/-victim. If `fn` is /p/-declared (e.g. EvilWrite), it inherits
298// victim authority. This is "victim returns a closure that's itself
299// an Apply-style trampoline" — a packaged Apply.
300func MakeApplyTrampoline() func(func(*Immutable)) {
301 return func(fn func(*Immutable)) {
302 fn(gImm)
303 }
304}