From bd3600ee76d7e024579f9ef061db35f676ccc36f Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Fri, 13 Feb 2026 03:12:56 +0000 Subject: [PATCH] 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 --- src/shell/braces.zig | 10 ++--- .../js/bun/shell/shell-brace-overflow.test.ts | 38 ++++++++++++++++--- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/src/shell/braces.zig b/src/shell/braces.zig index b8f17cdcb5..eb875e4834 100644 --- a/src/shell/braces.zig +++ b/src/shell/braces.zig @@ -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, diff --git a/test/js/bun/shell/shell-brace-overflow.test.ts b/test/js/bun/shell/shell-brace-overflow.test.ts index a0b88fa778..ba76f96222 100644 --- a/test/js/bun/shell/shell-brace-overflow.test.ts +++ b/test/js/bun/shell/shell-brace-overflow.test.ts @@ -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); });