Skip to content

Conversation

MikaelVallenet
Copy link
Member

@MikaelVallenet MikaelVallenet commented Jun 25, 2025

This PR add a new optional flag private in the gnomod.toml file.
Only realm can use this flag.

The rules of a private realm are:

  • It cannot be imported by other pkg
  • Other realms cannot store ref of an object stored in a private realm
  • Other realms cannot store object that used type decl/defined in a private realm
  • Other realms cannot store closures from private realm

These rules aim to one main goal: make the realm swappable without any effect on other realms.


Actual Working Process

  1. Keeper on AddPkg message, ensure private is realm
if gm.Private && !gno.IsRealmPath(pkgPath) {
	return ErrInvalidPackage("private packages must be realm packages")
}
  1. on typecheck mempkg block import of private realms
if mod != nil && mod.Private {
	// If the package is private, we cannot import it.
	err := ImportPrivateError{PkgPath: pkgPath}
	// NOTE: see comment above for ImportNotFoundError.
	result.err = err
	result.pending = false
	return nil, err
}
  1. SetPrivate on package initialization
	// make and set package if doesn't exist.
	pn := (*PackageNode)(nil)
	pv := (*PackageValue)(nil)
	if m.Package != nil && m.Package.PkgPath == mpkg.Path {
		...
	} else {
		pn = NewPackageNode(Name(mpkg.Name), mpkg.Path, &FileSet{})
		pv = pn.NewPackage()
		pv.SetPrivate(private) // HERE
		...
	}
  1. SetPrivate modify a map that contains the realms that are private
	pkgPrivateFromPkgID   = make(map[PkgID]struct{}, 100)
)

func SetPkgPrivate(path string, private bool) {
	pkgId := PkgIDFromPkgPath(path)
	if private {
		pkgPrivateFromPkgID[pkgId] = struct{}{}
	} else {
		delete(pkgPrivateFromPkgID, pkgId)
	}
}

func IsPkgPrivateFromPkgID(pkgID PkgID) bool {
	if _, ok := pkgPrivateFromPkgID[pkgID]; ok {
		return true
	}
	return false
}

func IsPkgPrivateFromPkgPath(path string) bool {
	pkgId := PkgIDFromPkgPath(path)
	return IsPkgPrivateFromPkgID(pkgId)
}
  1. apply changes on saveUnsavedObjectRecursively
// store unsaved children first.
func (rlm *Realm) saveUnsavedObjectRecursively(store Store, oo Object) {
         ...
	// assert no private dependencies.
	rlm.assertObjectIsPublic(oo, store)
        ...
}
  1. check for private objects
// assertObjectIsPublic ensures that the object is public and does not have any private dependencies.
// it check recursively the types of the object
// it does not recursively check the values because
// child objects are validated separately during the save traversal (saveUnsavedObjectRecursively)
func (rlm *Realm) assertObjectIsPublic(obj Object, store Store) {
	objID := obj.GetObjectID()
	if objID.PkgID != rlm.ID && IsPkgPrivateFromPkgID(objID.PkgID) {
		panic("cannot persist reference of object from private realm")
	}

	// NOTE: should i set the visited tids map at the higher level so it's set one time.
	// it could help to reduce the number of checks for the same type.
	tids := make(map[TypeID]struct{})
	switch v := obj.(type) {
	case *HeapItemValue:
		if v.Value.T != nil {
			rlm.assertTypeIsPublic(store, v.Value.T, tids)
		}
	case *ArrayValue:
		for _, av := range v.List {
			if av.T != nil {
				rlm.assertTypeIsPublic(store, av.T, tids)
			}
		}
	case *StructValue:
		for _, sv := range v.Fields {
			if sv.T != nil {
				rlm.assertTypeIsPublic(store, sv.T, tids)
			}
		}
	case *MapValue:
		for head := v.List.Head; head != nil; head = head.Next {
			if head.Key.T != nil {
				rlm.assertTypeIsPublic(store, head.Key.T, tids)
			}
			if head.Value.T != nil {
				rlm.assertTypeIsPublic(store, head.Value.T, tids)
			}
		}
	case *FuncValue:
		if v.PkgPath != rlm.Path && IsPkgPrivateFromPkgPath(v.PkgPath) {
			panic("cannot persist function or method from private realm")
		}
		if v.Type != nil {
			rlm.assertTypeIsPublic(store, v.Type, tids)
		}
		for _, capture := range v.Captures {
			if capture.T != nil {
				rlm.assertTypeIsPublic(store, capture.T, tids)
			}
		}
	case *BoundMethodValue:
		if v.Func.PkgPath != rlm.Path && IsPkgPrivateFromPkgPath(v.Func.PkgPath) {
			panic("cannot persist bound method from private realm")
		}
		if v.Receiver.T != nil {
			rlm.assertTypeIsPublic(store, v.Receiver.T, tids)
		}
	case *Block:
		for _, bv := range v.Values {
			if bv.T != nil {
				rlm.assertTypeIsPublic(store, bv.T, tids)
			}
		}
		if v.Blank.T != nil {
			rlm.assertTypeIsPublic(store, v.Blank.T, tids)
		}
	case *PackageValue:
		if v.PkgPath != rlm.Path && IsPkgPrivateFromPkgPath(v.PkgPath) {
			panic("cannot persist package from private realm")
		}
	default:
		panic(fmt.Sprintf("assertNoPrivateDeps: unhandled object type %T", v))
	}
}
  1. check for private types
