Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions interp/builtins/tests/cat/cat_fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2026-present Datadog, Inc.

package cat_test

import (
"bytes"
"context"
"os"
"path/filepath"
"testing"
"time"

"github.com/DataDog/rshell/interp"
"github.com/DataDog/rshell/interp/builtins/testutil"
)

func cmdRunCtx(ctx context.Context, t *testing.T, script, dir string) (string, string, int) {
t.Helper()
return testutil.RunScriptCtx(ctx, t, script, dir, interp.AllowedPaths([]string{dir}))
}

// FuzzCat fuzzes cat with arbitrary file content and verifies output equals input.
func FuzzCat(f *testing.F) {
f.Add([]byte("hello\nworld\n"))
f.Add([]byte{})
f.Add([]byte("no newline"))
f.Add([]byte("a\x00b\n"))
f.Add(bytes.Repeat([]byte("x"), 4097))
f.Add([]byte("\n\n\n"))
f.Add(bytes.Repeat([]byte("y"), 4096))
f.Add([]byte{0xff, 0xfe, 0x00, 0x01})

f.Fuzz(func(t *testing.T, input []byte) {
if len(input) > 1<<20 {
return
}

dir := t.TempDir()
err := os.WriteFile(filepath.Join(dir, "input.txt"), input, 0644)
if err != nil {
t.Fatal(err)
}

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

stdout, _, code := cmdRunCtx(ctx, t, "cat input.txt", dir)
if code != 0 && code != 1 {
t.Errorf("unexpected exit code %d", code)
}

// cat must output exactly the file contents
if code == 0 && stdout != string(input) {
t.Errorf("cat output differs from input: got %d bytes, want %d bytes", len(stdout), len(input))
}
})
}

// FuzzCatNumberLines fuzzes cat -n with arbitrary file content.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

P3 Badge Output equality assertion can fire as a false positive on context timeout

When the 5-second context deadline is exceeded, testutil.RunScriptCtx returns (partialStdout, "", 0) — exit code 0 with potentially incomplete output (because ctx.Err() != nil suppresses the t.Fatalf call but the exit code stays 0). The subsequent assertion code == 0 && stdout != string(input) would then record the timeout as a fuzz failure, potentially creating false-positive corpus entries.

This only affects extended fuzz runs (not the CI seed-corpus check), but could cause spurious findings during local fuzzing. Consider skipping the output assertion when the context is done:

Suggested change
// FuzzCatNumberLines fuzzes cat -n with arbitrary file content.
if code == 0 && ctx.Err() == nil && stdout != string(input) {
t.Errorf("cat output differs from input: got %d bytes, want %d bytes", len(stdout), len(input))
}

The same pattern applies to FuzzCatStdin at line 124.

func FuzzCatNumberLines(f *testing.F) {
f.Add([]byte("line1\nline2\n"))
f.Add([]byte{})
f.Add([]byte("no newline"))
f.Add([]byte("a\x00b\nc\n"))
f.Add([]byte("\n\n\n"))

f.Fuzz(func(t *testing.T, input []byte) {
if len(input) > 1<<20 {
return
}

dir := t.TempDir()
err := os.WriteFile(filepath.Join(dir, "input.txt"), input, 0644)
if err != nil {
t.Fatal(err)
}

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

_, _, code := cmdRunCtx(ctx, t, "cat -n input.txt", dir)
if code != 0 && code != 1 {
t.Errorf("cat -n unexpected exit code %d", code)
}
})
}

// FuzzCatStdin fuzzes cat reading from stdin via shell redirection.
func FuzzCatStdin(f *testing.F) {
f.Add([]byte("hello\nworld\n"))
f.Add([]byte{})
f.Add([]byte("no newline"))
f.Add([]byte("a\x00b\n"))
f.Add(bytes.Repeat([]byte("x"), 4097))
f.Add([]byte("\n\n\n"))

f.Fuzz(func(t *testing.T, input []byte) {
if len(input) > 1<<20 {
return
}

dir := t.TempDir()
err := os.WriteFile(filepath.Join(dir, "stdin.txt"), input, 0644)
if err != nil {
t.Fatal(err)
}

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

stdout, _, code := cmdRunCtx(ctx, t, "cat < stdin.txt", dir)
if code != 0 && code != 1 {
t.Errorf("cat stdin unexpected exit code %d", code)
}

if code == 0 && stdout != string(input) {
t.Errorf("cat stdin output differs from input: got %d bytes, want %d bytes", len(stdout), len(input))
}
})
}
147 changes: 147 additions & 0 deletions interp/builtins/tests/grep/grep_fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2026-present Datadog, Inc.

package grep_test

