Skip to content
Merged
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
64 changes: 62 additions & 2 deletions .github/workflows/fuzz.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,39 @@ jobs:
echo "No fuzz targets found in ${{ matrix.pkg }}, skipping"
exit 0
fi
# fuzz_run wraps a single fuzz invocation and treats "context deadline exceeded"
# at the fuzz time boundary as success. When -fuzztime=30s expires, Go's internal
# fuzz coordinator cancels any in-flight iteration and emits "context deadline
# exceeded" with no file:line reference — this is not a real test failure.
# Real failures always include a file:line reference (e.g. foo_test.go:42:).
#
# We use `tee` rather than output=$(...) so that:
# 1. Progress lines appear in the Actions log in real time (output=$(...) buffers
# everything and bash -e exits the function before we ever reach echo "$output").
# 2. PIPESTATUS[0] captures go test's exit code even when the pipe itself exits 0.
fuzz_run() {
local func="$1"
local tmpfile exit_code
tmpfile=$(mktemp)
go test -fuzz="^${func}$" -fuzztime=30s ${{ matrix.pkg }} -timeout 90s 2>&1 | tee "$tmpfile" || true
exit_code=${PIPESTATUS[0]}
if [ $exit_code -ne 0 ]; then
# Check whether any line has a file:line reference (real assertion failure).
# Coordinator boundary timeouts produce " context deadline exceeded" with
# no file:line, so they will NOT match this pattern.
if grep -qE '[[:space:]]+[^[:space:]]+_test\.go:[0-9]+:' "$tmpfile"; then
rm -f "$tmpfile"
echo "FAIL: $func — test assertion failure detected" >&2
return $exit_code
fi
echo "NOTE: $func — fuzz coordinator boundary timeout (expected at fuzz time limit, not a failure)"
fi
rm -f "$tmpfile"
return 0
}
for FUNC in $FUZZ_FUNCS; do
echo "Fuzzing $FUNC..."
go test -fuzz="^${FUNC}$" -fuzztime=30s ${{ matrix.pkg }} -timeout 300s
fuzz_run "$FUNC"
done

# Save corpus
Expand Down Expand Up @@ -159,9 +189,39 @@ jobs:
echo "No differential fuzz targets found in ${{ matrix.pkg }}, skipping"
exit 0
fi
# fuzz_run wraps a single fuzz invocation and treats "context deadline exceeded"
# at the fuzz time boundary as success. When -fuzztime=30s expires, Go's internal
# fuzz coordinator cancels any in-flight iteration and emits "context deadline
# exceeded" with no file:line reference — this is not a real test failure.
# Real failures always include a file:line reference (e.g. foo_test.go:42:).
#
# We use `tee` rather than output=$(...) so that:
# 1. Progress lines appear in the Actions log in real time (output=$(...) buffers
# everything and bash -e exits the function before we ever reach echo "$output").
# 2. PIPESTATUS[0] captures go test's exit code even when the pipe itself exits 0.
fuzz_run() {
local func="$1"
local tmpfile exit_code
tmpfile=$(mktemp)
go test -fuzz="^${func}$" -fuzztime=30s ${{ matrix.pkg }} -timeout 90s 2>&1 | tee "$tmpfile" || true
exit_code=${PIPESTATUS[0]}
if [ $exit_code -ne 0 ]; then
# Check whether any line has a file:line reference (real assertion failure).
# Coordinator boundary timeouts produce " context deadline exceeded" with
# no file:line, so they will NOT match this pattern.
if grep -qE '[[:space:]]+[^[:space:]]+_test\.go:[0-9]+:' "$tmpfile"; then
rm -f "$tmpfile"
echo "FAIL: $func — test assertion failure detected" >&2
return $exit_code
fi
echo "NOTE: $func — fuzz coordinator boundary timeout (expected at fuzz time limit, not a failure)"
fi
rm -f "$tmpfile"
return 0
}
for FUNC in $FUZZ_FUNCS; do
echo "Fuzzing $FUNC..."
go test -fuzz="^${FUNC}$" -fuzztime=30s ${{ matrix.pkg }} -timeout 300s
fuzz_run "$FUNC"
done