// assertTypeIsPublic ensure that the type t is not defined in a private realm.
// it do it recursively for all types in t and have recursive guard to avoid infinite recursion on declared types.
func (rlm *Realm) assertTypeIsPublic(store Store, t Type, visited map[TypeID]struct{}) {
	pkgPath := ""
	switch tt := t.(type) {
	case *FuncType:
		for _, param := range tt.Params {
			rlm.assertTypeIsPublic(store, param, visited)
		}
		for _, result := range tt.Results {
			rlm.assertTypeIsPublic(store, result, visited)
		}
	case FieldType:
		rlm.assertTypeIsPublic(store, tt.Type, visited)
	case *SliceType, *ArrayType, *ChanType, *PointerType:
		rlm.assertTypeIsPublic(store, tt.Elem(), visited)
	case *tupleType:
		for _, et := range tt.Elts {
			rlm.assertTypeIsPublic(store, et, visited)
		}
	case *MapType:
		rlm.assertTypeIsPublic(store, tt.Key, visited)
		rlm.assertTypeIsPublic(store, tt.Elem(), visited)
	case *InterfaceType:
		pkgPath = tt.GetPkgPath()
		for _, method := range tt.Methods {
			rlm.assertTypeIsPublic(store, method, visited)
		}
	case *StructType:
		pkgPath = tt.GetPkgPath()
		for _, field := range tt.Fields {
			rlm.assertTypeIsPublic(store, field, visited)
		}
	case *DeclaredType:
		tid := tt.TypeID()
		if _, exists := visited[tid]; !exists {
			visited[tid] = struct{}{}
			rlm.assertTypeIsPublic(store, tt.Base, visited)
			for _, method := range tt.Methods {
				rlm.assertTypeIsPublic(store, method.T, visited)
				if mv, ok := method.V.(*FuncValue); ok {
					rlm.assertTypeIsPublic(store, mv.Type, visited)
				}
			}
		}
		pkgPath = tt.GetPkgPath()
	case *RefType:
		t2 := store.GetType(tt.TypeID())
		rlm.assertTypeIsPublic(store, t2, visited)
	case PrimitiveType, *TypeType, *PackageType, blockType, heapItemType:
		// these types do not have a package path.
		// NOTE: PackageType have a TypeID, should i loat it from store and check it?
		return
	default:
		panic(fmt.Sprintf("assertTypeIsPublic: unhandled type %T", tt))
	}
	if pkgPath != "" && pkgPath != rlm.Path && IsPkgPrivateFromPkgPath(pkgPath) {
		panic("cannot persist object of type defined in a private realm")
	}
}

ℹ You can see exhaustive list of test in: gno.land/pkg/integration/testdata/addpkg_private.txtar

MikaelVallenet and others added 21 commits June 24, 2025 11:47
…o mikaelvallenet/gnovm/gnomod/add-new-draft-field
## Description

