From 2cba7c70fa5c38cbec912967de252208070d7a18 Mon Sep 17 00:00:00 2001 From: Travis Thieman Date: Thu, 26 Mar 2026 15:41:53 -0400 Subject: [PATCH 1/2] feat(interp): add global 10 MiB stdout cap to Runner.Run Wrap the runner's stdout writer with a limitWriter at the start of each Run() call. Bytes beyond maxStdoutBytes (10 MiB) are silently discarded so that Write() never returns an error and script execution continues normally. This prevents unbounded memory growth from runaway loops or commands that produce excessive output. Also adds TestGlobalStdoutCapSilentlyDrops to verify the cap fires and does not truncate output below the limit. Co-Authored-By: Claude Sonnet 4.6 --- interp/api.go | 4 ++++ interp/runner_expand.go | 6 ++++++ interp/tests/cmdsubst_hardening_test.go | 25 +++++++++++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/interp/api.go b/interp/api.go index ce817918..41cb3a3e 100644 --- a/interp/api.go +++ b/interp/api.go @@ -458,6 +458,10 @@ func (r *Runner) Run(ctx context.Context, node syntax.Node) (retErr error) { return r.exit.err } } + // Wrap stdout with a cap to prevent runaway scripts from producing more + // than maxStdoutBytes of output. Excess bytes are silently discarded so + // that Write never returns an error and downstream code is not disrupted. + r.stdout = &limitWriter{w: r.stdout, limit: maxStdoutBytes} r.startTime = time.Now() r.globReadDirCount = &atomic.Int64{} r.fillExpandConfig(ctx) diff --git a/interp/runner_expand.go b/interp/runner_expand.go index 2307a72d..9a4f3ddc 100644 --- a/interp/runner_expand.go +++ b/interp/runner_expand.go @@ -49,6 +49,12 @@ func (r *Runner) updateExpandOpts() { // commands that produce unbounded output. const maxCmdSubstOutput = 1 << 20 // 1 MiB +// maxStdoutBytes is the maximum number of bytes a script can write to stdout +// before further output is silently discarded. This caps total script output +// to prevent memory exhaustion from runaway commands (e.g. infinite loops +// writing to stdout). +const maxStdoutBytes = 10 * 1024 * 1024 // 10 MiB + // MaxGlobReadDirCalls is the maximum number of ReadDirForGlob invocations // allowed per Run() call. This prevents memory exhaustion from scripts that // trigger an excessive number of glob expansions (e.g. millions of unquoted diff --git a/interp/tests/cmdsubst_hardening_test.go b/interp/tests/cmdsubst_hardening_test.go index 8392dd83..9ccbed40 100644 --- a/interp/tests/cmdsubst_hardening_test.go +++ b/interp/tests/cmdsubst_hardening_test.go @@ -20,6 +20,31 @@ import ( // --- Memory limits: output capping --- +// TestGlobalStdoutCapSilentlyDrops verifies that the global 10 MiB stdout cap +// silently discards bytes beyond the limit rather than returning an error that +// would abort the script. The cat loop writes slightly more than 10 MiB; the +// captured stdout buffer must not exceed maxStdoutBytes. +func TestGlobalStdoutCapSilentlyDrops(t *testing.T) { + dir := t.TempDir() + + // Create a file of exactly 1 MiB. + content := strings.Repeat("A", 1<<20) + require.NoError(t, os.WriteFile(filepath.Join(dir, "mb.txt"), []byte(content), 0644)) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // cat the file 11 times in a loop — produces 11 MiB, exceeding the 10 MiB cap. + script := `for i in 1 2 3 4 5 6 7 8 9 10 11; do cat mb.txt; done` + stdout, _, code := cmdSubstRunCtx(ctx, t, script, dir) + assert.Equal(t, 0, code) + // Output must be capped at 10 MiB (10 * 1024 * 1024 bytes). + assert.LessOrEqual(t, len(stdout), 10*1024*1024, + "stdout must not exceed 10 MiB global cap; got %d bytes", len(stdout)) + // Some output must have been written (the cap does not suppress all output). + assert.Greater(t, len(stdout), 0, "expected non-empty stdout before cap") +} + func TestCmdSubstOutputCapped(t *testing.T) { // Generate output exceeding 1 MiB inside command substitution. // The output should be truncated, not cause OOM. From 48322a24a7b8103b935283c45fa2580ba4f82706 Mon Sep 17 00:00:00 2001 From: Travis Thieman Date: Thu, 26 Mar 2026 15:52:08 -0400 Subject: [PATCH 2/2] feat(fuzz): add semantic property invariants to all fuzz targets Add four invariants to every non-differential fuzz target: 1. Output bounded: assert stdout <= 10 MiB (exercises the new global cap and acts as a redundant safety net). 2. Determinism: run each script twice with identical filesystem state and assert byte-identical stdout and exit code. 3. Exit code validity: assert only 0, 1, (or 2 for commands that use it) are ever returned. 4. No panic: document that reaching the end of the fuzz body proves no panic escaped Runner.Run(). Invariant 2 is skipped for ss, ps, ip addr/link (live kernel state) and differential fuzz targets (bash comparison, not rshell vs rshell). ip route fuzz targets hold the procNetRouteMu mutex for both runs, ensuring the mocked /proc/net/route content is identical. Co-Authored-By: Claude Sonnet 4.6 --- builtins/tests/cat/cat_fuzz_test.go | 84 ++++++++++++- builtins/tests/cut/cut_fuzz_test.go | 113 +++++++++++++++++- builtins/tests/echo/echo_fuzz_test.go | 66 +++++++++- builtins/tests/grep/grep_fuzz_test.go | 110 ++++++++++++++++- builtins/tests/head/head_fuzz_test.go | 64 +++++++++- builtins/tests/ip/ip_fuzz_test.go | 25 +++- builtins/tests/ip/ip_route_fuzz_linux_test.go | 50 +++++++- builtins/tests/ls/ls_fuzz_test.go | 88 +++++++++++++- builtins/tests/ps/ps_fuzz_test.go | 20 ++++ builtins/tests/ss/ss_fuzz_test.go | 12 +- .../tests/strings_cmd/strings_fuzz_test.go | 88 +++++++++++++- builtins/tests/tail/tail_fuzz_test.go | 106 +++++++++++++++- builtins/tests/testcmd/testcmd_fuzz_test.go | 110 ++++++++++++++++- builtins/tests/uniq/uniq_fuzz_test.go | 88 +++++++++++++- builtins/tests/wc/wc_fuzz_test.go | 110 ++++++++++++++++- interp/tests/cmdsubst_fuzz_test.go | 69 ++++++++++- 16 files changed, 1154 insertions(+), 49 deletions(-) diff --git a/builtins/tests/cat/cat_fuzz_test.go b/builtins/tests/cat/cat_fuzz_test.go index 6eef2a06..5c660bee 100644 --- a/builtins/tests/cat/cat_fuzz_test.go +++ b/builtins/tests/cat/cat_fuzz_test.go @@ -85,14 +85,34 @@ func FuzzCat(f *testing.F) { if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("unexpected exit code %d", code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("output exceeds 10 MiB: %d bytes", len(stdout)) + } // 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)) } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtx(ctx2, t, "cat input.txt", dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on cat: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + code, len(stdout), code2, len(stdout2)) + } }) } @@ -137,14 +157,34 @@ func FuzzCatNumberLines(f *testing.F) { 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) + stdout, _, code := cmdRunCtx(ctx, t, "cat -n input.txt", dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("cat -n unexpected exit code %d", code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("cat -n output exceeds 10 MiB: %d bytes", len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtx(ctx2, t, "cat -n input.txt", dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on cat -n: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + code, len(stdout), code2, len(stdout2)) + } }) } @@ -205,14 +245,34 @@ func FuzzCatDisplayFlags(f *testing.F) { 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) + stdout, _, code := cmdRunCtx(ctx, t, "cat"+flags+" input.bin", dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("cat%s unexpected exit code %d", flags, code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("cat%s output exceeds 10 MiB: %d bytes", flags, len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtx(ctx2, t, "cat"+flags+" input.bin", dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on cat%s: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + flags, code, len(stdout), code2, len(stdout2)) + } }) } @@ -254,12 +314,32 @@ func FuzzCatStdin(f *testing.F) { if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("cat stdin unexpected exit code %d", code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("cat stdin output exceeds 10 MiB: %d bytes", len(stdout)) + } if code == 0 && stdout != string(input) { t.Errorf("cat stdin output differs from input: got %d bytes, want %d bytes", len(stdout), len(input)) } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtx(ctx2, t, "cat < stdin.txt", dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on cat stdin: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + code, len(stdout), code2, len(stdout2)) + } }) } diff --git a/builtins/tests/cut/cut_fuzz_test.go b/builtins/tests/cut/cut_fuzz_test.go index a6f87a5e..8d5dbdc8 100644 --- a/builtins/tests/cut/cut_fuzz_test.go +++ b/builtins/tests/cut/cut_fuzz_test.go @@ -102,14 +102,35 @@ func FuzzCutFields(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel - _, _, code := cmdRunCtxFuzz(ctx, t, fmt.Sprintf("cut -f %s input.txt", fieldSpec), dir) + script := fmt.Sprintf("cut -f %s input.txt", fieldSpec) + stdout, _, code := cmdRunCtxFuzz(ctx, t, script, dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("cut -f %s unexpected exit code %d", fieldSpec, code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("cut -f %s output exceeds 10 MiB: %d bytes", fieldSpec, len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtxFuzz(ctx2, t, script, dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on cut -f %s: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + fieldSpec, code, len(stdout), code2, len(stdout2)) + } }) } @@ -176,14 +197,35 @@ func FuzzCutBytes(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel - _, _, code := cmdRunCtxFuzz(ctx, t, fmt.Sprintf("cut -b %s input.txt", byteSpec), dir) + script := fmt.Sprintf("cut -b %s input.txt", byteSpec) + stdout, _, code := cmdRunCtxFuzz(ctx, t, script, dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("cut -b %s unexpected exit code %d", byteSpec, code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("cut -b %s output exceeds 10 MiB: %d bytes", byteSpec, len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtxFuzz(ctx2, t, script, dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on cut -b %s: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + byteSpec, code, len(stdout), code2, len(stdout2)) + } }) } @@ -249,14 +291,34 @@ func FuzzCutDelimiter(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel script := fmt.Sprintf("cut -d '%s' -f %s input.txt", delim, fieldSpec) - _, _, code := cmdRunCtxFuzz(ctx, t, script, dir) + stdout, _, code := cmdRunCtxFuzz(ctx, t, script, dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("cut -d '%s' -f %s unexpected exit code %d", delim, fieldSpec, code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("cut -d '%s' -f %s output exceeds 10 MiB: %d bytes", delim, fieldSpec, len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtxFuzz(ctx2, t, script, dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on cut -d '%s' -f %s: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + delim, fieldSpec, code, len(stdout), code2, len(stdout2)) + } }) } @@ -313,14 +375,35 @@ func FuzzCutComplement(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel - _, _, code := cmdRunCtxFuzz(ctx, t, fmt.Sprintf("cut --complement -b %s input.txt", byteSpec), dir) + script := fmt.Sprintf("cut --complement -b %s input.txt", byteSpec) + stdout, _, code := cmdRunCtxFuzz(ctx, t, script, dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("cut --complement -b %s unexpected exit code %d", byteSpec, code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("cut --complement -b %s output exceeds 10 MiB: %d bytes", byteSpec, len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtxFuzz(ctx2, t, script, dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on cut --complement -b %s: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + byteSpec, code, len(stdout), code2, len(stdout2)) + } }) } @@ -360,13 +443,33 @@ func FuzzCutStdin(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel - _, _, code := cmdRunCtxFuzz(ctx, t, "cut -f 1 < stdin.txt", dir) + stdout, _, code := cmdRunCtxFuzz(ctx, t, "cut -f 1 < stdin.txt", dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("cut stdin unexpected exit code %d", code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("cut stdin output exceeds 10 MiB: %d bytes", len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtxFuzz(ctx2, t, "cut -f 1 < stdin.txt", dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on cut stdin: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + code, len(stdout), code2, len(stdout2)) + } }) } diff --git a/builtins/tests/echo/echo_fuzz_test.go b/builtins/tests/echo/echo_fuzz_test.go index 98064ce1..b2da3022 100644 --- a/builtins/tests/echo/echo_fuzz_test.go +++ b/builtins/tests/echo/echo_fuzz_test.go @@ -65,14 +65,34 @@ func FuzzEcho(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel - _, _, code := fuzzRunCtx(ctx, t, "echo '"+arg+"'", dir) + stdout, _, code := fuzzRunCtx(ctx, t, "echo '"+arg+"'", dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity — echo always exits 0. if code != 0 { t.Errorf("echo unexpected exit code %d", code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("echo output exceeds 10 MiB: %d bytes", len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := fuzzRunCtx(ctx2, t, "echo '"+arg+"'", dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on echo: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + code, len(stdout), code2, len(stdout2)) + } }) } @@ -143,14 +163,34 @@ func FuzzEchoEscapes(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel - _, _, code := fuzzRunCtx(ctx, t, "echo -e '"+arg+"'", dir) + stdout, _, code := fuzzRunCtx(ctx, t, "echo -e '"+arg+"'", dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 { t.Errorf("echo -e unexpected exit code %d", code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("echo -e output exceeds 10 MiB: %d bytes", len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := fuzzRunCtx(ctx2, t, "echo -e '"+arg+"'", dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on echo -e: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + code, len(stdout), code2, len(stdout2)) + } }) } @@ -205,13 +245,33 @@ func FuzzEchoFlagInteraction(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel - _, _, code := fuzzRunCtx(ctx, t, "echo"+flags+" '"+arg+"'", dir) + stdout, _, code := fuzzRunCtx(ctx, t, "echo"+flags+" '"+arg+"'", dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 { t.Errorf("echo%s unexpected exit code %d", flags, code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("echo%s output exceeds 10 MiB: %d bytes", flags, len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := fuzzRunCtx(ctx2, t, "echo"+flags+" '"+arg+"'", dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on echo%s: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + flags, code, len(stdout), code2, len(stdout2)) + } }) } diff --git a/builtins/tests/grep/grep_fuzz_test.go b/builtins/tests/grep/grep_fuzz_test.go index de507b14..034a1b82 100644 --- a/builtins/tests/grep/grep_fuzz_test.go +++ b/builtins/tests/grep/grep_fuzz_test.go @@ -94,14 +94,34 @@ func FuzzGrepFileContent(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel script := "grep '" + pattern + "' input.txt" - _, _, code := cmdRunCtx(ctx, t, script, dir) + stdout, _, code := cmdRunCtx(ctx, t, script, dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity — grep exits 0 (match), 1 (no match), 2 (error). if code != 0 && code != 1 && code != 2 { t.Errorf("grep unexpected exit code %d", code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("grep output exceeds 10 MiB: %d bytes", len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtx(ctx2, t, script, dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on grep: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + code, len(stdout), code2, len(stdout2)) + } }) } @@ -172,14 +192,34 @@ func FuzzGrepPatterns(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel - _, _, code := cmdRunCtx(ctx, t, "grep '"+pattern+"' input.txt", dir) + stdout, _, code := cmdRunCtx(ctx, t, "grep '"+pattern+"' input.txt", dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 && code != 2 { t.Errorf("grep pattern %q unexpected exit code %d", pattern, code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("grep pattern %q output exceeds 10 MiB: %d bytes", pattern, len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtx(ctx2, t, "grep '"+pattern+"' input.txt", dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on grep pattern %q: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + pattern, code, len(stdout), code2, len(stdout2)) + } }) } @@ -216,14 +256,34 @@ func FuzzGrepStdin(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel - _, _, code := cmdRunCtx(ctx, t, "grep '.' < stdin.txt", dir) + stdout, _, code := cmdRunCtx(ctx, t, "grep '.' < stdin.txt", dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 && code != 2 { t.Errorf("grep stdin unexpected exit code %d", code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("grep stdin output exceeds 10 MiB: %d bytes", len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtx(ctx2, t, "grep '.' < stdin.txt", dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on grep stdin: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + code, len(stdout), code2, len(stdout2)) + } }) } @@ -293,14 +353,34 @@ func FuzzGrepFixedStrings(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel - _, _, code := cmdRunCtx(ctx, t, "grep -F '"+pattern+"' input.txt", dir) + stdout, _, code := cmdRunCtx(ctx, t, "grep -F '"+pattern+"' input.txt", dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 && code != 2 { t.Errorf("grep -F unexpected exit code %d", code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("grep -F output exceeds 10 MiB: %d bytes", len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtx(ctx2, t, "grep -F '"+pattern+"' input.txt", dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on grep -F: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + code, len(stdout), code2, len(stdout2)) + } }) } @@ -372,13 +452,33 @@ func FuzzGrepFlags(f *testing.F) { } script := "grep" + flags + " 'a' input.txt" - _, _, code := cmdRunCtx(ctx, t, script, dir) + stdout, _, code := cmdRunCtx(ctx, t, script, dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 && code != 2 { t.Errorf("grep%s unexpected exit code %d", flags, code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("grep%s output exceeds 10 MiB: %d bytes", flags, len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtx(ctx2, t, script, dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on grep%s: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + flags, code, len(stdout), code2, len(stdout2)) + } }) } diff --git a/builtins/tests/head/head_fuzz_test.go b/builtins/tests/head/head_fuzz_test.go index 4ebbf1a4..51d73bd9 100644 --- a/builtins/tests/head/head_fuzz_test.go +++ b/builtins/tests/head/head_fuzz_test.go @@ -84,9 +84,14 @@ func FuzzHeadLines(f *testing.F) { if t.Context().Err() != nil { return } + // Invariant 3: exit code validity — head exits 0 (success) or 1 (error). if code != 0 && code != 1 { t.Errorf("unexpected exit code %d", code) } + // Invariant 1: output bounded — the global 10 MiB stdout cap must hold. + if len(stdout) > 10*1024*1024 { + t.Errorf("output exceeds 10 MiB: %d bytes", len(stdout)) + } // If successful, output line count must be <= n if code == 0 && n >= 0 { @@ -95,6 +100,23 @@ func FuzzHeadLines(f *testing.F) { t.Errorf("head -n %d produced %d newlines in output", n, lineCount) } } + + // Invariant 4: no panic — the runner's deferred recover in api.go catches + // any panic and converts it to an error return. Reaching this line means + // no panic escaped the Run() call. + + // Invariant 2: determinism — identical inputs must produce identical outputs. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtx(ctx2, t, fmt.Sprintf("head -n %d input.txt", n), dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on head -n %d: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + n, code, len(stdout), code2, len(stdout2)) + } }) } @@ -154,9 +176,14 @@ func FuzzHeadBytes(f *testing.F) { if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("unexpected exit code %d", code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("output exceeds 10 MiB: %d bytes", len(stdout)) + } // If successful, output byte count must be <= n if code == 0 { @@ -165,6 +192,21 @@ func FuzzHeadBytes(f *testing.F) { t.Errorf("head -c %d produced %d bytes of output", n, outLen) } } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtx(ctx2, t, fmt.Sprintf("head -c %d input.txt", n), dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on head -c %d: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + n, code, len(stdout), code2, len(stdout2)) + } }) } @@ -206,13 +248,33 @@ func FuzzHeadStdin(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel - _, _, code := cmdRunCtx(ctx, t, fmt.Sprintf("head -n %d < stdin.txt", n), dir) + stdout, _, code := cmdRunCtx(ctx, t, fmt.Sprintf("head -n %d < stdin.txt", n), dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("unexpected exit code %d (stdin mode)", code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("output exceeds 10 MiB: %d bytes", len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtx(ctx2, t, fmt.Sprintf("head -n %d < stdin.txt", n), dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on head stdin -n %d: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + n, code, len(stdout), code2, len(stdout2)) + } }) } diff --git a/builtins/tests/ip/ip_fuzz_test.go b/builtins/tests/ip/ip_fuzz_test.go index 4173aae6..decf604e 100644 --- a/builtins/tests/ip/ip_fuzz_test.go +++ b/builtins/tests/ip/ip_fuzz_test.go @@ -151,7 +151,7 @@ func FuzzIPSubcommand(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel script := "ip " + subcmd - _, _, code := cmdRunCtxFuzz(ctx, t, script) + stdout, _, code := cmdRunCtxFuzz(ctx, t, script) timedOut := ctx.Err() == context.DeadlineExceeded // capture before cancel() cancel() if t.Context().Err() != nil { @@ -160,12 +160,23 @@ func FuzzIPSubcommand(f *testing.F) { if code == -1 { return // shell/parse error before the builtin ran — not our bug } + // Invariant 3: exit code validity. if code != 0 && code != 1 && code != 255 { t.Errorf("ip %q: unexpected exit code %d", subcmd, code) } if timedOut { t.Errorf("ip %q: timed out (possible hang)", subcmd) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("ip %q output exceeds 10 MiB: %d bytes", subcmd, len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Note: Invariant 2 (determinism) is intentionally skipped for ip addr/link + // because these subcommands read live kernel network interface state, which + // may change between calls. }) } @@ -232,7 +243,7 @@ func FuzzIPFlags(f *testing.F) { if subcmd != "" { script += " " + subcmd } - _, _, code := cmdRunCtxFuzz(ctx, t, script) + stdout, _, code := cmdRunCtxFuzz(ctx, t, script) timedOut := ctx.Err() == context.DeadlineExceeded // capture before cancel() cancel() if t.Context().Err() != nil { @@ -241,11 +252,21 @@ func FuzzIPFlags(f *testing.F) { if code == -1 { return // shell/parse error before the builtin ran — not our bug } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("ip %q %q: unexpected exit code %d", flags, subcmd, code) } if timedOut { t.Errorf("ip %q %q: timed out", flags, subcmd) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("ip %q %q output exceeds 10 MiB: %d bytes", flags, subcmd, len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Note: Invariant 2 (determinism) is intentionally skipped for ip addr/link + // because these subcommands read live kernel network interface state. }) } diff --git a/builtins/tests/ip/ip_route_fuzz_linux_test.go b/builtins/tests/ip/ip_route_fuzz_linux_test.go index 81b81175..728fb6b7 100644 --- a/builtins/tests/ip/ip_route_fuzz_linux_test.go +++ b/builtins/tests/ip/ip_route_fuzz_linux_test.go @@ -140,7 +140,7 @@ func FuzzIPRouteParse(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() - _, _, code := cmdRunCtxFuzz(ctx, t, "ip route show") + stdout, _, code := cmdRunCtxFuzz(ctx, t, "ip route show") timedOut := ctx.Err() == context.DeadlineExceeded cancel() if timedOut { @@ -150,9 +150,32 @@ func FuzzIPRouteParse(f *testing.F) { if code == -1 { return // internal shell error before the builtin ran — not our bug } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("FuzzIPRouteParse: unexpected exit code %d", code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("FuzzIPRouteParse: output exceeds 10 MiB: %d bytes", len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism — same proc content (mutex still held) must give same output. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtxFuzz(ctx2, t, "ip route show") + cancel2() + if t.Context().Err() != nil { + return + } + if code2 == -1 { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("FuzzIPRouteParse: determinism violation: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + code, len(stdout), code2, len(stdout2)) + } }) } @@ -216,7 +239,7 @@ func FuzzIPRouteGetAddr(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() - _, _, code := cmdRunCtxFuzz(ctx, t, "ip route get "+addr) + stdout, _, code := cmdRunCtxFuzz(ctx, t, "ip route get "+addr) timedOut := ctx.Err() == context.DeadlineExceeded cancel() if timedOut { @@ -226,8 +249,31 @@ func FuzzIPRouteGetAddr(f *testing.F) { if code == -1 { return // internal shell error before the builtin ran — not our bug } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("FuzzIPRouteGetAddr %q: unexpected exit code %d", addr, code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("FuzzIPRouteGetAddr %q: output exceeds 10 MiB: %d bytes", addr, len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism — same proc content (mutex still held) must give same output. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtxFuzz(ctx2, t, "ip route get "+addr) + cancel2() + if t.Context().Err() != nil { + return + } + if code2 == -1 { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("FuzzIPRouteGetAddr %q: determinism violation: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + addr, code, len(stdout), code2, len(stdout2)) + } }) } diff --git a/builtins/tests/ls/ls_fuzz_test.go b/builtins/tests/ls/ls_fuzz_test.go index acc5c16b..e8b24ca8 100644 --- a/builtins/tests/ls/ls_fuzz_test.go +++ b/builtins/tests/ls/ls_fuzz_test.go @@ -99,14 +99,34 @@ func FuzzLsFlags(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel - _, _, code := cmdRunCtx(ctx, t, "ls"+flags, dir) + stdout, _, code := cmdRunCtx(ctx, t, "ls"+flags, dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("ls%s unexpected exit code %d", flags, code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("ls%s output exceeds 10 MiB: %d bytes", flags, len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtx(ctx2, t, "ls"+flags, dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on ls%s: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + flags, code, len(stdout), code2, len(stdout2)) + } }) } @@ -153,14 +173,34 @@ func FuzzLsRecursive(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel - _, _, code := cmdRunCtx(ctx, t, "ls -R", dir) + stdout, _, code := cmdRunCtx(ctx, t, "ls -R", dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("ls -R unexpected exit code %d", code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("ls -R output exceeds 10 MiB: %d bytes", len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtx(ctx2, t, "ls -R", dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on ls -R: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + code, len(stdout), code2, len(stdout2)) + } }) } @@ -218,14 +258,34 @@ func FuzzLsHumanReadable(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel - _, _, code := cmdRunCtx(ctx, t, "ls -lh testfile.bin", dir) + stdout, _, code := cmdRunCtx(ctx, t, "ls -lh testfile.bin", dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("ls -lh unexpected exit code %d", code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("ls -lh output exceeds 10 MiB: %d bytes", len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtx(ctx2, t, "ls -lh testfile.bin", dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on ls -lh: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + code, len(stdout), code2, len(stdout2)) + } }) } @@ -282,13 +342,33 @@ func FuzzLsMultipleFiles(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel - _, _, code := cmdRunCtx(ctx, t, "ls"+flags, dir) + stdout, _, code := cmdRunCtx(ctx, t, "ls"+flags, dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("ls%s unexpected exit code %d", flags, code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("ls%s output exceeds 10 MiB: %d bytes", flags, len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtx(ctx2, t, "ls"+flags, dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on ls%s multiple files: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + flags, code, len(stdout), code2, len(stdout2)) + } }) } diff --git a/builtins/tests/ps/ps_fuzz_test.go b/builtins/tests/ps/ps_fuzz_test.go index 94b81b00..a05c4333 100644 --- a/builtins/tests/ps/ps_fuzz_test.go +++ b/builtins/tests/ps/ps_fuzz_test.go @@ -102,6 +102,16 @@ func FuzzPSPidList(f *testing.F) { t.Errorf("unexpected runner error: %v", runErr) } } + + // Invariant 1: output bounded. + if outBuf.Len() > 10*1024*1024 { + t.Errorf("ps output exceeds 10 MiB: %d bytes", outBuf.Len()) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Note: Invariant 2 (determinism) is intentionally skipped for ps because + // it reads live kernel process state, which changes between calls. }) } @@ -152,5 +162,15 @@ func FuzzPSFlags(f *testing.F) { t.Errorf("unexpected runner error: %v", runErr) } } + + // Invariant 1: output bounded. + if outBuf.Len() > 10*1024*1024 { + t.Errorf("ps flags output exceeds 10 MiB: %d bytes", outBuf.Len()) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Note: Invariant 2 (determinism) is intentionally skipped for ps because + // it reads live kernel process state, which changes between calls. }) } diff --git a/builtins/tests/ss/ss_fuzz_test.go b/builtins/tests/ss/ss_fuzz_test.go index a75d1852..f4f649cc 100644 --- a/builtins/tests/ss/ss_fuzz_test.go +++ b/builtins/tests/ss/ss_fuzz_test.go @@ -143,13 +143,23 @@ func FuzzSSFlags(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel - _, _, code := cmdRunCtxFuzzSS(ctx, t, script) + stdout, _, code := cmdRunCtxFuzzSS(ctx, t, script) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("unexpected exit code %d for flags %q", code, flags) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("ss output exceeds 10 MiB: %d bytes", len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Note: Invariant 2 (determinism) is intentionally skipped for ss because + // it reads live kernel socket state, which changes between calls. }) } diff --git a/builtins/tests/strings_cmd/strings_fuzz_test.go b/builtins/tests/strings_cmd/strings_fuzz_test.go index 513f18f0..ecbcf95a 100644 --- a/builtins/tests/strings_cmd/strings_fuzz_test.go +++ b/builtins/tests/strings_cmd/strings_fuzz_test.go @@ -97,14 +97,34 @@ func FuzzStrings(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel - _, _, code := cmdRunCtx(ctx, t, "strings input.bin", dir) + stdout, _, code := cmdRunCtx(ctx, t, "strings input.bin", dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("strings unexpected exit code %d", code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("strings output exceeds 10 MiB: %d bytes", len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtx(ctx2, t, "strings input.bin", dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on strings: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + code, len(stdout), code2, len(stdout2)) + } }) } @@ -155,14 +175,34 @@ func FuzzStringsMinLen(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel - _, _, code := cmdRunCtx(ctx, t, fmt.Sprintf("strings -n %d input.bin", minLen), dir) + stdout, _, code := cmdRunCtx(ctx, t, fmt.Sprintf("strings -n %d input.bin", minLen), dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("strings -n %d unexpected exit code %d", minLen, code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("strings -n %d output exceeds 10 MiB: %d bytes", minLen, len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtx(ctx2, t, fmt.Sprintf("strings -n %d input.bin", minLen), dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on strings -n %d: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + minLen, code, len(stdout), code2, len(stdout2)) + } }) } @@ -211,14 +251,34 @@ func FuzzStringsRadix(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel - _, _, code := cmdRunCtx(ctx, t, fmt.Sprintf("strings -t %s input.bin", radix), dir) + stdout, _, code := cmdRunCtx(ctx, t, fmt.Sprintf("strings -t %s input.bin", radix), dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("strings -t %s unexpected exit code %d", radix, code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("strings -t %s output exceeds 10 MiB: %d bytes", radix, len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtx(ctx2, t, fmt.Sprintf("strings -t %s input.bin", radix), dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on strings -t %s: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + radix, code, len(stdout), code2, len(stdout2)) + } }) } @@ -255,13 +315,33 @@ func FuzzStringsStdin(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel - _, _, code := cmdRunCtx(ctx, t, "strings < stdin.bin", dir) + stdout, _, code := cmdRunCtx(ctx, t, "strings < stdin.bin", dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("strings stdin unexpected exit code %d", code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("strings stdin output exceeds 10 MiB: %d bytes", len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtx(ctx2, t, "strings < stdin.bin", dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on strings stdin: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + code, len(stdout), code2, len(stdout2)) + } }) } diff --git a/builtins/tests/tail/tail_fuzz_test.go b/builtins/tests/tail/tail_fuzz_test.go index dea12fa4..0cf5e233 100644 --- a/builtins/tests/tail/tail_fuzz_test.go +++ b/builtins/tests/tail/tail_fuzz_test.go @@ -76,9 +76,14 @@ func FuzzTailLines(f *testing.F) { if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("tail -n %d unexpected exit code %d", n, code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("tail -n %d output exceeds 10 MiB: %d bytes", n, len(stdout)) + } // If successful, output line count must be <= n if code == 0 && n >= 0 { @@ -87,6 +92,21 @@ func FuzzTailLines(f *testing.F) { t.Errorf("tail -n %d produced %d newlines in output", n, lineCount) } } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtx(ctx2, t, fmt.Sprintf("tail -n %d input.txt", n), dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on tail -n %d: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + n, code, len(stdout), code2, len(stdout2)) + } }) } @@ -139,9 +159,14 @@ func FuzzTailBytes(f *testing.F) { if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("tail -c %d unexpected exit code %d", n, code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("tail -c %d output exceeds 10 MiB: %d bytes", n, len(stdout)) + } // If successful, output byte count must be <= n if code == 0 { @@ -150,6 +175,21 @@ func FuzzTailBytes(f *testing.F) { t.Errorf("tail -c %d produced %d bytes of output", n, outLen) } } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtx(ctx2, t, fmt.Sprintf("tail -c %d input.txt", n), dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on tail -c %d: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + n, code, len(stdout), code2, len(stdout2)) + } }) } @@ -187,14 +227,34 @@ func FuzzTailStdin(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel - _, _, code := cmdRunCtx(ctx, t, fmt.Sprintf("tail -n %d < stdin.txt", n), dir) + stdout, _, code := cmdRunCtx(ctx, t, fmt.Sprintf("tail -n %d < stdin.txt", n), dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("tail stdin unexpected exit code %d", code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("tail stdin output exceeds 10 MiB: %d bytes", len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtx(ctx2, t, fmt.Sprintf("tail -n %d < stdin.txt", n), dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on tail stdin -n %d: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + n, code, len(stdout), code2, len(stdout2)) + } }) } @@ -240,14 +300,34 @@ func FuzzTailLinesOffset(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel - _, _, code := cmdRunCtx(ctx, t, fmt.Sprintf("tail -n +%d input.txt", n), dir) + stdout, _, code := cmdRunCtx(ctx, t, fmt.Sprintf("tail -n +%d input.txt", n), dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("tail -n +%d unexpected exit code %d", n, code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("tail -n +%d output exceeds 10 MiB: %d bytes", n, len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtx(ctx2, t, fmt.Sprintf("tail -n +%d input.txt", n), dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on tail -n +%d: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + n, code, len(stdout), code2, len(stdout2)) + } }) } @@ -290,13 +370,33 @@ func FuzzTailBytesOffset(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel - _, _, code := cmdRunCtx(ctx, t, fmt.Sprintf("tail -c +%d input.txt", n), dir) + stdout, _, code := cmdRunCtx(ctx, t, fmt.Sprintf("tail -c +%d input.txt", n), dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("tail -c +%d unexpected exit code %d", n, code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("tail -c +%d output exceeds 10 MiB: %d bytes", n, len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtx(ctx2, t, fmt.Sprintf("tail -c +%d input.txt", n), dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on tail -c +%d: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + n, code, len(stdout), code2, len(stdout2)) + } }) } diff --git a/builtins/tests/testcmd/testcmd_fuzz_test.go b/builtins/tests/testcmd/testcmd_fuzz_test.go index 2c306239..5b0b5239 100644 --- a/builtins/tests/testcmd/testcmd_fuzz_test.go +++ b/builtins/tests/testcmd/testcmd_fuzz_test.go @@ -90,14 +90,34 @@ func FuzzTestStringOps(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel script := fmt.Sprintf("test '%s' %s '%s'", left, op, right) - _, _, code := cmdRunCtx(ctx, t, script, baseDir) + stdout, _, code := cmdRunCtx(ctx, t, script, baseDir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 && code != 2 { t.Errorf("test string op unexpected exit code %d", code) } + // Invariant 1: output bounded (test produces no stdout, but cap applies). + if len(stdout) > 10*1024*1024 { + t.Errorf("test string op output exceeds 10 MiB: %d bytes", len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtx(ctx2, t, script, baseDir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on test string op: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + code, len(stdout), code2, len(stdout2)) + } }) } @@ -143,14 +163,34 @@ func FuzzTestIntegerOps(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel script := fmt.Sprintf("test %d %s %d", left, op, right) - _, _, code := cmdRunCtx(ctx, t, script, baseDir) + stdout, _, code := cmdRunCtx(ctx, t, script, baseDir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 && code != 2 { t.Errorf("test %d %s %d unexpected exit code %d", left, op, right, code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("test integer op output exceeds 10 MiB: %d bytes", len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtx(ctx2, t, script, baseDir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on test integer op: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + code, len(stdout), code2, len(stdout2)) + } }) } @@ -200,14 +240,34 @@ func FuzzTestFileOps(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel script := fmt.Sprintf("test %s %s", op, target) - _, _, code := cmdRunCtx(ctx, t, script, dir) + stdout, _, code := cmdRunCtx(ctx, t, script, dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 && code != 2 { t.Errorf("test %s unexpected exit code %d", op, code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("test file op output exceeds 10 MiB: %d bytes", len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. File state (created or not) is stable within iteration. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtx(ctx2, t, script, dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on test file op: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + code, len(stdout), code2, len(stdout2)) + } }) } @@ -259,14 +319,34 @@ func FuzzTestStringUnary(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel script := fmt.Sprintf("test %s '%s'", op, arg) - _, _, code := cmdRunCtx(ctx, t, script, baseDir) + stdout, _, code := cmdRunCtx(ctx, t, script, baseDir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 && code != 2 { t.Errorf("test %s unexpected exit code %d", op, code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("test unary op output exceeds 10 MiB: %d bytes", len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtx(ctx2, t, script, baseDir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on test unary op: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + code, len(stdout), code2, len(stdout2)) + } }) } @@ -351,13 +431,33 @@ func FuzzTestNesting(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel script := fmt.Sprintf("test %s", expr) - _, _, code := cmdRunCtx(ctx, t, script, baseDir) + stdout, _, code := cmdRunCtx(ctx, t, script, baseDir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 && code != 2 { t.Errorf("test %q unexpected exit code %d", expr, code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("test nesting output exceeds 10 MiB: %d bytes", len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtx(ctx2, t, script, baseDir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on test nesting: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + code, len(stdout), code2, len(stdout2)) + } }) } diff --git a/builtins/tests/uniq/uniq_fuzz_test.go b/builtins/tests/uniq/uniq_fuzz_test.go index 701f27c5..8ae2172a 100644 --- a/builtins/tests/uniq/uniq_fuzz_test.go +++ b/builtins/tests/uniq/uniq_fuzz_test.go @@ -80,14 +80,34 @@ func FuzzUniq(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel - _, _, code := fuzzRunCtx(ctx, t, "uniq input.txt", dir) + stdout, _, code := fuzzRunCtx(ctx, t, "uniq input.txt", dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("uniq unexpected exit code %d", code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("uniq output exceeds 10 MiB: %d bytes", len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := fuzzRunCtx(ctx2, t, "uniq input.txt", dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on uniq: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + code, len(stdout), code2, len(stdout2)) + } }) } @@ -125,14 +145,34 @@ func FuzzUniqCount(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel - _, _, code := fuzzRunCtx(ctx, t, "uniq -c input.txt", dir) + stdout, _, code := fuzzRunCtx(ctx, t, "uniq -c input.txt", dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("uniq -c unexpected exit code %d", code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("uniq -c output exceeds 10 MiB: %d bytes", len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := fuzzRunCtx(ctx2, t, "uniq -c input.txt", dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on uniq -c: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + code, len(stdout), code2, len(stdout2)) + } }) } @@ -211,14 +251,34 @@ func FuzzUniqFlags(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel - _, _, code := fuzzRunCtx(ctx, t, "uniq"+flags+" input.txt", dir) + stdout, _, code := fuzzRunCtx(ctx, t, "uniq"+flags+" input.txt", dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("uniq%s unexpected exit code %d", flags, code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("uniq%s output exceeds 10 MiB: %d bytes", flags, len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := fuzzRunCtx(ctx2, t, "uniq"+flags+" input.txt", dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on uniq%s: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + flags, code, len(stdout), code2, len(stdout2)) + } }) } @@ -250,13 +310,33 @@ func FuzzUniqStdin(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel - _, _, code := fuzzRunCtx(ctx, t, "uniq < stdin.txt", dir) + stdout, _, code := fuzzRunCtx(ctx, t, "uniq < stdin.txt", dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("uniq stdin unexpected exit code %d", code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("uniq stdin output exceeds 10 MiB: %d bytes", len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := fuzzRunCtx(ctx2, t, "uniq < stdin.txt", dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on uniq stdin: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + code, len(stdout), code2, len(stdout2)) + } }) } diff --git a/builtins/tests/wc/wc_fuzz_test.go b/builtins/tests/wc/wc_fuzz_test.go index ec50880a..2ca8a6cf 100644 --- a/builtins/tests/wc/wc_fuzz_test.go +++ b/builtins/tests/wc/wc_fuzz_test.go @@ -75,14 +75,34 @@ func FuzzWc(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel - _, _, code := cmdRunCtx(ctx, t, "wc input.txt", dir) + stdout, _, code := cmdRunCtx(ctx, t, "wc input.txt", dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("wc unexpected exit code %d", code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("wc output exceeds 10 MiB: %d bytes", len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtx(ctx2, t, "wc input.txt", dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on wc: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + code, len(stdout), code2, len(stdout2)) + } }) } @@ -119,14 +139,34 @@ func FuzzWcLines(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel - _, _, code := cmdRunCtx(ctx, t, "wc -l input.txt", dir) + stdout, _, code := cmdRunCtx(ctx, t, "wc -l input.txt", dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("wc -l unexpected exit code %d", code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("wc -l output exceeds 10 MiB: %d bytes", len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtx(ctx2, t, "wc -l input.txt", dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on wc -l: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + code, len(stdout), code2, len(stdout2)) + } }) } @@ -161,14 +201,34 @@ func FuzzWcBytes(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel - _, _, code := cmdRunCtx(ctx, t, "wc -c input.txt", dir) + stdout, _, code := cmdRunCtx(ctx, t, "wc -c input.txt", dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("wc -c unexpected exit code %d", code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("wc -c output exceeds 10 MiB: %d bytes", len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtx(ctx2, t, "wc -c input.txt", dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on wc -c: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + code, len(stdout), code2, len(stdout2)) + } }) } @@ -210,14 +270,34 @@ func FuzzWcChars(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel - _, _, code := cmdRunCtx(ctx, t, "wc -m input.txt", dir) + stdout, _, code := cmdRunCtx(ctx, t, "wc -m input.txt", dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("wc -m unexpected exit code %d", code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("wc -m output exceeds 10 MiB: %d bytes", len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtx(ctx2, t, "wc -m input.txt", dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on wc -m: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + code, len(stdout), code2, len(stdout2)) + } }) } @@ -257,13 +337,33 @@ func FuzzWcStdin(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() // safety net if t.Fatal fires before explicit cancel - _, _, code := cmdRunCtx(ctx, t, "wc < stdin.txt", dir) + stdout, _, code := cmdRunCtx(ctx, t, "wc < stdin.txt", dir) cancel() if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("wc stdin unexpected exit code %d", code) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("wc stdin output exceeds 10 MiB: %d bytes", len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdRunCtx(ctx2, t, "wc < stdin.txt", dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on wc stdin: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + code, len(stdout), code2, len(stdout2)) + } }) } diff --git a/interp/tests/cmdsubst_fuzz_test.go b/interp/tests/cmdsubst_fuzz_test.go index 264490d4..a79cd372 100644 --- a/interp/tests/cmdsubst_fuzz_test.go +++ b/interp/tests/cmdsubst_fuzz_test.go @@ -49,13 +49,34 @@ func FuzzCmdSubstEcho(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() - _, _, code := cmdSubstRunCtx(ctx, t, `x=$(echo '`+arg+`'); echo "$x"`, dir) + script := `x=$(echo '` + arg + `'); echo "$x"` + stdout, _, code := cmdSubstRunCtx(ctx, t, script, dir) if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("unexpected exit code %d for arg %q", code, arg) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("cmdsubst echo output exceeds 10 MiB: %d bytes", len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdSubstRunCtx(ctx2, t, script, dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on cmdsubst echo %q: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + arg, code, len(stdout), code2, len(stdout2)) + } }) } @@ -95,13 +116,34 @@ func FuzzCmdSubstNested(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() - _, _, code := cmdSubstRunCtx(ctx, t, `echo $(echo '`+arg+`')`, dir) + script := `echo $(echo '` + arg + `')` + stdout, _, code := cmdSubstRunCtx(ctx, t, script, dir) if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("unexpected exit code %d for arg %q", code, arg) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("cmdsubst nested output exceeds 10 MiB: %d bytes", len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := cmdSubstRunCtx(ctx2, t, script, dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on nested cmdsubst %q: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + arg, code, len(stdout), code2, len(stdout2)) + } }) } @@ -135,12 +177,33 @@ func FuzzSubshellCommands(f *testing.F) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() - _, _, code := subshellRunCtx(ctx, t, `(echo '`+arg+`')`, dir) + script := `(echo '` + arg + `')` + stdout, _, code := subshellRunCtx(ctx, t, script, dir) if t.Context().Err() != nil { return } + // Invariant 3: exit code validity. if code != 0 && code != 1 { t.Errorf("unexpected exit code %d for arg %q", code, arg) } + // Invariant 1: output bounded. + if len(stdout) > 10*1024*1024 { + t.Errorf("subshell output exceeds 10 MiB: %d bytes", len(stdout)) + } + + // Invariant 4: no panic — reaching this line proves no panic escaped Run(). + + // Invariant 2: determinism. + ctx2, cancel2 := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel2() + stdout2, _, code2 := subshellRunCtx(ctx2, t, script, dir) + cancel2() + if t.Context().Err() != nil { + return + } + if stdout != stdout2 || code != code2 { + t.Errorf("determinism violation on subshell %q: outputs differ on identical input\nrun1: exit=%d, len=%d\nrun2: exit=%d, len=%d", + arg, code, len(stdout), code2, len(stdout2)) + } }) }