Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

launderattack.gno

7.07 Kb · 171 lines
  1// Package launderattack imports launderpkg and provides functions
  2// that attempt to mutate a /p/-typed value passed by pointer. This is
  3// the "/p/attack imports /p/orig" variation: the goal is for code
  4// declared in this /p/ package to mutate a /r/-victim's instance of
  5// launderpkg.Object.
  6package launderattack
  7
  8import "gno.land/p/demo/tests/launderpkg"
  9
 10// TamperDirect writes through the pointer in its own body. The body
 11// runs under whatever m.Realm the caller had at PushFrameCall — for a
 12// top-level /p/ function with no receiver, that's the caller's realm.
 13// If the caller is attacker-realm, m.Realm at the write is
 14// attacker-realm and the readonly check fires on a foreign-stamped
 15// base.
 16func TamperDirect(o *launderpkg.Object, s string) {
 17	o.Field = s
 18}
 19
 20// TamperViaMethod dispatches through the launderpkg.Object's own /p/
 21// method. PushFrameCall for Set sees a receiver stamped with the
 22// victim's realm (the pointer aliases victim's persisted state),
 23// triggering borrow rule 2: m.Realm becomes victim for the duration
 24// of Set. The write succeeds — but this requires launderpkg.Object
 25// to expose a Set method to begin with.
 26func TamperViaMethod(o *launderpkg.Object, s string) {
 27	o.Set(s)
 28}
 29
 30// Tamper has the SAME underlying struct layout as launderpkg.Immutable.
 31// /p/launderpkg deliberately gave Immutable no mutator method —
 32// callers were supposed to be unable to write its Field. /p/attack
 33// declares its own type with the same layout and adds a mutator. An
 34// attacker that holds a *launderpkg.Immutable can convert it to
 35// *Tamper and invoke Tamper.Set — the conversion is purely a
 36// type-tag change, the pointer still aliases victim's persisted
 37// memory. PushFrameCall for Tamper.Set then sees recv stamped with
 38// victim's realm (the underlying object is unchanged) and
 39// borrow-routes m.Realm to victim. The write succeeds.
 40type Tamper struct {
 41	Field string
 42}
 43
 44// Set is the mutator that launderpkg.Immutable deliberately did NOT
 45// expose. /p/attack adds it via the parallel-type trick.
 46func (t *Tamper) Set(s string) {
 47	t.Field = s
 48}
 49
 50// Convert is the attacker's helper that does the type punning so the
 51// caller doesn't have to write the conversion inline.
 52func Convert(p *launderpkg.Immutable) *Tamper {
 53	return (*Tamper)(p)
 54}
 55
 56// EvilMutator implements launderpkg.Mutator with a PRIMITIVE
 57// underlying type. Underlying-type matters: a struct/array/etc.
 58// receiver has a *StructValue that gets PkgID-stamped at allocation,
 59// triggering PushFrameCall's receiver-borrow rule to shift m.Realm
 60// back to /p/launderattack. A primitive-underlying type has no
 61// *StructValue and no PkgID — `recv.GetFirstObject` returns nil, so
 62// the borrow rule's `if obj != nil { ... }` branch is skipped and
 63// m.Realm stays at whatever the caller had it set to.
 64//
 65// When Run is dispatched via interface from inside a /p/-method
 66// body that was receiver-borrowed to the victim, m.Realm at Run's
 67// entry is the victim's — and stays the victim's, because EvilMutator
 68// (an int underneath) carries no PkgID to borrow against. The write
 69// inside Run commits under victim authority.
 70type EvilMutator int
 71
 72func (EvilMutator) Run(i *launderpkg.Immutable) {
 73	i.Field = "pwnd-via-iface"
 74}
 75
 76// EvilNilRecv tests the nil-pointer-receiver variant. Calling a method
 77// on a nil *EvilNilRecv is legal in Gno when the body doesn't deref
 78// the receiver. recv = PointerValue{Base: nil} → GetBase returns nil
 79// → GetFirstObject returns nil. Same "no anchor" gap as the
 80// primitive-receiver case, reachable through *T receivers.
 81type EvilNilRecv struct {
 82	X int // unused
 83}
 84
 85func (n *EvilNilRecv) Run(i *launderpkg.Immutable) {
 86	// Does NOT dereference n. Just writes through target.
 87	i.Field = "pwnd-via-nilrecv"
 88}
 89
 90// EvilFunc / EvilSlice / EvilMap — additional nil-anchor shapes.
 91// nil-valued receivers of defined types whose underlying is a
 92// reference type (slice/map/func) also have GetFirstObject == nil.
 93// The Attack H/I fix should cover all of them via the
 94// recvDeclaredTypePkgPath helper.
 95type EvilFunc func()
 96
 97func (EvilFunc) Run(i *launderpkg.Immutable) { i.Field = "pwnd-via-func" }
 98
 99type EvilSlice []int