Renaming to Gno.land, adding back in the gnoclient docs that were
dropped with the Docs V2.
## Description

Fixing the coin checker so it supports the newest gno-forms PR.

The issue was that the form can only take you to a pre-determined render
path within the realm, without the ability to parametrize it. I had to
switch to parametrizing via query parameters. Would appreciate if
someone can play around a bit.
## Description

Fixes outdated link.
…o mikaelvallenet/gnovm/gnomod/add-new-draft-field
@github-actions github-actions bot added 🧾 package/realm Tag used for new Realms or Packages. 📦 🤖 gnovm Issues or PRs gnovm related 📦 ⛰️ gno.land Issues or PRs gno.land package related labels Jun 25, 2025
@Gno2D2 Gno2D2 requested a review from a team June 25, 2025 10:46
@Gno2D2
Copy link
Collaborator

Gno2D2 commented Jun 25, 2025

🛠 PR Checks Summary

All Automated Checks passed. ✅

Manual Checks (for Reviewers):
  • IGNORE the bot requirements for this PR (force green CI check)
Read More

🤖 This bot helps streamline PR reviews by verifying automated checks and providing guidance for contributors and reviewers.

✅ Automated Checks (for Contributors):

🟢 Maintainers must be able to edit this pull request (more info)
🟢 Pending initial approval by a review team member, or review from tech-staff

☑️ Contributor Actions:
  1. Fix any issues flagged by automated checks.
  2. Follow the Contributor Checklist to ensure your PR is ready for review.
    • Add new tests, or document why they are unnecessary.
    • Provide clear examples/screenshots, if necessary.
    • Update documentation, if required.
    • Ensure no breaking changes, or include BREAKING CHANGE notes.
    • Link related issues/PRs, where applicable.
☑️ Reviewer Actions:
  1. Complete manual checks for the PR, including the guidelines and additional checks if applicable.
📚 Resources:
Debug
Automated Checks
Maintainers must be able to edit this pull request (more info)

If

🟢 Condition met
└── 🟢 And
    ├── 🟢 The base branch matches this pattern: ^master$
    └── 🟢 The pull request was created from a fork (head branch repo: MikaelVallenet/gno)

Then

🟢 Requirement satisfied
└── 🟢 Maintainer can modify this pull request

Pending initial approval by a review team member, or review from tech-staff

If

🟢 Condition met
└── 🟢 And
    ├── 🟢 The base branch matches this pattern: ^master$
    └── 🟢 Not (🔴 Pull request author is a member of the team: tech-staff)

Then

🟢 Requirement satisfied
└── 🟢 If
    ├── 🟢 Condition
    │   └── 🟢 Or
    │       ├── 🔴 At least one of these user(s) reviewed the pull request: [jefft0 leohhhn n0izn0iz notJoon omarsy x1unix] (with state "APPROVED")
    │       ├── 🟢 At least 1 user(s) of the team tech-staff reviewed pull request
    │       └── 🔴 This pull request is a draft
    └── 🟢 Then
        └── 🟢 And
            ├── 🟢 Not (🔴 This label is applied to pull request: review/triage-pending)
            └── 🟢 At least 1 user(s) of the team tech-staff reviewed pull request

Manual Checks
**IGNORE** the bot requirements for this PR (force green CI check)

If

🟢 Condition met
└── 🟢 On every pull request

Can be checked by

  • Any user with comment edit permission

Copy link
Member

@thehowl thehowl left a comment

Choose a reason for hiding this comment

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

Partial, initial review of the changes in gnolang.

I don't think we should have private/public information in the ObjectID, it is quite unflexible.

@ltzmaxwell Do you agree it is better suited to be in the Realm struct?

Copy link
Member

Choose a reason for hiding this comment

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

I'm not convinced about storing private/public information in the ObjectID. While this does simplify checks when running FinalizeRealm, it makes it very inflexible to ever switch from a private realm to a public realm.

I think the information on whether a package is private should be fetched either on the Realm, or if that is too slow, maybe in another database field keeping track of the attributes of a given package.

Comment on lines 911 to 912
// assertNoPrivateDeps ensures that the object is not private
// it does not recursively check the values, but it does check recursively the types
Copy link
Member

Choose a reason for hiding this comment

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

