Fix shell crash when piping assignments into commands (#22336)

## Summary
- Fixes crash when running shell commands with variable assignments
piped to other commands
- Resolves #15714

## Problem
The shell was crashing with "Invalid tag" error when running commands
like:
```bash
bun exec "FOO=bar BAR=baz | echo hi"
```

## Root Cause
In `Pipeline.zig`, the `cmds` array was allocated with the wrong size:
- It used `node.items.len` (which includes assignments)
- But only filled entries for actual commands (assignments are skipped
in pipelines)
- This left uninitialized memory that caused crashes when accessed

## Solution
Changed the allocation to use the correct `cmd_count` instead of
`node.items.len`:
```zig
// Before
this.cmds = if (cmd_count >= 1) bun.handleOom(this.base.allocator().alloc(CmdOrResult, this.node.items.len)) else null;

// After  
this.cmds = if (cmd_count >= 1) bun.handleOom(this.base.allocator().alloc(CmdOrResult, cmd_count)) else null;
```

## Test plan
 Added comprehensive regression test in
`test/regression/issue/15714.test.ts` that:
- Tests the exact case from the issue
- Tests multiple assignments
- Tests single assignment
- Tests assignments in middle of pipeline
- Verified test fails on main branch (exit code 133 = SIGTRAP)
- Verified test passes with fix

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Zack Radisic <56137411+zackradisic@users.noreply.github.com>
This commit is contained in:
robobun
2025-09-02 22:53:03 -07:00
committed by GitHub
parent cff2c2690b
commit e2bfeefc9d
2 changed files with 273 additions and 1 deletions

View File

@@ -95,7 +95,7 @@ fn setupCommands(this: *Pipeline) ?Yield {
break :brk i;
};
this.cmds = if (cmd_count >= 1) bun.handleOom(this.base.allocator().alloc(CmdOrResult, this.node.items.len)) else null;
this.cmds = if (cmd_count >= 1) bun.handleOom(this.base.allocator().alloc(CmdOrResult, cmd_count)) else null;
if (this.cmds == null) return null;
var pipes = bun.handleOom(this.base.allocator().alloc(Pipe, if (cmd_count > 1) cmd_count - 1 else 1));

View File

@@ -0,0 +1,272 @@
import { describe } from "bun:test";
import { createTestBuilder } from "./util";
const TestBuilder = createTestBuilder(import.meta.path);
describe("shell: piping assignments into command", () => {
// Original test cases
TestBuilder.command`FOO=bar BAR=baz | echo hi`
.stdout("hi\n")
.stderr("")
.exitCode(0)
.runAsTest("should not crash with multiple assignments (issue #15714)");
TestBuilder.command`A=1 B=2 C=3 | echo test`
.stdout("test\n")
.stderr("")
.exitCode(0)
.runAsTest("should handle multiple assignments");
TestBuilder.command`FOO=bar | echo single`
.stdout("single\n")
.stderr("")
.exitCode(0)
.runAsTest("should handle single assignment");
TestBuilder.command`echo start | FOO=bar BAR=baz | echo end`
.stdout("end\n")
.stderr("")
.exitCode(0)
.runAsTest("should handle assignments in middle of pipeline");
// New comprehensive test cases
// Many assignments in a single pipeline
TestBuilder.command`A=1 B=2 C=3 D=4 E=5 F=6 G=7 H=8 I=9 J=10 | echo many`
.stdout("many\n")
.stderr("")
.exitCode(0)
.runAsTest("should handle many assignments (10+) in pipeline");
// Empty assignment values
TestBuilder.command`EMPTY= ALSO_EMPTY= | echo empty`
.stdout("empty\n")
.stderr("")
.exitCode(0)
.runAsTest("should handle empty assignment values");
// Assignments with spaces in values (quoted)
TestBuilder.command`FOO="bar baz" HELLO="world test" | echo quoted`
.stdout("quoted\n")
.stderr("")
.exitCode(0)
.runAsTest("should handle assignments with quoted values containing spaces");
// Assignments with special characters
TestBuilder.command`VAR='$HOME' OTHER='$(echo test)' | echo special`
.stdout("special\n")
.stderr("")
.exitCode(0)
.runAsTest("should handle assignments with special characters in single quotes");
// Complex pipeline with assignments at different positions
TestBuilder.command`A=1 | B=2 C=3 | echo first | D=4 | echo second | E=5 F=6 | echo third`
.stdout("third\n")
.stderr("")
.exitCode(0)
.runAsTest("should handle assignments scattered throughout complex pipeline");
// Assignments only (no actual commands except assignments)
TestBuilder.command`FOO=bar BAR=baz | QUX=quux | true`
.stdout("")
.stderr("")
.exitCode(0)
.runAsTest("should handle pipeline with only assignments followed by true");
// Long assignment values
const longValue = "x".repeat(1000);
TestBuilder.command`LONG="${longValue}" | echo long`
.stdout("long\n")
.stderr("")
.exitCode(0)
.runAsTest("should handle very long assignment values");
// Assignments with equals signs in values
TestBuilder.command`EQUATION="a=b+c" FORMULA="x=y*z" | echo math`
.stdout("math\n")
.stderr("")
.exitCode(0)
.runAsTest("should handle equals signs in assignment values");
// Unicode in assignments
TestBuilder.command`EMOJI="🚀" CHINESE="你好" | echo unicode`
.stdout("unicode\n")
.stderr("")
.exitCode(0)
.runAsTest("should handle unicode characters in assignments");
// Assignments with expansions
TestBuilder.command`HOME_BACKUP=$HOME USER_BACKUP=$USER | echo expand`
.stdout("expand\n")
.stderr("")
.exitCode(0)
.runAsTest("should handle variable expansions in assignments");
// Multiple pipelines with assignments chained with && and ||
TestBuilder.command`A=1 | echo first && B=2 | echo second`
.stdout("first\nsecond\n")
.stderr("")
.exitCode(0)
.runAsTest("should handle assignments in chained pipelines with &&");
TestBuilder.command`false || X=fail | echo fallback`
.stdout("fallback\n")
.stderr("")
.exitCode(0)
.runAsTest("should handle assignments in fallback pipeline with ||");
// Nested command substitution with assignments
TestBuilder.command`VAR=$(echo FOO=bar | cat) | echo nested`
.stdout("nested\n")
.stderr("")
.exitCode(0)
.runAsTest("should handle nested command substitution with assignments");
// Assignments with glob patterns (shouldn't expand in assignments)
TestBuilder.command`PATTERN="*.txt" GLOB="[a-z]*" | echo glob`
.stdout("glob\n")
.stderr("")
.exitCode(0)
.runAsTest("should handle glob patterns in assignments without expansion");
// Assignments with backslashes and escape sequences
TestBuilder.command`PATH_WIN="C:\\Users\\test" NEWLINE="line1\nline2" | echo escape`
.stdout("escape\n")
.stderr("")
.exitCode(0)
.runAsTest("should handle backslashes and escape sequences in assignments");
// Pipeline where assignments appear after regular commands
TestBuilder.command`echo before | A=1 B=2 | echo after`
.stdout("after\n")
.stderr("")
.exitCode(0)
.runAsTest("should handle assignments after regular commands in pipeline");
// Stress test: very long pipeline with alternating assignments and commands
TestBuilder.command`A=1 | echo a | B=2 | echo b | C=3 | echo c | D=4 | echo d | E=5 | echo e`
.stdout("e\n")
.stderr("")
.exitCode(0)
.runAsTest("should handle long pipeline with alternating assignments and commands");
// Assignment with command substitution that itself contains assignments
TestBuilder.command`RESULT=$(X=1 Y=2 echo done) | echo subshell`
.stdout("subshell\n")
.stderr("")
.exitCode(0)
.runAsTest("should handle command substitution containing assignments");
// Multiple assignment statements separated by semicolons in pipeline
TestBuilder.command`A=1; B=2; C=3 | echo semicolon`
.stdout("semicolon\n")
.stderr("")
.exitCode(0)
.runAsTest("should handle semicolon-separated assignments before pipeline");
// Assignments with numeric names (edge case)
TestBuilder.command`_1=first _2=second _3=third | echo numeric`
.stdout("numeric\n")
.stderr("")
.exitCode(0)
.runAsTest("should handle assignments with underscore-prefixed numeric names");
// Pipeline with assignments and input/output redirection
TestBuilder.command`echo "test" | A=1 B=2 | cat`
.stdout("test\n")
.stderr("")
.exitCode(0)
.runAsTest("should pass through stdin when assignments are in pipeline");
// Assignments with underscores and numbers
TestBuilder.command`ARR_0=a ARR_1=b | echo array`
.stdout("array\n")
.stderr("")
.exitCode(0)
.runAsTest("should handle assignments with underscores and numbers");
// Pipeline where every item is an assignment (stress test)
TestBuilder.command`A=1 | B=2 | C=3 | D=4 | E=5 | true`
.stdout("")
.stderr("")
.exitCode(0)
.runAsTest("should handle pipeline with multiple assignments ending with true");
// Assignments with quotes and spaces in various combinations
TestBuilder.command`A="hello world" B='single quotes' C=no_quotes | echo mixed`
.stdout("mixed\n")
.stderr("")
.exitCode(0)
.runAsTest("should handle mixed quoting styles in assignments");
// Pipeline with assignments and background processes (if supported)
TestBuilder.command`A=1 | echo fg | B=2`
.stdout("fg\n")
.stderr("")
.exitCode(0)
.runAsTest("should handle assignments with foreground processes");
// Assignments that look like commands
TestBuilder.command`echo=notecho ls=notls | echo real`
.stdout("real\n")
.stderr("")
.exitCode(0)
.runAsTest("should handle assignments that shadow command names");
// Complex nested pipeline with subshells and assignments
TestBuilder.command`(A=1 | echo inner) | B=2 | echo outer`
.stdout("outer\n")
.stderr("")
.exitCode(0)
.runAsTest("should handle assignments in subshells within pipelines");
// Assignment with line continuation (if supported)
TestBuilder.command`MULTI="line1 \
line2" | echo multiline`
.stdout("multiline\n")
.stderr("")
.exitCode(0)
.runAsTest("should handle multi-line assignment values");
// Edge case: assignment-like patterns that aren't assignments
TestBuilder.command`echo A=1 | B=2 | cat`
.stdout("A=1\n")
.stderr("")
.exitCode(0)
.runAsTest("should distinguish between assignment and assignment-like echo output");
// Verify assignments don't affect the shell environment
TestBuilder.command`TEST_VAR=should_not_persist | echo $TEST_VAR`
.stdout("\n")
.stderr("")
.exitCode(0)
.runAsTest("should not persist assignment variables in pipeline to shell environment");
// Assignments with percent signs and other special chars
TestBuilder.command`PERCENT="100%" DOLLAR="$100" | echo special_chars`
.stdout("special_chars\n")
.stderr("")
.exitCode(0)
.runAsTest("should handle percent signs and dollar signs in assignments");
// Pipeline with error in command but assignments present
TestBuilder.command`A=1 B=2 | false | C=3 | echo continue`
.stdout("continue\n")
.stderr("")
.exitCode(0)
.runAsTest("should continue pipeline even when command fails with assignments present");
// Extreme case: single character variable names
TestBuilder.command`A=a B=b C=c D=d E=e F=f G=g H=h I=i J=j | echo singles`
.stdout("singles\n")
.stderr("")
.exitCode(0)
.runAsTest("should handle single character variable names");
// Assignment with tabs and other whitespace
TestBuilder.command`TAB=" " SPACE=" " | echo whitespace`
.stdout("whitespace\n")
.stderr("")
.exitCode(0)
.runAsTest("should handle tabs and spaces in assignment values");
});