- name: Save fuzz corpus
Expand Down
8 changes: 5 additions & 3 deletions builtins/internal/procinfo/procinfo_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,18 @@ func getSession(ctx context.Context, procPath string) ([]ProcInfo, error) {

selfPID := os.Getpid()
ancestors := make(map[int]bool)
visited := make(map[int]bool)
cur := selfPID
for cur > 0 {
if visited[cur] {
break // cycle detected in PPID chain
}
visited[cur] = true
ancestors[cur] = true
p, ok := byPID[cur]
if !ok {
break
}
if p.PPID == cur {
break // avoid infinite loop for PID 0
}
cur = p.PPID
}

Expand Down
16 changes: 14 additions & 2 deletions builtins/ping/ping_fuzz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ func FuzzPingFlags(f *testing.F) {
f.Add("-h", "")

f.Fuzz(func(t *testing.T, flag, value string) {
if t.Context().Err() != nil {
return
}
// Only allow characters that are safe to pass unquoted in a shell script.
// Using an allowlist is more robust than a denylist: any character not
// explicitly permitted here could cause shell parse errors or command
Expand All @@ -76,7 +79,7 @@ func FuzzPingFlags(f *testing.F) {
return
}

ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
ctx, cancel := context.WithTimeout(t.Context(), 3*time.Second)
defer cancel()

var script string
Expand All @@ -87,6 +90,9 @@ func FuzzPingFlags(f *testing.F) {
}

_, _, code := cmdRunCtxFuzz(ctx, t, script)
if t.Context().Err() != nil {
return
}
if code != 0 && code != 1 {
t.Errorf("unexpected exit code %d for script: %s", code, script)
}
Expand Down Expand Up @@ -122,6 +128,9 @@ func FuzzPingHostname(f *testing.F) {
f.Add("no-such-host-xyzzy.invalid")

f.Fuzz(func(t *testing.T, hostname string) {
if t.Context().Err() != nil {
return
}
// Only allow characters that are safe to pass unquoted as a shell
// argument. An allowlist is more robust than a denylist because the
// shell parser has many special characters and we cannot enumerate
Expand All @@ -144,11 +153,14 @@ func FuzzPingHostname(f *testing.F) {
// 1s is enough for fast DNS + socket attempt; shorter than the
// 3s default to keep CI fuzz runs from stalling on unresolvable
// hostnames when the corpus grows to include slow-DNS entries.
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
ctx, cancel := context.WithTimeout(t.Context(), 1*time.Second)
defer cancel()

script := fmt.Sprintf("ping -c 1 -W 500ms -- %s", hostname)
_, _, code := cmdRunCtxFuzz(ctx, t, script)
if t.Context().Err() != nil {
return
}
if code != 0 && code != 1 {
t.Errorf("unexpected exit code %d for hostname: %q", code, hostname)
}
Expand Down
11 changes: 4 additions & 7 deletions builtins/tests/cat/cat_differential_fuzz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ func FuzzCatDifferential(f *testing.F) {
var counter atomic.Int64

f.Fuzz(func(t *testing.T, input []byte) {
if t.Context().Err() != nil {
return
}
if len(input) > 64*1024 {
return
}
Expand All @@ -86,17 +89,11 @@ func FuzzCatDifferential(f *testing.F) {
t.Fatal(err)
}

// Use context.Background() (not t.Context()) so the fuzz engine's
// cancellation does not kill the command mid-run; each iteration still
// enforces its own 5 s deadline.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel() // safety net if t.Fatal fires before explicit cancel
rshellOut, rshellErr, rshellCode := cmdRunCtx(ctx, t, "cat input.txt", dir)
cancel()

// If the fuzz engine's budget expired (t.Context(), not the per-command
// context above), bail out without comparing — partial output would cause
// false failures.
if t.Context().Err() != nil {
return
}
Expand Down
32 changes: 28 additions & 4 deletions builtins/tests/cat/cat_fuzz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ func FuzzCat(f *testing.F) {
var counter atomic.Int64

f.Fuzz(func(t *testing.T, input []byte) {
if t.Context().Err() != nil {
return
}
if len(input) > 1<<20 {
return
}
Expand All @@ -75,10 +78,13 @@ func FuzzCat(f *testing.F) {
t.Fatal(err)
}

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel() // safety net if t.Fatal fires before explicit cancel
stdout, _, code := cmdRunCtx(ctx, t, "cat input.txt", dir)
cancel()
if t.Context().Err() != nil {
return
}
if code != 0 && code != 1 {
t.Errorf("unexpected exit code %d", code)
}
Expand Down Expand Up @@ -114,6 +120,9 @@ func FuzzCatNumberLines(f *testing.F) {
var counter atomic.Int64

f.Fuzz(func(t *testing.T, input []byte) {
if t.Context().Err() != nil {
return
}
if len(input) > 1<<20 {
return
}
Expand All @@ -126,10 +135,13 @@ func FuzzCatNumberLines(f *testing.F) {
t.Fatal(err)
}

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel() // safety net if t.Fatal fires before explicit cancel
_, _, code := cmdRunCtx(ctx, t, "cat -n input.txt", dir)
cancel()
if t.Context().Err() != nil {
return
}
if code != 0 && code != 1 {
t.Errorf("cat -n unexpected exit code %d", code)
}
Expand Down Expand Up @@ -163,6 +175,9 @@ func FuzzCatDisplayFlags(f *testing.F) {
var counter atomic.Int64

f.Fuzz(func(t *testing.T, input []byte, flagV, flagE, flagT bool) {
if t.Context().Err() != nil {
return
}
if len(input) > 1<<20 {
return
}
Expand All @@ -188,10 +203,13 @@ func FuzzCatDisplayFlags(f *testing.F) {
flags += " -T"
}

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel() // safety net if t.Fatal fires before explicit cancel
_, _, code := cmdRunCtx(ctx, t, "cat"+flags+" input.bin", dir)
cancel()
if t.Context().Err() != nil {
return
}
if code != 0 && code != 1 {
t.Errorf("cat%s unexpected exit code %d", flags, code)
}
Expand All @@ -214,6 +232,9 @@ func FuzzCatStdin(f *testing.F) {
var counter atomic.Int64

f.Fuzz(func(t *testing.T, input []byte) {
if t.Context().Err() != nil {
return
}
if len(input) > 1<<20 {
return
}
Expand All @@ -226,10 +247,13 @@ func FuzzCatStdin(f *testing.F) {
t.Fatal(err)
}

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel() // safety net if t.Fatal fires before explicit cancel
stdout, _, code := cmdRunCtx(ctx, t, "cat < stdin.txt", dir)
cancel()
if t.Context().Err() != nil {
return
}
if code != 0 && code != 1 {
t.Errorf("cat stdin unexpected exit code %d", code)
}
Expand Down
Loading
Loading