You cannot just check the types if you have interfaces; interfaces may contain values from any package within....

@MikaelVallenet MikaelVallenet requested a review from thehowl August 13, 2025 14:35
@piux2
Copy link
Contributor

piux2 commented Aug 14, 2025

@MikaelVallenet thank you for working on this!

I have two questions here

  1. I'm curious about the rationale behind these rules—specifically, how they relate to

enabling private realm swapping without impacting other realms

  1. A private realm is callable by gnokey. How is that different from a public realm that are never be discovered or imported by other realms? Why we would treat them differently in the above rules?

    It looks like these assertObjectIsPublic() and assertTypeIsPublic() are called at runtime, with validation for all objects recursively and repeatedly, regardless of whether a private realm is involved or not for AddPkg, Call and Run
    https://github.com/gnolang/gno/pull/4422/files#diff-7041eca77b078f22ee334239995c403673f040b3726bcffa5a8a053b68a951a7R805

I'm trying to understand the purpose and trade-offs behind these constraints. Any insights would be appreciated.

@MikaelVallenet
Copy link
Member Author

MikaelVallenet commented Aug 14, 2025

@MikaelVallenet thank you for working on this!

I have two questions here

  1. I'm curious about the rationale behind these rules—specifically, how they relate to

enabling private realm swapping without impacting other realms

  1. A private realm is callable by gnokey. How is that different from a public realm that are never be discovered or imported by other realms? Why we would treat them differently in the above rules?
    It looks like these assertObjectIsPublic() and assertTypeIsPublic() are called at runtime, with validation for all objects recursively and repeatedly, regardless of whether a private realm is involved or not for AddPkg, Call and Run
    https://github.com/gnolang/gno/pull/4422/files#diff-7041eca77b078f22ee334239995c403673f040b3726bcffa5a8a053b68a951a7R805

I'm trying to understand the purpose and trade-offs behind these constraints. Any insights would be appreciated.

Hi,

  • "For variables of private types: they can be modified within the public realm but cannot be stored by it."
    Since a public realm cannot import private types by design, I’m not sure how this case would arise except through passing interfaces and i don't think it would cause problem. Do you have an example? I’m not entirely certain I understand the scenario you’re describing here but we just don't want the public realm to have references to the private realm so if we remove/update the private realm it does not affect the public one.

  • "For private primitive-type variables: they can be stored by the public realm."
    What exactly is meant by a “private primitive type”? If it’s just a primitive value passed through a function, it will be copied. If you pass a pointer, the public realm still can’t store it if it's owned by the private one.

  • "Private array of string in the private realm can be stored by the public realm."
    That’s not the case; there’s a test showing called SaveYourSliceToPublicRealm that is a []string{} stored in private and that show it fails

  • "A private realm is callable by gnokey. How is that different from a public realm that are never be discovered or imported by other realms? Why we would treat them differently in the above rules?"
    You can’t guarantee that a public realm will remain undiscovered or unimported forever. With private realms, the rules can be enforced to ensure isolation and safe swapping/removal later.

  • "It looks like these assertObjectIsPublic() and assertTypeIsPublic() are called at runtime, with validation for all objects recursively and repeatedly, regardless of whether a private realm is involved or not for AddPkg, Call and Run."
    Good point — if the private realm isn’t the one being called/deployed, and since it cannot be imported i don't think we need to run the validation, it should only be on deploy/call a private realm or a msg run since a run will be a private realm i think. I did not think of that before, i think it can be done in another PR or later if this approach is accepted

@omarsy
Copy link
Member

omarsy commented Aug 17, 2025

A proposal to improve complexity:

Actually, we only ever persist:

  • the current object (oo), and
  • the unsaved children returned by getUnsavedChildObjects(oo).

saveUnsavedObjectRecursively already walks that exact set depth‑first:

  1. For oo, it calls getUnsavedChildObjects(oo).
  2. For each unsaved child that isn’t escaped, it recurses into saveUnsavedObjectRecursively(child).
  3. After children are handled, it persists oo itself.

