Skip to content

fix: Missing Type Check for Built-in Function Arguments (len, cap) Causes Runtime Panic #3454 #4033

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

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
172 changes: 170 additions & 2 deletions gnovm/pkg/gnolang/preprocess.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"math/big"
"reflect"
"slices"
"strconv"
"strings"
"sync/atomic"

Expand Down Expand Up @@ -1362,6 +1363,18 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node {
ift := evalStaticTypeOf(store, last, n.Func)
switch cft := baseOf(ift).(type) {
case *FuncType:
fn := extractFunctionName(n)
if fn == "len" { //nolint
at := evalStaticTypeOf(store, last, n.Args[0])
validateLenArg(at)
}
if fn == "cap" { //nolint
at := evalStaticTypeOf(store, last, n.Args[0])
validateCapArg(at)
}
if fn == "make" {
assertValidBuiltinArgType(store, last, n)
Copy link
Member

Choose a reason for hiding this comment

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

Just use evalStaticTypeOf on each argument.

}
ft = cft
case *NativeType:
ft = store.Go2GnoType(cft.Type).(*FuncType)
Expand All @@ -1372,11 +1385,9 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node {
n.NumArgs = 1
ct := evalStaticType(store, last, n.Func)
at := evalStaticTypeOf(store, last, n.Args[0])

if _, isIface := baseOf(ct).(*InterfaceType); isIface {
assertAssignableTo(n, at, ct, false)
}

var constConverted bool
switch arg0 := n.Args[0].(type) {
case *ConstExpr:
Expand Down Expand Up @@ -5497,3 +5508,160 @@ func SaveBlockNodes(store Store, fn *FileNode) {
return n, TRANS_CONTINUE
})
}

// Check for invalid arguments in builtin functions
// len, cap, make
func assertValidBuiltinArgType(store Store, last BlockNode, currExpr Expr) {
switch currExpr := currExpr.(type) {
case *ConstExpr:
assertValidBuiltinArgType(store, last, currExpr.Source)
case *CallExpr:
ift := evalStaticTypeOf(store, last, currExpr.Func)
switch baseOf(ift).(type) {
case *FuncType:
validateMakeArg(store, last, currExpr)
}
}
}

// Validate argument for len() function
func validateLenArg(at Type) {
if at == nil {
panic("invalid argument: nil for built-in len")
}

switch at := unwrapPointerType(baseOf(at)).(type) {
case PrimitiveType:
if at.Kind() != StringKind && at != UntypedStringType {
panic(fmt.Sprintf("unexpected type for len(): %v", at))
}
case *ArrayType, *SliceType, *MapType, *ChanType:
// Valid types for len()
default:
panic(fmt.Sprintf("unexpected type for len(): %v", baseOf(at)))
}
}

// Validate argument for cap() function
func validateCapArg(at Type) {
if at == nil {
panic("invalid argument: nil for built-in cap")
}

switch at := unwrapPointerType(baseOf(at)).(type) {
case *ArrayType, *SliceType, *ChanType:
// Valid types for cap()
default:
panic(fmt.Sprintf("unexpected type for cap(): %v", at))
}
}

// Validate arguments for make() function
func validateMakeArg(store Store, last BlockNode, currExpr *CallExpr) {
if len(currExpr.Args) < 1 || len(currExpr.Args) > 3 {
panic(fmt.Sprintf("invalid number of arguments for built-in make: expected 1 to 3, got %d", len(currExpr.Args)))
}

// Validate first argument: must be a slice, map, or channel type
at := evalStaticType(store, last, currExpr.Args[0])
validateMakeType(baseOf(at))

var length int
var capacity int

// Validate length argument (second argument for make)
if len(currExpr.Args) > 1 {
at1 := evalStaticTypeOf(store, last, currExpr.Args[1])
if at1 == nil {
panic("invalid argument 2: nil for built-in make")
}

var err error
length, err = parseIntValue(currExpr.Args[1], "length")
if err != nil {
panic(err.Error()) // Convert error to panic message
}
if length < 0 {
panic(fmt.Sprintf("invalid length argument for built-in %s: cannot be negative, got %d", "make", length))
}
}

// Validate capacity argument (third argument for make)
if len(currExpr.Args) > 2 {
at2 := evalStaticTypeOf(store, last, currExpr.Args[2])
if at2 == nil {
panic("invalid argument 3: nil for built-in make")
}
var err error
capacity, err = parseIntValue(currExpr.Args[2], "capacity")
if err != nil {
panic(err.Error()) // Convert error to panic message
}
if capacity < length {
capacity = length
}
}
}

// Ensures that the first argument to make() is a valid type
func validateMakeType(tp Type) {
switch tp.(type) {
case *SliceType, *MapType, *ChanType:
// Valid types for make()
default:
panic(fmt.Sprintf("invalid type for built-in %s: %s (must be slice, map, or channel)", "make", tp.String()))
}
}

func parseIntValue(expr Expr, argName string) (int, error) {
switch e := expr.(type) {
case *ConstExpr:
return parseIntValue(e.Source, argName)

case *BasicLitExpr:
var num int64
if e.Kind == FLOAT {
f, err := hasFractionalPart(e.Value)
if err != nil {
return 0, err
}
num = int64(f)
} else {
n, err := strconv.Atoi(e.Value)
if err != nil {
return 0, fmt.Errorf("invalid %s argument for built-in %s: cannot convert %q to int", argName, "make", e.Value)
}
return n, nil
}
return int(num), nil
}
return 0, nil
}

func extractFunctionName(n *CallExpr) string {
ce, ok := n.Func.(*ConstExpr)
if !ok {
return ""
}

ne, ok := ce.Source.(*NameExpr)
if !ok {
return ""
}
return string(ne.Name)
}

func hasFractionalPart(s string) (float64, error) {
// Parse the string as a float64
f, err := strconv.ParseFloat(s, 64)
if err != nil {
return 0, err
}

// Check if the float has a fractional part by comparing it with its integer part
intPart := math.Trunc(f)
if f != intPart {
return f, fmt.Errorf("invalid argument: %s has a fractional part", s)
}
return f, nil
}
2 changes: 1 addition & 1 deletion gnovm/tests/files/cap10.gno
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ func main() {
}

// Error:
// unexpected type for cap(): struct{A int}
// main/files/cap10.gno:4:17: unexpected type for cap(): struct{A int}
2 changes: 1 addition & 1 deletion gnovm/tests/files/cap7.gno
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ func main() {
}

// Error:
// unexpected type for cap(): string
// main/files/cap7.gno:5:17: unexpected type for cap(): string
2 changes: 1 addition & 1 deletion gnovm/tests/files/cap8.gno
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ func main() {
}

// Error:
// unexpected type for cap(): *int
// main/files/cap8.gno:5:17: unexpected type for cap(): int
2 changes: 1 addition & 1 deletion gnovm/tests/files/cap9.gno
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ func main() {
}

// Error:
// unexpected type for cap(): *int
// main/files/cap9.gno:5:17: unexpected type for cap(): int
2 changes: 1 addition & 1 deletion gnovm/tests/files/len7.gno
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ func main() {
}

// Error:
// unexpected type for len(): *int
// main/files/len7.gno:4:10: unexpected type for len(): int
4 changes: 2 additions & 2 deletions gnovm/tests/files/len8.gno
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package main
package main

func main() {
println(len(struct {
Expand All @@ -7,4 +7,4 @@ func main() {
}

// Error:
// unexpected type for len(): struct{A int;B int}
// main/files/len8.gno:4:10: unexpected type for len(): struct{A int;B int}