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 15 commits into
base: master
Choose a base branch
from
Open
14 changes: 12 additions & 2 deletions gnovm/pkg/gnolang/preprocess.go
Original file line number Diff line number Diff line change
Expand Up @@ -1362,6 +1362,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)
}
ft = cft
case *NativeType:
ft = store.Go2GnoType(cft.Type).(*FuncType)
Expand All @@ -1372,11 +1384,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
163 changes: 163 additions & 0 deletions gnovm/pkg/gnolang/type_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package gnolang

import (
"fmt"
"math"
"reflect"
"strconv"

"github.com/gnolang/gno/tm2/pkg/errors"
)
Expand Down Expand Up @@ -1276,3 +1278,164 @@ func isBlankIdentifier(x Expr) bool {
}
return false
}

// 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 {
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")
}

typeStr := at.String()

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

// 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 _, ok := currExpr.Args[2].(*BasicLitExpr); ok {
if capacity < length {
panic(fmt.Sprintf("invalid argument: length and capacity swapped"))
}
}
}
}

// 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("%g (untyped float constant) truncated to int", f)
}
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}