Skip to content

Commit 441e74f

Browse files
committed
Exclude pre-call roots from post-call contract rebinding
1 parent 206e938 commit 441e74f

2 files changed

Lines changed: 64 additions & 9 deletions

File tree

vibes/capability_contracts_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,28 @@ func (argPassThroughContractCapability) CapabilityContracts() map[string]Capabil
430430
}
431431
}
432432

433+
type hashMergeContractLeakProbeCapability struct{}
434+
435+
func (hashMergeContractLeakProbeCapability) Bind(binding CapabilityBinding) (map[string]Value, error) {
436+
return map[string]Value{
437+
"cap": NewObject(map[string]Value{
438+
"touch": NewBuiltin("cap.touch", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) {
439+
return NewString("ok"), nil
440+
}),
441+
}),
442+
}, nil
443+
}
444+
445+
func (hashMergeContractLeakProbeCapability) CapabilityContracts() map[string]CapabilityMethodContract {
446+
return map[string]CapabilityMethodContract{
447+
"hash.merge": {
448+
ValidateArgs: func(args []Value, kwargs map[string]Value, block Value) error {
449+
return fmt.Errorf("hash.merge contract should not bind to foreign builtin")
450+
},
451+
},
452+
}
453+
}
454+
433455
func TestCapabilityContractRejectsInvalidArguments(t *testing.T) {
434456
engine := MustNewEngine(Config{})
435457
script, err := engine.Compile(`def run()
@@ -778,3 +800,30 @@ end`)
778800
t.Fatalf("unexpected result: %#v", result)
779801
}
780802
}
803+
804+
func TestCapabilityContractsDoNotHijackReceiverStoredForeignBuiltins(t *testing.T) {
805+
engine := MustNewEngine(Config{})
806+
script, err := engine.Compile(`def run()
807+
cap.foreign = { a: 1 }.merge
808+
cap.touch()
809+
cap.foreign({ b: 2 })
810+
end`)
811+
if err != nil {
812+
t.Fatalf("compile failed: %v", err)
813+
}
814+
815+
result, err := script.Call(context.Background(), "run", nil, CallOptions{
816+
Capabilities: []CapabilityAdapter{
817+
hashMergeContractLeakProbeCapability{},
818+
},
819+
})
820+
if err != nil {
821+
t.Fatalf("unexpected error: %v", err)
822+
}
823+
if result.Kind() != KindHash {
824+
t.Fatalf("expected hash result, got %v", result.Kind())
825+
}
826+
if got, ok := result.Hash()["b"]; !ok || got.Kind() != KindInt || got.Int() != 2 {
827+
t.Fatalf("unexpected merge result: %#v", result.Hash())
828+
}
829+
}

vibes/execution.go

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -615,14 +615,20 @@ func (exec *Execution) invokeCallable(callee Value, receiver Value, args []Value
615615
case KindBuiltin:
616616
builtin := callee.Builtin()
617617
scope := exec.capabilityContractScopes[builtin]
618-
var preCallArgBuiltins map[*Builtin]struct{}
618+
var preCallKnownBuiltins map[*Builtin]struct{}
619619
if scope != nil && len(scope.contracts) > 0 {
620-
preCallArgBuiltins = make(map[*Builtin]struct{})
620+
preCallKnownBuiltins = make(map[*Builtin]struct{})
621+
if receiver.Kind() != KindNil {
622+
collectCapabilityBuiltins(receiver, preCallKnownBuiltins)
623+
}
624+
for _, root := range scope.roots {
625+
collectCapabilityBuiltins(root, preCallKnownBuiltins)
626+
}
621627
for _, arg := range args {
622-
collectCapabilityBuiltins(arg, preCallArgBuiltins)
628+
collectCapabilityBuiltins(arg, preCallKnownBuiltins)
623629
}
624630
for _, kwarg := range kwargs {
625-
collectCapabilityBuiltins(kwarg, preCallArgBuiltins)
631+
collectCapabilityBuiltins(kwarg, preCallKnownBuiltins)
626632
}
627633
}
628634
contract, hasContract := exec.capabilityContracts[builtin]
@@ -645,22 +651,22 @@ func (exec *Execution) invokeCallable(callee Value, receiver Value, args []Value
645651
// Capability methods can lazily publish additional builtins at runtime
646652
// (e.g. through factory return values or receiver mutation). Re-scan
647653
// these values so future calls still enforce declared contracts.
648-
bindCapabilityContracts(result, scope, exec.capabilityContracts, exec.capabilityContractScopes)
654+
bindCapabilityContractsExcluding(result, scope, exec.capabilityContracts, exec.capabilityContractScopes, preCallKnownBuiltins)
649655
if receiver.Kind() != KindNil {
650-
bindCapabilityContracts(receiver, scope, exec.capabilityContracts, exec.capabilityContractScopes)
656+
bindCapabilityContractsExcluding(receiver, scope, exec.capabilityContracts, exec.capabilityContractScopes, preCallKnownBuiltins)
651657
}
652658
// Methods can mutate sibling scope roots via captured references; refresh
653659
// all adapter roots so newly exposed builtins also get bound.
654660
for _, root := range scope.roots {
655-
bindCapabilityContracts(root, scope, exec.capabilityContracts, exec.capabilityContractScopes)
661+
bindCapabilityContractsExcluding(root, scope, exec.capabilityContracts, exec.capabilityContractScopes, preCallKnownBuiltins)
656662
}
657663
// Methods can also publish builtins by mutating positional or keyword
658664
// argument objects supplied by script code.
659665
for _, arg := range args {
660-
bindCapabilityContractsExcluding(arg, scope, exec.capabilityContracts, exec.capabilityContractScopes, preCallArgBuiltins)
666+
bindCapabilityContractsExcluding(arg, scope, exec.capabilityContracts, exec.capabilityContractScopes, preCallKnownBuiltins)
661667
}
662668
for _, kwarg := range kwargs {
663-
bindCapabilityContractsExcluding(kwarg, scope, exec.capabilityContracts, exec.capabilityContractScopes, preCallArgBuiltins)
669+
bindCapabilityContractsExcluding(kwarg, scope, exec.capabilityContracts, exec.capabilityContractScopes, preCallKnownBuiltins)
664670
}
665671
}
666672
return result, nil

0 commit comments

Comments
 (0)