starbind: fix use of ptr variables in starlark (#3386)

This commit is contained in:
Andrei Matei 2023-05-31 13:29:36 -04:00 committed by GitHub
parent 407ace96c8
commit 7603e46f75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 92 additions and 2 deletions

@ -0,0 +1,17 @@
package main
import "runtime"
func main() {
type StructWithInterface struct {
Val any
}
var i int
var ba = StructWithInterface{}
ba.Val = &i
var ptrPtr **int
_ = ptrPtr
runtime.Breakpoint()
}

@ -453,10 +453,15 @@ func (v ptrVariableAsStarlarkValue) Attr(name string) (starlark.Value, error) {
} }
func (v ptrVariableAsStarlarkValue) AttrNames() []string { func (v ptrVariableAsStarlarkValue) AttrNames() []string {
if len(v.v.Children) == 0 {
// The pointer variable was not loaded; we don't know the field names.
return nil
}
if v.v.Children[0].Kind != reflect.Struct { if v.v.Children[0].Kind != reflect.Struct {
return nil return nil
} }
// autodereference // autodereference: present the field names of the pointed-to struct as the
// fields of this pointer variable.
x := structVariableAsStarlarkValue{&v.v.Children[0], v.env} x := structVariableAsStarlarkValue{&v.v.Children[0], v.env}
return x.AttrNames() return x.AttrNames()
} }

@ -141,12 +141,13 @@ func (env *Env) printFunc() func(_ *starlark.Thread, msg string) {
// Source can be either a []byte, a string or a io.Reader. If source is nil // Source can be either a []byte, a string or a io.Reader. If source is nil
// Execute will execute the file specified by 'path'. // Execute will execute the file specified by 'path'.
// After the file is executed if a function named mainFnName exists it will be called, passing args to it. // After the file is executed if a function named mainFnName exists it will be called, passing args to it.
func (env *Env) Execute(path string, source interface{}, mainFnName string, args []interface{}) (starlark.Value, error) { func (env *Env) Execute(path string, source interface{}, mainFnName string, args []interface{}) (_ starlark.Value, _err error) {
defer func() { defer func() {
err := recover() err := recover()
if err == nil { if err == nil {
return return
} }
_err = fmt.Errorf("panic executing starlark script: %v", err)
fmt.Fprintf(env.out, "panic executing starlark script: %v\n", err) fmt.Fprintf(env.out, "panic executing starlark script: %v\n", err)
for i := 0; ; i++ { for i := 0; ; i++ {
pc, file, line, ok := runtime.Caller(i) pc, file, line, ok := runtime.Caller(i)

@ -158,3 +158,70 @@ func TestStarlarkVariable(t *testing.T) {
} }
}) })
} }
// Test that pointer variables that were not loaded don't lead to crashes when
// used in Starlark scripts.
func TestStarlarkVariablePointerNotLoaded(t *testing.T) {
withTestTerminal("testvariables_pointers_not_loaded", t, func(term *FakeTerminal) {
term.MustExec("continue")
// We're going to partially load some variables through the eval()
// builtin. Then we're going to attempt to evaluate expressions which
// try to access a field from a pointer variable that wasn't loaded
// (i.e. a ptrVariableAsStarlarkValue with no children). The tests will
// verify that we get an error. This is a regression test; we used to
// panic.
for _, tc := range []struct{ name, script, expErr string }{
{
// In this test, we'll load v. v will have a child (i.e.
// v.Children[0]), but because v is nil, v.Children[0] will not
// be loaded. We'll turn v.Children[0] into a
// ptrVariableAsStarlarkValue, and attempt to access a field on
// it.
//
// v -> structAsStarlarkValue<api.Variable<**int>>
// v.Children[0] -> structAsStarlarkValue<api.Variable<*int>>
// v.Children[0].Value -> ptrVariableAsStarlarkValue<*int> ; this pointer variable has not been loaded
name: "partial load because of nil ptr",
script: `
v = eval(
None, "ptrPtr", None
).Variable
v.Children[0].Value.XXX
`,
expErr: "*int has no .XXX field or method",
},
{
// In this test, MaxStructFields = 10 and MaxVariableRecurse = 0
// will cause us to load v, and v.Children[0] (in the sense that
// v.Children[0] has a child), but _not_ load
// v.Children[0].Children[0] (because the recursion limit is
// exhausted by the descent into the top-level struct, so we
// don't load what hides under the interface).
//
// v -> structAsStarlarkValue<api.Variable<StructWithInterface>>
// v.Children[0] -> structAsStarlarkValue<api.Variable<interface{}>>
// v.Children[0].Children[0] -> structAsStarlarkValue<api.Variable<*int>>
// v.Children[0].Children[0].Value -> ptrVariableAsStarlarkValue<*int> ; this pointer variable has not been loaded
name: "partial load because of recursion limit",
script: `
v = eval(
None, "ba", {"MaxVariableRecurse": 0, "MaxStructFields": 10}
).Variable;
v.Children[0].Children[0].Value.XXX
`,
expErr: "*int has no .XXX field or method",
},
} {
t.Run(tc.name, func(t *testing.T) {
_, err := term.ExecStarlark(tc.script)
if err == nil {
t.Fatalf("expected error %q, got success", tc.expErr)
}
if !strings.Contains(err.Error(), tc.expErr) {
t.Fatalf("expected error %q, got \"%s\"", tc.expErr, err)
}
})
}
})
}