Skip to content

[WIP] Interrealm take 2 #4028

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 11 commits into from
59 changes: 52 additions & 7 deletions gnovm/pkg/gnolang/machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -1969,16 +1969,56 @@
}
}

// Pop a pointer (for writing only).
func (m *Machine) PopAsPointer(lx Expr) PointerValue {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment should say, pop a pointer for writing. All calls to this will end up writing to it.

pv, ro := m.PopAsPointer2(lx)
if ro {
// panic("cannot modify external-realm or non-realm object with " + lx.String())
panic("cannot modify external-realm or non-realm object")
}
return pv
}

// Returns true if tv is N_Readonly or, its "first object" resides in a
// different non-zero realm. Returns false for non-N_Readonly StringValue, free
// floating pointers, and unreal objects.
func (m *Machine) IsReadonly(tv *TypedValue) bool {
if m.Realm == nil {
return false
}
if tv.IsReadonly() {
return true
}
tvoid, ok := tv.GetFirstObjectID()
if !ok {
// e.g. if tv is a string, or free floating pointers.
return false
}
if tvoid.IsZero() {
return false
}
if tvoid.PkgID != m.Realm.ID {
return true
}
return false
}

// Returns ro = true if the base is readonly,
// or if the base's storage realm != m.Realm and both are non-nil,
// and the lx isn't a name (base is a block),
// and the lx isn't a composit lit expr.

Check failure on line 2009 in gnovm/pkg/gnolang/machine.go

View workflow job for this annotation

GitHub Actions / Run GnoVM suite / Go Lint / lint