100
101func (EvilSlice) Run(i *launderpkg.Immutable) { i.Field = "pwnd-via-slice" }
102
103type EvilMap map[string]int
104
105func (EvilMap) Run(i *launderpkg.Immutable) { i.Field = "pwnd-via-map" }
106
107// EvilSliceMutator's Run takes a slice — NOT a pointer parameter, so
108// the Attack H/I fix's `hasForeignPPtrParam` predicate skips it.
109// Tests whether the anchor predicate needs to look INSIDE composite
110// parameter types for foreign-/p/ pointers.
111type EvilSliceMutator int
112
113func (EvilSliceMutator) Run(s []*launderpkg.Immutable) {
114	s[0].Field = "pwnd-via-slice-arg"
115}
116
117// EvilAnyMutator's Run takes `any` and type-asserts to *Immutable.
118// The signature reveals NO foreign-/p/-pointer statically — the
119// pointer is hidden inside the interface box. Probes whether the
120// predicate needs to fire on interface-typed params too.
121type EvilAnyMutator int
122
123func (EvilAnyMutator) Run(x any) {
124	t := x.(*launderpkg.Immutable)
125	t.Field = "pwnd-via-any-arg"
126}
127
128// EvilWrite is a top-level /p/-declared function value matching
129// `func(*launderpkg.Immutable)`. Top-level /p/ functions trigger
130// neither borrow rule #1 (not /r/-declared) nor borrow rule #2 (no receiver), so
131// when EvilWrite is invoked as a callback from inside a
132// borrowed-to-victim /p/-method body (e.g. Immutable.Apply, or any
133// avl.Tree.Iterate-style hook), it inherits the victim's m.Realm
134// and the write commits under victim authority.
135func EvilWrite(i *launderpkg.Immutable) {
136	i.Field = "pwnd-via-apply"
137}
138
139// EvilObjectWrite is the same shape but for *Object — used by tests
140// that exercise Object's mutator surface through Apply-style callbacks.
141func EvilObjectWrite(o *launderpkg.Object) {
142	o.Field = "pwnd-via-apply-obj"
143}
144
145// StoredHook is a /p/attack package-level closure: a FuncLit evaluated
146// during /p/launderattack init, so its ObjectInfo.PkgID is stamped
147// /p/launderattack. Unlike a top-level FuncDecl (EvilWrite/EvilObjectWrite,
148// IsClosure=false), invoking a closure triggers PushFrameCall's borrow rule
149// #3, which borrows m.Realm to the closure's construction realm
150// (/p/launderattack). Used to probe rule #3: a write through it to a foreign
151// /r/ object must still be rejected. (If rule #3 ever regressed to a nil
152// borrow, m.Realm would go nil and the write would silently succeed.)
153var StoredHook = func(o *launderpkg.Object) {
154	o.Field = "pwnd-via-stored-closure"
155}
156
157// Tamperer is a stamped /p/attack value (constructed at init under
158// /p/attack's realm context, stamped /p/attack, persisted Frozen).
159// Methods on Tamperer get receiver-borrowed to /p/attack at call
160// time — useful as a control to compare against attacks where the
161// receiver is victim-stamped.
162type Tamperer struct{}
163
164// TamperMethod is a /p/attack-defined method. When the caller invokes
165// Tamperer{}.TamperMethod(o, s), the receiver Tamperer{} is
166// constructed in the caller's realm, so receiver-borrow lands at the
167// caller's realm, not /p/attack — the receiver carries the caller's
168// stamp, not /p/attack's.
169func (Tamperer) TamperMethod(o *launderpkg.Object, s string) {
170	o.Field = s
171}