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>
This commit is contained in:
Dylan Conway
2026-02-13 03:12:56 +00:00
parent 11ce814c23
commit bd3600ee76
2 changed files with 38 additions and 10 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,

View File

@@ -1,12 +1,12 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, isPosix } from "harness";
// Regression test: brace expansion with 256+ items used a u8 counter for
// variant counting, which overflowed and caused a segfault on release builds
// or integer overflow panic on debug builds.
// 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("256 items in a brace group does not crash", async () => {
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}}`;
@@ -40,7 +40,7 @@ describe.if(isPosix)("brace expansion should handle large item counts", () => {
expect(exitCode).toBe(0);
});
test("500 items in a brace group does not crash", async () => {
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}}`;
@@ -68,4 +68,32 @@ describe.if(isPosix)("brace expansion should handle large item counts", () => {
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);
});