Because of this recursion, every object that will be persisted is inevitably revisited as the “current oo” in its own recursive call. That’s the ideal moment to validate it. We don’t need I think an early deep scan over the whole graph or all nested types: the traversal guarantees coverage. If an object appears multiple times in the graph, flags (IsNewReal/IsDirty/IsEscaped) and the guards in the recursion prevent double persistence and thus avoid duplicate validation. Escaped/new‑escaped nodes are intentionally skipped from preemptive saving (and thus from validation here), since they’re not persisted via this path.

Practically, this means:

  • Perform object‑level checks (private FuncValue PkgPath, private foreign ObjectID) right before persisting the current oo.
  • Perform shallow, slot-by-slot type checks for the current oo’s owned fields (Array items, Struct fields, Func captures, BoundMethod func/receiver, Map key/value, HeapItem payload). We validate “what we own” now; deeper objects will be validated when they become the current oo in their own recursive call.

Somethinh like this I think

--- a/gnovm/pkg/gnolang/realm.go
+++ b/gnovm/pkg/gnolang/realm.go
@@ -801,9 +801,6 @@ func (rlm *Realm) saveUnsavedObjectRecursively(store Store, oo Object) {
 		}
 	}
 
-	// assert object have no private dependencies.
-	rlm.assertObjectIsPublic(oo, store)
-
 	// first, save unsaved children.
 	unsaved := getUnsavedChildObjects(oo)
 	for _, uch := range unsaved {
@@ -852,6 +849,19 @@ func (rlm *Realm) saveObject(store Store, oo Object) {
 		oo.SetIsEscaped(true)
 		// XXX anything else to do?
 	}
+
+	if fv, ok := oo.(*FuncValue); ok {
+		if fv.PkgPath != rlm.Path && IsPkgPrivateFromPkgPath(fv.PkgPath) {
+			panic("cannot persist function or method from private realm")
+		}
+	}
+
+	if oo.GetObjectID().PkgID != rlm.ID && IsPkgPrivateFromPkgID(oo.GetObjectID().PkgID) {
+		panic("cannot persist reference of object from private realm")
+	}
+
+	rlm.assertTypeCanBeOwned(oo)
+
 	// set object to store.
 	// NOTE: also sets the hash to object.
 	rlm.sumDiff += store.SetObject(oo)
@@ -904,143 +914,6 @@ func (rlm *Realm) clearMarks() {
 	rlm.escaped = nil
 }
 
-// assertObjectIsPublic ensures that the object is public and does not have any private dependencies.
-// it check recursively the types of the object
-// it does not recursively check the values because
-// child objects are validated separately during the save traversal (saveUnsavedObjectRecursively)
-func (rlm *Realm) assertObjectIsPublic(obj Object, store Store) {
-	objID := obj.GetObjectID()
-	if objID.PkgID != rlm.ID && IsPkgPrivateFromPkgID(objID.PkgID) {
-		panic("cannot persist reference of object from private realm")
-	}
-
-	// NOTE: should i set the visited tids map at the higher level so it's set one time.
-	// it could help to reduce the number of checks for the same type.
-	tids := make(map[TypeID]struct{})
-	switch v := obj.(type) {
-	case *HeapItemValue:
-		if v.Value.T != nil {
-			rlm.assertTypeIsPublic(store, v.Value.T, tids)
-		}
-	case *ArrayValue:
-		for _, av := range v.List {
-			if av.T != nil {
-				rlm.assertTypeIsPublic(store, av.T, tids)
-			}
-		}
-	case *StructValue:
-		for _, sv := range v.Fields {
-			if sv.T != nil {
-				rlm.assertTypeIsPublic(store, sv.T, tids)
-			}
-		}
-	case *MapValue:
-		for head := v.List.Head; head != nil; head = head.Next {
-			if head.Key.T != nil {
-				rlm.assertTypeIsPublic(store, head.Key.T, tids)
-			}
-			if head.Value.T != nil {
-				rlm.assertTypeIsPublic(store, head.Value.T, tids)
-			}
-		}
-	case *FuncValue:
-		if v.PkgPath != rlm.Path && IsPkgPrivateFromPkgPath(v.PkgPath) {
-			panic("cannot persist function or method from private realm")
-		}
-		if v.Type != nil {
-			rlm.assertTypeIsPublic(store, v.Type, tids)
-		}
-		for _, capture := range v.Captures {
-			if capture.T != nil {
-				rlm.assertTypeIsPublic(store, capture.T, tids)
-			}
-		}
-	case *BoundMethodValue:
-		if v.Func.PkgPath != rlm.Path && IsPkgPrivateFromPkgPath(v.Func.PkgPath) {
-			panic("cannot persist bound method from private realm")
-		}
-		if v.Receiver.T != nil {
-			rlm.assertTypeIsPublic(store, v.Receiver.T, tids)
-		}
-	case *Block:
-		for _, bv := range v.Values {
-			if bv.T != nil {
-				rlm.assertTypeIsPublic(store, bv.T, tids)
-			}
-		}
-		if v.Blank.T != nil {
-			rlm.assertTypeIsPublic(store, v.Blank.T, tids)
-		}
-	case *PackageValue:
-		if v.PkgPath != rlm.Path && IsPkgPrivateFromPkgPath(v.PkgPath) {
-			panic("cannot persist package from private realm")
-		}
-	default:
-		panic(fmt.Sprintf("assertNoPrivateDeps: unhandled object type %T", v))
-	}
-}
-
-// assertTypeIsPublic ensure that the type t is not defined in a private realm.
-// it do it recursively for all types in t and have recursive guard to avoid infinite recursion on declared types.
-func (rlm *Realm) assertTypeIsPublic(store Store, t Type, visited map[TypeID]struct{}) {
-	pkgPath := ""
-	switch tt := t.(type) {
-	case *FuncType:
-		for _, param := range tt.Params {
-			rlm.assertTypeIsPublic(store, param, visited)
-		}
-		for _, result := range tt.Results {
-			rlm.assertTypeIsPublic(store, result, visited)
-		}
-	case FieldType:
-		rlm.assertTypeIsPublic(store, tt.Type, visited)
-	case *SliceType, *ArrayType, *ChanType, *PointerType:
-		rlm.assertTypeIsPublic(store, tt.Elem(), visited)
-	case *tupleType:
-		for _, et := range tt.Elts {
-			rlm.assertTypeIsPublic(store, et, visited)
-		}
-	case *MapType:
-		rlm.assertTypeIsPublic(store, tt.Key, visited)
-		rlm.assertTypeIsPublic(store, tt.Elem(), visited)
-	case *InterfaceType:
-		pkgPath = tt.GetPkgPath()
-		for _, method := range tt.Methods {
-			rlm.assertTypeIsPublic(store, method, visited)
-		}
-	case *StructType:
-		pkgPath = tt.GetPkgPath()
-		for _, field := range tt.Fields {
-			rlm.assertTypeIsPublic(store, field, visited)
-		}
-	case *DeclaredType:
-		tid := tt.TypeID()
-		if _, exists := visited[tid]; !exists {
-			visited[tid] = struct{}{}
-			rlm.assertTypeIsPublic(store, tt.Base, visited)
-			for _, method := range tt.Methods {
-				rlm.assertTypeIsPublic(store, method.T, visited)
-				if mv, ok := method.V.(*FuncValue); ok {
-					rlm.assertTypeIsPublic(store, mv.Type, visited)
-				}
-			}
-		}
-		pkgPath = tt.GetPkgPath()
-	case *RefType:
-		t2 := store.GetType(tt.TypeID())
-		rlm.assertTypeIsPublic(store, t2, visited)
-	case PrimitiveType, *TypeType, *PackageType, blockType, heapItemType:
-		// these types do not have a package path.
-		// NOTE: PackageType have a TypeID, should i loat it from store and check it?
-		return
-	default:
-		panic(fmt.Sprintf("assertTypeIsPublic: unhandled type %T", tt))
-	}
-	if pkgPath != "" && pkgPath != rlm.Path && IsPkgPrivateFromPkgPath(pkgPath) {
-		panic("cannot persist object of type defined in a private realm")
-	}
-}
-
 //----------------------------------------
 // getSelfOrChildObjects
 
@@ -1567,6 +1440,75 @@ func fillTypesTV(store Store, tv *TypedValue) {
 	tv.V = fillTypesOfValue(store, tv.V)
 }
 
+func (rlm *Realm) assertTypeCanBeOwned(oo Object) {
+	switch cv := oo.(type) {
+	case nil:
+	case *ArrayValue:
+		for _, ctv := range cv.List {
+			rlm.assertTypeCanBeOwned2(ctv.T)
+		}
+	case *StructValue:
+		for _, ctv := range cv.Fields {
+			rlm.assertTypeCanBeOwned2(ctv.T)
+		}
+	case *FuncValue:
+		for _, c := range cv.Captures {
+			rlm.assertTypeCanBeOwned2(c.T)
+		}
+	case *BoundMethodValue:
+		rlm.assertTypeCanBeOwned2(cv.Func.Type)
+		rlm.assertTypeCanBeOwned2(cv.Receiver.T)
+	case *MapValue:
+		for cur := cv.List.Head; cur != nil; cur = cur.Next {
+			rlm.assertTypeCanBeOwned2(cur.Key.T)
+			rlm.assertTypeCanBeOwned2(cur.Value.T)
+		}
+	case *PackageValue:
+	case *Block:
+	case *HeapItemValue:
+		rlm.assertTypeCanBeOwned2(cv.Value.T)
+	default:
+		panic(fmt.Sprintf(
+			"unexpected type %v",
+			reflect.TypeOf(oo)))
+	}
+
+}
+
+func (rlm *Realm) assertTypeCanBeOwned2(ty Type) {
+	var pkgPaths []string
+	switch tt := ty.(type) {
+	case nil:
+	case *FuncType: // maybe impossible
+	case FieldType:
+		fmt.Printf("assertTypeCanBeOwned2: %T\n", ty)
+	case *SliceType, *ArrayType, *ChanType, *PointerType:
+		pkgPaths = append(pkgPaths, tt.Elem().GetPkgPath())
+	case *tupleType: // maybe impossible
+	case *MapType:
+		pkgPaths = append(pkgPaths, tt.Key.GetPkgPath())
+		pkgPaths = append(pkgPaths, tt.Elem().GetPkgPath())
+	case *InterfaceType: // maybe impossible
+		fmt.Printf("assertTypeCanBeOwned2: %T\n", ty)
+		pkgPaths = append(pkgPaths, tt.GetPkgPath())
+	case *StructType:
+		pkgPaths = append(pkgPaths, tt.GetPkgPath())
+	case *DeclaredType:
+		pkgPaths = append(pkgPaths, tt.GetPkgPath())
+	case *RefType:
+		pkgPaths = append(pkgPaths, tt.GetPkgPath())
+	case PrimitiveType, *TypeType, *PackageType, blockType, heapItemType:
+	default:
+		panic(fmt.Sprintf("assertTypeIsPublic: unhandled type %T", tt))
+	}
+
+	for _, pkgPath := range pkgPaths {
+		if pkgPath != "" && pkgPath != rlm.Path && IsPkgPrivateFromPkgPath(pkgPath) {
+			panic("cannot persist object of type defined in a private realm")
+		}
+	}
+}
+
 // Partially fills loaded objects shallowly, similarly to
 // getUnsavedTypes. Replaces all RefTypes with corresponding types.
 func fillTypesOfValue(store Store, val Value) Value {

WDYT ?

@MikaelVallenet
Copy link
Member Author

MikaelVallenet commented Aug 18, 2025

+func (rlm *Realm) assertTypeCanBeOwned2(ty Type) {
+	var pkgPaths []string
+	switch tt := ty.(type) {
+	case nil:
+	case *FuncType: // maybe impossible
+	case FieldType:
+		fmt.Printf("assertTypeCanBeOwned2: %T\n", ty)
+	case *SliceType, *ArrayType, *ChanType, *PointerType:
+		pkgPaths = append(pkgPaths, tt.Elem().GetPkgPath())
+	case *tupleType: // maybe impossible
+	case *MapType:
+		pkgPaths = append(pkgPaths, tt.Key.GetPkgPath())
+		pkgPaths = append(pkgPaths, tt.Elem().GetPkgPath())
+	case *InterfaceType: // maybe impossible
+		fmt.Printf("assertTypeCanBeOwned2: %T\n", ty)
+		pkgPaths = append(pkgPaths, tt.GetPkgPath())
+	case *StructType:
+		pkgPaths = append(pkgPaths, tt.GetPkgPath())
+	case *DeclaredType:
+		pkgPaths = append(pkgPaths, tt.GetPkgPath())
+	case *RefType:
+		pkgPaths = append(pkgPaths, tt.GetPkgPath())
+	case PrimitiveType, *TypeType, *PackageType, blockType, heapItemType:
+	default:
+		panic(fmt.Sprintf("assertTypeIsPublic: unhandled type %T", tt))
+	}
+
+	for _, pkgPath := range pkgPaths {
+		if pkgPath != "" && pkgPath != rlm.Path && IsPkgPrivateFromPkgPath(pkgPath) {
+			panic("cannot persist object of type defined in a private realm")
+		}
+	}
+}

if you don't check type recursively you open vuln i think
traversal by object / values does not ensure there is no private types (especially nil typed values)
it's why i did recursive on types when values traversal is handled by the function
but after trying your code i saw it was passing the test so i added one to demonstrate what i mean:

func SaveNilSliceOfSliceOfPrivateToPublicRealm(cur realm) {
    // nil value with type [][]*PrivateData
    var arr [][]*PrivateData = nil
    publicrealm.SaveYourInterface(cross, arr)
}

This succeed with your impl. when it should fails.
Do you see a way to handle this in a better way than i did ?
Thanks a lot for review ❤️

@omarsy
Copy link
Member

omarsy commented Aug 18, 2025

I think this could be a solution, but I'm not entirely certain. My sample doesn't handle empty elements, which should be improved. To address your issue maybe this (we should handle map too):

func (rlm *Realm) assertTypeCanBeOwned2(ty Type) {
	var pkgPaths []string
	fmt.Printf("assertTypeCanBeOwned2: %T %v\n", ty, ty)
	switch tt := ty.(type) {
	case nil:
	case *FuncType: // maybe impossible
	case FieldType:
	case *SliceType, *ArrayType, *ChanType, *PointerType:
		ty = tt.Elem()
	Loop:
		for { // When slice or array are empty we will not have any value in get child object so we should check all the declaration
			switch tt := ty.(type) {
			case *SliceType, *ArrayType, *ChanType, *PointerType:
				ty = tt.Elem()
			default:
				pkgPaths = append(pkgPaths, ty.GetPkgPath())
				break Loop
			}
		}
	case *tupleType:
	case *MapType: // maybe handle empty case
		pkgPaths = append(pkgPaths, tt.Key.GetPkgPath())
		pkgPaths = append(pkgPaths, tt.Elem().GetPkgPath())
	case *InterfaceType:
		pkgPaths = append(pkgPaths, tt.GetPkgPath())
	case *StructType:
		pkgPaths = append(pkgPaths, tt.GetPkgPath())
	case *DeclaredType:
		pkgPaths = append(pkgPaths, tt.GetPkgPath())
	case *RefType:
		pkgPaths = append(pkgPaths, tt.GetPkgPath())
	case PrimitiveType, *TypeType, *PackageType, blockType, heapItemType:
	default:
		panic(fmt.Sprintf("assertTypeIsPublic: unhandled type %T", tt))
	}

	for _, pkgPath := range pkgPaths {
		if pkgPath != "" && pkgPath != rlm.Path && IsPkgPrivateFromPkgPath(pkgPath) {
			panic("cannot persist object of type defined in a private realm")
		}
	}
}

WDYT ?

@MikaelVallenet
Copy link
Member Author

MikaelVallenet commented Aug 18, 2025

[c73fde0](/gnolang/gno/pull/4422/commits/c73fde00596919ae9fc21eafd4555889b77572b8)

would not be safer to use a visited map ? : c73fde0 ?
This way we still process everything without bypass possible on empty values but we mark them to avoid process same type multiple time.

@Kouteki Kouteki removed the request for review from moul August 31, 2025 18:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🚀 ci 🤝 contribs 📦 ⛰️ gno.land Issues or PRs gno.land package related 📦 🤖 gnovm Issues or PRs gnovm related 🧾 package/realm Tag used for new Realms or Packages.
Projects
Status: No status
Status: In Review
Development

Successfully merging this pull request may close these issues.

10 participants