From d3d973d2118d7ab2691c827eee7b090610fcd416 Mon Sep 17 00:00:00 2001 From: Steve Kemp Date: Wed, 22 Nov 2023 20:34:32 +0200 Subject: [PATCH] Add a fuzz-tester This pull-request, still in-progress, adds a minimal fuzzer to the test-suite and patches up some bogus crashes and panics that were found almost immediately. Ouch. * Division by zero is caught. * Issues with the backtick operator were caught. * Issues with null-operands were caught * Though I kinda feel the parser is at fault here. --- evaluator/evaluator.go | 64 +++++++++++++++++++++- fuzz_test.go | 67 +++++++++++++++++++++++ lexer/lexer.go | 19 +++++++ parser/parser.go | 7 ++- testdata/fuzz/FuzzMonkey/34fb718936c5abee | 2 + testdata/fuzz/FuzzMonkey/3eefa231244e4231 | 2 + testdata/fuzz/FuzzMonkey/6b91ac7f9f7618fb | 2 + testdata/fuzz/FuzzMonkey/6ca869e0fd4fbda4 | 2 + testdata/fuzz/FuzzMonkey/7503d7e4a29aaa55 | 2 + testdata/fuzz/FuzzMonkey/bb33601cb4718064 | 2 + testdata/fuzz/FuzzMonkey/f35e37c0e30ffd2e | 2 + 11 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 fuzz_test.go create mode 100644 testdata/fuzz/FuzzMonkey/34fb718936c5abee create mode 100644 testdata/fuzz/FuzzMonkey/3eefa231244e4231 create mode 100644 testdata/fuzz/FuzzMonkey/6b91ac7f9f7618fb create mode 100644 testdata/fuzz/FuzzMonkey/6ca869e0fd4fbda4 create mode 100644 testdata/fuzz/FuzzMonkey/7503d7e4a29aaa55 create mode 100644 testdata/fuzz/FuzzMonkey/bb33601cb4718064 create mode 100644 testdata/fuzz/FuzzMonkey/f35e37c0e30ffd2e diff --git a/evaluator/evaluator.go b/evaluator/evaluator.go index 5f9d372..dabde86 100644 --- a/evaluator/evaluator.go +++ b/evaluator/evaluator.go @@ -277,6 +277,11 @@ func evalBangOperatorExpression(right object.Object) object.Object { } func evalMinusPrefixOperatorExpression(right object.Object) object.Object { + // Found by fuzzing + if right == nil { + return newError("null operand %v", right) + } + switch obj := right.(type) { case *object.Integer: return &object.Integer{Value: -obj.Value} @@ -288,6 +293,12 @@ func evalMinusPrefixOperatorExpression(right object.Object) object.Object { } func evalInfixExpression(operator string, left, right object.Object, env *object.Environment) object.Object { + + // Found by fuzzing + if left == nil || right == nil { + return newError("null operand %v %v", left, right) + } + switch { case left.Type() == object.INTEGER_OBJ && right.Type() == object.INTEGER_OBJ: return evalIntegerInfixExpression(operator, left, right) @@ -412,6 +423,11 @@ func evalBooleanInfixExpression(operator string, left, right object.Object) obje } func evalIntegerInfixExpression(operator string, left, right object.Object) object.Object { + // Found by fuzzing + if left == nil || right == nil { + return newError("null operand %v %v", left, right) + } + leftVal := left.(*object.Integer).Value rightVal := right.(*object.Integer).Value switch operator { @@ -420,6 +436,11 @@ func evalIntegerInfixExpression(operator string, left, right object.Object) obje case "+=": return &object.Integer{Value: leftVal + rightVal} case "%": + // Found by fuzzing + if rightVal == 0 { + return newError("divide by zero") + } + return &object.Integer{Value: leftVal % rightVal} case "**": return &object.Integer{Value: int64(math.Pow(float64(leftVal), float64(rightVal)))} @@ -432,6 +453,10 @@ func evalIntegerInfixExpression(operator string, left, right object.Object) obje case "*=": return &object.Integer{Value: leftVal * rightVal} case "/": + // Found by fuzzing + if rightVal == 0 { + return newError("divide by zero") + } return &object.Integer{Value: leftVal / rightVal} case "/=": return &object.Integer{Value: leftVal / rightVal} @@ -498,6 +523,10 @@ func evalFloatInfixExpression(operator string, left, right object.Object) object case "**": return &object.Float{Value: math.Pow(leftVal, rightVal)} case "/": + // Found by fuzzing + if rightVal == 0 { + return newError("divide by zero") + } return &object.Float{Value: leftVal / rightVal} case "/=": return &object.Float{Value: leftVal / rightVal} @@ -538,6 +567,10 @@ func evalFloatIntegerInfixExpression(operator string, left, right object.Object) case "**": return &object.Float{Value: math.Pow(leftVal, rightVal)} case "/": + // Found by fuzzing + if rightVal == 0 { + return newError("divide by zero") + } return &object.Float{Value: leftVal / rightVal} case "/=": return &object.Float{Value: leftVal / rightVal} @@ -578,6 +611,10 @@ func evalIntegerFloatInfixExpression(operator string, left, right object.Object) case "**": return &object.Float{Value: math.Pow(leftVal, rightVal)} case "/": + // Found by fuzzing + if rightVal == 0 { + return newError("divide by zero") + } return &object.Float{Value: leftVal / rightVal} case "/=": return &object.Float{Value: leftVal / rightVal} @@ -1009,9 +1046,29 @@ func trimQuotes(in string, c byte) string { // `stderr`, `stdout`, and `error` will be the fields func backTickOperation(command string) object.Object { + command = strings.TrimSpace(command) + if command == "" { + return newError("empty command") + } + + // default arguments, if none are found + args := []string{} + // split the command toExec := splitCommand(command) - cmd := exec.Command(toExec[0], toExec[1:]...) + + // Did that work? + if len(args) == 0 { + return newError("error - empty command") + } + + // Use the real args if we got any + if len(args) > 1 { + args = toExec[1:] + } + + // Run the ocmmand. + cmd := exec.Command(toExec[0], args...) // get the result var outb, errb bytes.Buffer @@ -1171,6 +1228,11 @@ func RegisterBuiltin(name string, fun object.BuiltinFunction) { func evalObjectCallExpression(ctx context.Context, call *ast.ObjectCallExpression, env *object.Environment) object.Object { obj := EvalContext(ctx, call.Object, env) + + if obj == nil { + return newError("impossible object-call on an empty object") + } + if method, ok := call.Call.(*ast.CallExpression); ok { // diff --git a/fuzz_test.go b/fuzz_test.go new file mode 100644 index 0000000..575e8ca --- /dev/null +++ b/fuzz_test.go @@ -0,0 +1,67 @@ +//go:build go1.18 +// +build go1.18 + +package main + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/skx/monkey/evaluator" + "github.com/skx/monkey/lexer" + "github.com/skx/monkey/object" + "github.com/skx/monkey/parser" +) + +// FuzzMonkey runs the fuzz-testing against our parser and interpreter. +func FuzzMonkey(f *testing.F) { + + // Known errors we might see + known := []string{ + "as integer", + "divide by zero", + "null operand", + "could not parse", + "exceeded", + "expected assign", + "expected next token", + "impossible", + "nested ternary expressions are illegal", + "no prefix parse function", + } + + f.Fuzz(func(t *testing.T, input []byte) { + + ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) + defer cancel() + + env := object.NewEnvironment() + l := lexer.New(string(input)) + p := parser.New(l) + + program := p.ParseProgram() + falsePositive := false + + // No errors? Then execute + if len(p.Errors()) == 0 { + + evaluator.EvalContext(ctx, program, env) + return + } + + for _, msg := range p.Errors() { + for _, ignored := range known { + if strings.Contains(msg, ignored) { + falsePositive = true + } + } + + } + + if !falsePositive { + t.Fatalf("error running input: '%s': %v", input, p.Errors()) + } + }) +} diff --git a/lexer/lexer.go b/lexer/lexer.go index 23320c6..379e2ca 100644 --- a/lexer/lexer.go +++ b/lexer/lexer.go @@ -271,7 +271,26 @@ func (l *Lexer) NextToken() token.Token { return tok } + + // Not printable? That's a bug + if !unicode.IsPrint(l.ch) { + tok.Literal = string(l.ch) + tok.Type = token.ILLEGAL + + // skip the characters + l.readChar() + return tok + } + tok.Literal = l.readIdentifier() + + // Did we fail to read a token? + if len(tok.Literal) == 0 { + // Then we've got an illegal + tok.Type = token.ILLEGAL + l.readChar() + return tok + } tok.Type = token.LookupIdentifier(tok.Literal) l.prevToken = tok diff --git a/parser/parser.go b/parser/parser.go index 73ce6dd..0049bc1 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -930,7 +930,12 @@ func (p *Parser) parseAssignExpression(name ast.Expression) ast.Expression { if n, ok := name.(*ast.Identifier); ok { stmt.Name = n } else { - msg := fmt.Sprintf("expected assign token to be IDENT, got %s instead around line %d", name.TokenLiteral(), p.l.GetLine()) + msg := "expected assign token to be IDENT, got null instead" + + // found by fuzzer + if name != nil { + msg = fmt.Sprintf("expected assign token to be IDENT, got %s instead around line %d", name.TokenLiteral(), p.l.GetLine()) + } p.errors = append(p.errors, msg) } diff --git a/testdata/fuzz/FuzzMonkey/34fb718936c5abee b/testdata/fuzz/FuzzMonkey/34fb718936c5abee new file mode 100644 index 0000000..4f69cdb --- /dev/null +++ b/testdata/fuzz/FuzzMonkey/34fb718936c5abee @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("0%00") diff --git a/testdata/fuzz/FuzzMonkey/3eefa231244e4231 b/testdata/fuzz/FuzzMonkey/3eefa231244e4231 new file mode 100644 index 0000000..7325d8b --- /dev/null +++ b/testdata/fuzz/FuzzMonkey/3eefa231244e4231 @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("`0`") diff --git a/testdata/fuzz/FuzzMonkey/6b91ac7f9f7618fb b/testdata/fuzz/FuzzMonkey/6b91ac7f9f7618fb new file mode 100644 index 0000000..6353c80 --- /dev/null +++ b/testdata/fuzz/FuzzMonkey/6b91ac7f9f7618fb @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("0b=") diff --git a/testdata/fuzz/FuzzMonkey/6ca869e0fd4fbda4 b/testdata/fuzz/FuzzMonkey/6ca869e0fd4fbda4 new file mode 100644 index 0000000..a907d5c --- /dev/null +++ b/testdata/fuzz/FuzzMonkey/6ca869e0fd4fbda4 @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("\xe0.0A)000") diff --git a/testdata/fuzz/FuzzMonkey/7503d7e4a29aaa55 b/testdata/fuzz/FuzzMonkey/7503d7e4a29aaa55 new file mode 100644 index 0000000..f2fa28a --- /dev/null +++ b/testdata/fuzz/FuzzMonkey/7503d7e4a29aaa55 @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("7\xdc%00000000000\"00000000000000") diff --git a/testdata/fuzz/FuzzMonkey/bb33601cb4718064 b/testdata/fuzz/FuzzMonkey/bb33601cb4718064 new file mode 100644 index 0000000..ffcbcaa --- /dev/null +++ b/testdata/fuzz/FuzzMonkey/bb33601cb4718064 @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("``") diff --git a/testdata/fuzz/FuzzMonkey/f35e37c0e30ffd2e b/testdata/fuzz/FuzzMonkey/f35e37c0e30ffd2e new file mode 100644 index 0000000..95d1611 --- /dev/null +++ b/testdata/fuzz/FuzzMonkey/f35e37c0e30ffd2e @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("-")