import (
"bytes"
"context"
"os"
"path/filepath"
"testing"
"time"

"unicode/utf8"
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

P3 Badge unicode/utf8 import in its own group — should be grouped with other stdlib imports

unicode/utf8 is a standard library package but is placed in a separate import group between the other stdlib packages and the third-party packages. goimports convention is: stdlib packages together, then a blank line, then third-party. gofmt doesn't flag this (it doesn't reorder imports), but editors with goimports configured will reformat on save.

Suggested change
"unicode/utf8"
import (
"bytes"
"context"
"os"
"path/filepath"
"testing"
"time"
"unicode/utf8"
"github.com/DataDog/rshell/interp"
"github.com/DataDog/rshell/interp/builtins/testutil"
)


"github.com/DataDog/rshell/interp"
"github.com/DataDog/rshell/interp/builtins/testutil"
)

func cmdRunCtx(ctx context.Context, t *testing.T, script, dir string) (string, string, int) {
t.Helper()
return testutil.RunScriptCtx(ctx, t, script, dir, interp.AllowedPaths([]string{dir}))
}

// FuzzGrepFileContent fuzzes grep with a fixed pattern and arbitrary file content.
func FuzzGrepFileContent(f *testing.F) {
f.Add([]byte("apple\nbanana\ncherry\n"), "banana")
f.Add([]byte{}, "anything")
f.Add([]byte("no newline"), "new")
f.Add([]byte("a\x00b\nc\n"), "a")
f.Add(bytes.Repeat([]byte("x"), 4097), "x")
f.Add([]byte("\n\n\n"), ".")
f.Add([]byte("hello world\nfoo bar\n"), "foo")
f.Add([]byte{0xff, 0xfe}, "a")

f.Fuzz(func(t *testing.T, input []byte, pattern string) {
if len(input) > 1<<20 {
return
}
// Skip patterns containing non-UTF-8 sequences: the shell parser's
// tokenizer rejects them before grep runs, so they exercise the parser
// error path rather than the grep builtin.
if !utf8.ValidString(pattern) {
return
}
// Skip patterns that would be problematic in shell quoting or cause the
// shell parser to fail before grep runs.
for _, c := range pattern {
if c == '\'' || c == '\x00' || c == '\n' {
return
Comment thread
thieman marked this conversation as resolved.
}
}
if len(pattern) == 0 {
return
}
if len(pattern) > 100 {
return
}

dir := t.TempDir()
err := os.WriteFile(filepath.Join(dir, "input.txt"), input, 0644)
if err != nil {
t.Fatal(err)
}

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

// Use single-quoted pattern to avoid shell interpretation
script := "grep '" + pattern + "' input.txt"
_, _, code := cmdRunCtx(ctx, t, script, dir)
Comment thread
thieman marked this conversation as resolved.
// grep exits 0 (match found), 1 (no match), or 2 (error/invalid regex)
if code != 0 && code != 1 && code != 2 {
t.Errorf("grep unexpected exit code %d", code)
}
})
}

// FuzzGrepStdin fuzzes grep reading from stdin with arbitrary content.
func FuzzGrepStdin(f *testing.F) {
f.Add([]byte("apple\nbanana\ncherry\n"))
f.Add([]byte{})
f.Add([]byte("no newline"))
f.Add([]byte("a\x00b\nc\n"))
f.Add(bytes.Repeat([]byte("x"), 4097))
f.Add([]byte("\n\n\n"))

f.Fuzz(func(t *testing.T, input []byte) {
if len(input) > 1<<20 {
return
}

dir := t.TempDir()
err := os.WriteFile(filepath.Join(dir, "stdin.txt"), input, 0644)
if err != nil {
t.Fatal(err)
}

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

_, _, code := cmdRunCtx(ctx, t, "grep '.' < stdin.txt", dir)
if code != 0 && code != 1 && code != 2 {
t.Errorf("grep stdin unexpected exit code %d", code)
}
})
}

// FuzzGrepFlags fuzzes grep with various flags and arbitrary file content.
func FuzzGrepFlags(f *testing.F) {
f.Add([]byte("Hello\nworld\nHELLO\n"), true, false)
f.Add([]byte("line1\nline2\n"), false, true)
f.Add([]byte{}, true, true)
f.Add([]byte("no newline"), false, false)
f.Add(bytes.Repeat([]byte("abc\n"), 100), true, false)

f.Fuzz(func(t *testing.T, input []byte, caseInsensitive bool, invertMatch bool) {
if len(input) > 1<<20 {
return
}

dir := t.TempDir()
err := os.WriteFile(filepath.Join(dir, "input.txt"), input, 0644)
if err != nil {
t.Fatal(err)
}

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

flags := ""
if caseInsensitive {
flags += " -i"
}
if invertMatch {
flags += " -v"
}

script := "grep" + flags + " 'a' input.txt"
_, _, code := cmdRunCtx(ctx, t, script, dir)
if code != 0 && code != 1 && code != 2 {
t.Errorf("grep%s unexpected exit code %d", flags, code)
}
})
}
Loading
Loading