`composit` is a misspelling of `compost` (misspell)
func (m *Machine) PopAsPointer2(lx Expr) (pv PointerValue, ro bool) {
switch lx := lx.(type) {
case *NameExpr:
switch lx.Type {
case NameExprTypeNormal:
lb := m.LastBlock()
return lb.GetPointerTo(m.Store, lx.Path)
pv = lb.GetPointerTo(m.Store, lx.Path)
ro = false // always mutable
case NameExprTypeHeapUse:
lb := m.LastBlock()
return lb.GetPointerToHeapUse(m.Store, lx.Path)
pv = lb.GetPointerToHeapUse(m.Store, lx.Path)
ro = false // always mutable
case NameExprTypeHeapClosure:
panic("should not happen")
default:
Expand All @@ -1987,24 +2027,29 @@
case *IndexExpr:
iv := m.PopValue()
xv := m.PopValue()
return xv.GetPointerAtIndex(m.Alloc, m.Store, iv)
pv = xv.GetPointerAtIndex(m.Alloc, m.Store, iv)
ro = m.IsReadonly(xv)
case *SelectorExpr:
xv := m.PopValue()
return xv.GetPointerToFromTV(m.Alloc, m.Store, lx.Path)
pv = xv.GetPointerToFromTV(m.Alloc, m.Store, lx.Path)
ro = m.IsReadonly(xv)
case *StarExpr:
ptr := m.PopValue().V.(PointerValue)
return ptr
xv := m.PopValue()
pv = xv.V.(PointerValue)
ro = m.IsReadonly(xv)
case *CompositeLitExpr: // for *RefExpr
tv := *m.PopValue()
hv := m.Alloc.NewHeapItem(tv)
return PointerValue{
pv = PointerValue{
TV: &hv.Value,
Base: hv,
Index: 0,
}
ro = false // always mutable
default:
panic("should not happen")
}
return
}

// for testing.
Expand Down
4 changes: 2 additions & 2 deletions gnovm/pkg/gnolang/op_binary.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,8 +344,8 @@ func (m *Machine) doOpBandn() {
func isEql(store Store, lv, rv *TypedValue) bool {
// If one is undefined, the other must be as well.
// Fields/items are set to defaultValue along the way.
lvu := lv.IsUndefined()
rvu := rv.IsUndefined()
lvu := lv.IsUndefined2()
rvu := rv.IsUndefined2()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

after this in separate PR will convert all IsDefined() to IsDefined2().

if lvu {
return rvu
} else if rvu {
Expand Down
1 change: 0 additions & 1 deletion gnovm/pkg/gnolang/op_call.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ func (m *Machine) doOpPrecall() {
if cx.GetAttribute(ATTR_SHIFT_RHS) == true {
xv.AssertNonNegative("runtime error: negative shift amount")
}

m.PushOp(OpConvert)
if debug {
if len(cx.Args) != 1 {
Expand Down
2 changes: 2 additions & 0 deletions gnovm/pkg/gnolang/op_decl.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ func (m *Machine) doOpValueDecl() {
// requiring the consideration of the typed-nil case.
if nt == nil {
tv = TypedValue{}
} else if nt.Kind() == InterfaceKind {
tv = TypedValue{}
} else {
tv = TypedValue{T: nt, V: defaultValue(m.Alloc, nt)}
}
Expand Down
68 changes: 55 additions & 13 deletions gnovm/pkg/gnolang/op_expressions.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ func (m *Machine) doOpIndex1() {
}
iv := m.PopValue() // index
xv := m.PeekValue(1) // x
ro := m.IsReadonly(xv)
switch ct := baseOf(xv.T).(type) {
case *MapType:
mv := xv.V.(*MapValue)
Expand All @@ -32,6 +33,7 @@ func (m *Machine) doOpIndex1() {
res := xv.GetPointerAtIndex(m.Alloc, m.Store, iv)
*xv = res.Deref() // reuse as result
}
xv.SetReadonly(ro)
}

// NOTE: keep in sync with doOpIndex1.
Expand All @@ -43,6 +45,7 @@ func (m *Machine) doOpIndex2() {
}
iv := m.PeekValue(1) // index
xv := m.PeekValue(2) // x
ro := m.IsReadonly(xv)
switch ct := baseOf(xv.T).(type) {
case *MapType:
vt := ct.Value
Expand All @@ -69,17 +72,20 @@ func (m *Machine) doOpIndex2() {
default:
panic("should not happen")
}
xv.SetReadonly(ro)
}

func (m *Machine) doOpSelector() {
sx := m.PopExpr().(*SelectorExpr)
xv := m.PeekValue(1)
ro := m.IsReadonly(xv)
res := xv.GetPointerToFromTV(m.Alloc, m.Store, sx.Path).Deref()
if debug {
m.Printf("-v[S] %v\n", xv)
m.Printf("+v[S] %v\n", res)
}
*xv = res // reuse as result
xv.SetReadonly(ro)
}

func (m *Machine) doOpSlice() {
Expand All @@ -101,12 +107,18 @@ func (m *Machine) doOpSlice() {
}
// slice base x
xv := m.PopValue()
ro := m.IsReadonly(xv)
// if a is a pointer to an array, a[low : high : max] is
// shorthand for (*a)[low : high : max]
// XXX fix this in precompile instead.
if xv.T.Kind() == PointerKind &&
xv.T.Elem().Kind() == ArrayKind {
// simply deref xv.
*xv = xv.V.(PointerValue).Deref()
// check array also for ro.
if !ro {
ro = xv.IsReadonly()
}
}
// fill default based on xv
if sx.High == nil {
Expand All @@ -115,10 +127,10 @@ func (m *Machine) doOpSlice() {
// all low:high:max cases
if maxVal == -1 {
sv := xv.GetSlice(m.Alloc, lowVal, highVal)
m.PushValue(sv)
m.PushValue(sv.WithReadonly(ro))
} else {
sv := xv.GetSlice2(m.Alloc, lowVal, highVal, maxVal)
m.PushValue(sv)
m.PushValue(sv.WithReadonly(ro))
}
}

Expand Down Expand Up @@ -152,17 +164,13 @@ func (m *Machine) doOpStar() {
tv.SetUint8(dbv.GetByte())
m.PushValue(tv)
} else {
if pv.TV.IsUndefined() && bt.Elt.Kind() != InterfaceKind {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is vestigial; unless interface value, TV is never undefined, it at least has the concrete value set by defaultValue().

refv := TypedValue{T: bt.Elt}
m.PushValue(refv)
} else {
m.PushValue(*pv.TV)
}
ro := m.IsReadonly(xv)
pvtv := (*pv.TV).WithReadonly(ro)
m.PushValue(pvtv)
}
case *TypeType:
t := xv.GetType()
pt := &PointerType{Elt: t}

m.PushValue(asValue(pt))
default:
panic(fmt.Sprintf(
Expand All @@ -174,17 +182,16 @@ func (m *Machine) doOpStar() {
// XXX this is wrong, for var i interface{}; &i is *interface{}.
func (m *Machine) doOpRef() {
rx := m.PopExpr().(*RefExpr)
m.Alloc.AllocatePointer()
xv := m.PopAsPointer(rx.X)
// when obtaining a pointer of the databyte type, use the ElemType of databyte
xv, ro := m.PopAsPointer2(rx.X)
elt := xv.TV.T
if elt == DataByteType {
elt = xv.TV.V.(DataByteValue).ElemType
}
m.Alloc.AllocatePointer()
m.PushValue(TypedValue{
T: m.Alloc.NewType(&PointerType{Elt: elt}),
V: xv,
})
}.WithReadonly(ro))
}

// NOTE: keep in sync with doOpTypeAssert2.
Expand Down Expand Up @@ -704,6 +711,41 @@ func (m *Machine) doOpFuncLit() {
func (m *Machine) doOpConvert() {
xv := m.PopValue()
t := m.PopValue().GetType()

// BEGIN conversion checks
// These protect against inter-realm conversion exploits.

// Case 1.
// Do not allow conversion of value stored in eternal realm.
// Otherwise anyone could convert an external object insecurely.
if xv.T != nil && !xv.T.IsImmutable() && m.IsReadonly(xv) {
if xvdt, ok := xv.T.(*DeclaredType); ok &&
xvdt.PkgPath == m.Realm.Path {
// Except allow if xv.T is m.Realm.
// XXX do we need/want this?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What could go wrong?

} else {
xvdt, ok := xv.T.(*DeclaredType)
fmt.Println(m.String())
fmt.Println(xv.String(), t.String())
fmt.Println(xvdt, ok)
panic("illegal conversion of readonly or externally stored value")
}
}

// Case 2.
// Do not allow conversion to type of external realm.
// Only code declared within the same realm my perform such
// conversions, otherwise the realm could be tricked
// into executing a subtle exploit of mutating some
// value (say a pointer) stored in its own realm by
// a hostile construction converted to look safe.
if tdt, ok := t.(*DeclaredType); ok && !tdt.IsImmutable() && m.Realm != nil {
if IsRealmPath(tdt.PkgPath) && tdt.PkgPath != m.Realm.Path {
panic("illegal conversion to external realm type")
}
}
// END conversion checks

ConvertTo(m.Alloc, m.Store, xv, t, false)
m.PushValue(*xv)
}
40 changes: 40 additions & 0 deletions gnovm/pkg/gnolang/ownership.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ func (oid ObjectID) IsZero() bool {
return oid.PkgID.IsZero()
}

type ObjectIDer interface {
GetObjectID() ObjectID
}

type Object interface {
Value
GetObjectInfo() *ObjectInfo
Expand Down Expand Up @@ -359,6 +363,8 @@ func (tv *TypedValue) GetFirstObject(store Store) Object {
return cv
case *BoundMethodValue:
return cv
case *PackageValue:
return cv.GetBlock(store)
case *Block:
return cv
case RefValue:
Expand All @@ -372,3 +378,37 @@ func (tv *TypedValue) GetFirstObject(store Store) Object {
return nil
}
}

// returns false if there is no object.
func (tv *TypedValue) GetFirstObjectID() (ObjectID, bool) {
switch cv := tv.V.(type) {
case PointerValue:
if cv.Base == nil {
return ObjectID{}, false
}
return cv.Base.(ObjectIDer).GetObjectID(), true
case *ArrayValue:
return cv.GetObjectID(), true
case *SliceValue:
return cv.Base.(ObjectIDer).GetObjectID(), true
case *StructValue:
return cv.GetObjectID(), true
case *FuncValue:
return cv.Closure.(ObjectIDer).GetObjectID(), true
case *MapValue:
return cv.GetObjectID(), true
case *BoundMethodValue:
return cv.GetObjectID(), true
case *PackageValue:
return cv.Block.(ObjectIDer).GetObjectID(), true
case *Block:
return cv.GetObjectID(), true
case RefValue:
return cv.GetObjectID(), true
case *HeapItemValue:
// should only appear in PointerValue.Base
panic("heap item value should only appear as a pointer's base")
default:
return ObjectID{}, false
}
}
27 changes: 19 additions & 8 deletions gnovm/pkg/gnolang/preprocess.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package gnolang

// XXX do not allow conversion to external realm type.

import (
"fmt"
"math"
Expand Down Expand Up @@ -1092,13 +1094,18 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node {
if ric {
// Left const, Right const ----------------------
// Replace with *ConstExpr if const operands.
//
// First, convert untyped as necessary.
if !shouldSwapOnSpecificity(lcx.T, rcx.T) {
// convert n.Left to right type.
checkOrConvertType(store, last, n, &n.Left, rcx.T, false)
} else {
// convert n.Right to left type.
checkOrConvertType(store, last, n, &n.Right, lcx.T, false)
// If either is interface type no conversion is required.
if (lt == nil || lt.Kind() != InterfaceKind) &&
(rt == nil || rt.Kind() != InterfaceKind) {
if !shouldSwapOnSpecificity(lcx.T, rcx.T) {
// convert n.Left to right type.
checkOrConvertType(store, last, n, &n.Left, rcx.T, false)
} else {
// convert n.Right to left type.
checkOrConvertType(store, last, n, &n.Right, lcx.T, false)
}
}
// Then, evaluate the expression.
cx := evalConst(store, last, n)
Expand Down Expand Up @@ -3531,8 +3538,11 @@ func convertType(store Store, last BlockNode, n Node, x *Expr, t Type) {
// convert x to destination type t
doConvertType(store, last, x, t)
} else {
// if one side is declared name type and the other side is unnamed type
if isNamedConversion(xt, t) {
// if t is interface do nothing
if t != nil && t.Kind() == InterfaceKind {
// do nothing
} else if isNamedConversion(xt, t) {
// if one side is declared name type and the other side is unnamed type
// covert right (xt) to the type of the left (t)
doConvertType(store, last, x, t)
}
Expand All @@ -3542,6 +3552,7 @@ func convertType(store Store, last BlockNode, n Node, x *Expr, t Type) {

// convert x to destination type t
func doConvertType(store Store, last BlockNode, x *Expr, t Type) {
// XXX
cx := Expr(Call(constType(*x, t), *x))
cx = Preprocess(store, last, cx).(Expr)
*x = cx
Expand Down
Loading
Loading