Compare commits

...

3 Commits

Author SHA1 Message Date
Jarred Sumner
0398e73812 Merge branch 'main' into claude/fix-shell-brace-expansion-overflow 2026-02-13 14:34:56 -08:00
Dylan Conway
bd3600ee76 fix(shell): also fix u16 overflow in brace expansion output counter
The out_key_counter in expandFlat/expandNested was u16, overflowing
when cartesian product of brace groups exceeds 65535 (e.g. {a,b}
repeated 16 times = 2^16 = 65536). Changed to u32.

Added test for the cartesian product overflow case.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 03:12:56 +00:00
Dylan Conway
11ce814c23 fix(shell): prevent integer overflow in brace expansion variant counting
The `calculateExpandedAmount` function used a `u8` per nesting level to
count comma-separated items in brace groups. With 256+ items (e.g.
`{x0,x1,...,x255}`), the counter overflowed: panic on debug builds,
segfault on release builds.

Fix: use `u32` per nesting level, and use checked multiplication for
the cross-group variant count to prevent u32 overflow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 03:00:31 +00:00
2 changed files with 108 additions and 7 deletions

View File

@@ -59,7 +59,7 @@ pub fn expand(
out: []std.array_list.Managed(u8),
contains_nested: bool,
) ExpandError!void {
var out_key_counter: u16 = 1;
var out_key_counter: u32 = 1;
if (!contains_nested) {
var expansions_table = try buildExpansionTableAlloc(allocator, tokens);
@@ -74,8 +74,8 @@ pub fn expand(
fn expandNested(
root: *AST.Group,
out: []std.array_list.Managed(u8),
out_key: u16,
out_key_counter: *u16,
out_key: u32,
out_key_counter: *u32,
start: u32,
) ExpandError!void {
if (root.atoms == .single) {
@@ -157,8 +157,8 @@ fn expandFlat(
tokens: []const Token,
expansion_table: []const ExpansionVariant,
out: []std.array_list.Managed(u8),
out_key: u16,
out_key_counter: *u16,
out_key: u32,
out_key_counter: *u32,
depth_: u8,
start: usize,
end: usize,
@@ -380,7 +380,9 @@ pub const Parser = struct {
};
pub fn calculateExpandedAmount(tokens: []const Token) u32 {
var nested_brace_stack = bun.SmallList(u8, MAX_NESTED_BRACES){};
// Use u32 per nesting level to avoid overflow when a single brace group
// has more than 255 comma-separated items.
var nested_brace_stack = bun.SmallList(u32, MAX_NESTED_BRACES){};
defer nested_brace_stack.deinit(bun.default_allocator);
var variant_count: u32 = 0;
var prev_comma: bool = false;
@@ -405,7 +407,7 @@ pub fn calculateExpandedAmount(tokens: []const Token) u32 {
} else if (variant_count == 0) {
variant_count = variants;
} else {
variant_count *= variants;
variant_count = std.math.mul(u32, variant_count, variants) catch std.math.maxInt(u32);
}
},
else => {},

View File

@@ -0,0 +1,99 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, isPosix } from "harness";
// Regression tests for brace expansion integer overflows:
// 1. u8 variant counter overflowed at 256 items in a single brace group
// 2. u16 out_key_counter overflowed at 65536 total cartesian product expansions
describe.if(isPosix)("brace expansion should handle large item counts", () => {
test.concurrent("256 items in a brace group does not crash", async () => {
// Generate {x0,x1,x2,...,x255} - 256 items overflows u8
const items = Array.from({ length: 256 }, (_, i) => `x${i}`).join(",");
const cmd = `echo {${items}}`;
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
import { $ } from "bun";
$.throws(false);
const r = await $\`${cmd}\`;
const words = r.stdout.toString().trim().split(" ");
console.log("count:" + words.length);
console.log("first:" + words[0]);
console.log("last:" + words[words.length - 1]);
console.log("exitCode:" + r.exitCode);
`,
],
env: bunEnv,
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("count:256");
expect(stdout).toContain("first:x0");
expect(stdout).toContain("last:x255");
expect(stdout).toContain("exitCode:0");
expect(exitCode).toBe(0);
});
test.concurrent("500 items in a brace group does not crash", async () => {
const items = Array.from({ length: 500 }, (_, i) => `y${i}`).join(",");
const cmd = `echo {${items}}`;
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
import { $ } from "bun";
$.throws(false);
const r = await $\`${cmd}\`;
const words = r.stdout.toString().trim().split(" ");
console.log("count:" + words.length);
console.log("exitCode:" + r.exitCode);
`,
],
env: bunEnv,
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("count:500");
expect(stdout).toContain("exitCode:0");
expect(exitCode).toBe(0);
});
test("cartesian product exceeding 65535 does not crash", async () => {
// {a,b} repeated 16 times = 2^16 = 65536 expansions, which overflows u16
// Use ${{raw:...}} to pass brace expression without shell interpolation
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
import { $ } from "bun";
$.throws(false);
const r = await $\`echo $\{{ raw: "{a,b}".repeat(16) }}\`;
const words = r.stdout.toString().trim().split(" ");
console.log("count:" + words.length);
console.log("exitCode:" + r.exitCode);
`,
],
env: bunEnv,
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("count:65536");
expect(stdout).toContain("exitCode:0");
expect(exitCode).toBe(0);
}, 30_000);
});