Skip to content

Commit

Permalink
Add a fuzz-tester
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
skx committed Nov 22, 2023
1 parent 6c4e83f commit d3d973d
Show file tree
Hide file tree
Showing 11 changed files with 169 additions and 2 deletions.
64 changes: 63 additions & 1 deletion evaluator/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -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)))}
Expand All @@ -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}
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {

//
Expand Down
67 changes: 67 additions & 0 deletions fuzz_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
})
}
19 changes: 19 additions & 0 deletions lexer/lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 6 additions & 1 deletion parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
2 changes: 2 additions & 0 deletions testdata/fuzz/FuzzMonkey/34fb718936c5abee
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("0%00")
2 changes: 2 additions & 0 deletions testdata/fuzz/FuzzMonkey/3eefa231244e4231
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("`0`")
2 changes: 2 additions & 0 deletions testdata/fuzz/FuzzMonkey/6b91ac7f9f7618fb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("0b=")
2 changes: 2 additions & 0 deletions testdata/fuzz/FuzzMonkey/6ca869e0fd4fbda4
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("\xe0.0A)000")
2 changes: 2 additions & 0 deletions testdata/fuzz/FuzzMonkey/7503d7e4a29aaa55
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("7\xdc%00000000000\"00000000000000")
2 changes: 2 additions & 0 deletions testdata/fuzz/FuzzMonkey/bb33601cb4718064
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("``")
2 changes: 2 additions & 0 deletions testdata/fuzz/FuzzMonkey/f35e37c0e30ffd2e
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("-")

0 comments on commit d3d973d

Please sign in to comment.