From 836ab205754f6a1132e0ab5363106beebf301951 Mon Sep 17 00:00:00 2001 From: garrytan Date: Mon, 19 Oct 2026 21:22:00 +0000 Subject: [PATCH] fix: keep browse/src/bun-child-config.ts unchanged so the native cookie qualification still binds Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- CHANGELOG.md | 2 +- browse/src/bun-child-config.ts | 37 ++++++++++++++++++++++++++++++-- lib/bun-isolation.ts | 3 +++ test/bun-entry-isolation.test.ts | 4 ++++ 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92b6f4566..abe8cadb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ ### Security - **Every bun entry point starts isolated:** all 123 bun shebangs are now `#!/usr/bin/env -S bun --no-env-file --config=/dev/null`. `bun run ` ignores the shebang, so the callers carry the flags too: skill templates, generated skill docs, resolvers, hook wrappers, `setup` and the bash bins pass `bun --no-env-file --config=/dev/null` for every `run`, `-e`, `-p` and script path. -- **TypeScript spawns use `bunIsolationArgs()`** from `lib/bun-isolation.ts`, which now owns browse's `emptyBunConfig()`. This covers `scriptInvocation` (how TypeScript launches gstack's own bins), restamp, qa-deadline, the timeline hook's repair child, gbrain sync's ingest child, the agents-digest regen and the Claude Code migration. Native Windows has no `/dev/null`, so there the helper passes a private zero-byte bunfig. Under Git Bash, MSYS rewrites the shebang's `/dev/null` to `nul`, which Bun 1.4.2 reads as an empty config. A windows-latest run verified both. +- **TypeScript spawns use `bunIsolationArgs()`** from `lib/bun-isolation.ts`, which follows browse's `emptyBunConfig()`. This covers `scriptInvocation` (how TypeScript launches gstack's own bins), restamp, qa-deadline, the timeline hook's repair child, gbrain sync's ingest child, the agents-digest regen and the Claude Code migration. Native Windows has no `/dev/null`, so there the helper passes a private zero-byte bunfig. Under Git Bash, MSYS rewrites the shebang's `/dev/null` to `nul`, which Bun 1.4.2 reads as an empty config. A windows-latest run verified both. - **Compiled builds:** `build:gates` and `scripts/build-app.sh` now pass `--no-compile-autoload-dotenv --no-compile-autoload-bunfig`, as `scripts/build.sh` already did. Without them, compiled binaries load both files. - **A ratchet and regressions:** - `test/bun-entry-isolation.test.ts` fails on any bun shebang, shell or skill caller, TypeScript spawn or `build --compile` that drops the flags. diff --git a/browse/src/bun-child-config.ts b/browse/src/bun-child-config.ts index 98e6a8ab0..045a97571 100644 --- a/browse/src/bun-child-config.ts +++ b/browse/src/bun-child-config.ts @@ -1,2 +1,35 @@ -/** The empty `--config` for every Bun child browse spawns (#3085); lib/bun-isolation.ts owns it. */ -export { emptyBunConfig } from '../../lib/bun-isolation'; +/** + * The `--config` file for every Bun child browse spawns (#3085). The child + * runs in the user's project, where Bun would otherwise load that project's + * bunfig.toml, whose preload runs code. POSIX passes /dev/null. Windows has no + * /dev/null and `--config=NUL` panicked Bun 1.3.10 while parsing the flag, so + * Windows gets an empty bunfig created exclusively in a private directory and + * re-checked to be a zero-byte regular file before each spawn; anything else + * is replaced, so the file cannot become a preload vector. + */ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +let emptyConfig: string | null = null; + +function isEmptyRegularFile(file: string): boolean { + try { + const stat = fs.lstatSync(file); + return stat.isFile() && stat.size === 0; + } catch { + return false; + } +} + +export function emptyBunConfig(platform: NodeJS.Platform = process.platform): string { + if (platform !== 'win32') return '/dev/null'; + if (emptyConfig && isEmptyRegularFile(emptyConfig)) return emptyConfig; + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-bunfig-')); + const file = path.join(dir, 'bunfig.toml'); + fs.closeSync(fs.openSync(file, 'wx', 0o600)); + if (!isEmptyRegularFile(file)) throw new Error(`empty Bun config ${file} is not a zero-byte file`); + process.once('exit', () => fs.rmSync(dir, { recursive: true, force: true })); + emptyConfig = file; + return file; +} diff --git a/lib/bun-isolation.ts b/lib/bun-isolation.ts index 15b016dc1..0853541d5 100644 --- a/lib/bun-isolation.ts +++ b/lib/bun-isolation.ts @@ -14,6 +14,9 @@ * Windows gets an empty bunfig created exclusively in a private directory and * re-checked to be a zero-byte regular file before each spawn; anything else * is replaced, so the file cannot become a preload vector. + * browse/src/bun-child-config.ts keeps its own copy of emptyBunConfig(): the + * Windows native cookie qualification binds that file's bytes and bundles it + * without lib/. */ import * as fs from 'fs'; import * as os from 'os'; diff --git a/test/bun-entry-isolation.test.ts b/test/bun-entry-isolation.test.ts index 1c21957e9..d2de23212 100644 --- a/test/bun-entry-isolation.test.ts +++ b/test/bun-entry-isolation.test.ts @@ -22,6 +22,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import { bunIsolationArgs, emptyBunConfig } from '../lib/bun-isolation'; import { scriptInvocation } from '../lib/script-invocation'; +import { emptyBunConfig as browseEmptyBunConfig } from '../browse/src/bun-child-config'; const ROOT = path.resolve(import.meta.dir, '..'); const FLAGS = '--no-env-file --config=/dev/null'; @@ -154,6 +155,9 @@ describe('GSTA-56: gstack never takes configuration from the audited repository' expect(inv).toEqual({ ok: true, cmd: 'bun', argv: [...bunIsolationArgs(), path.join(ROOT, 'bin', 'gstack-decision-log'), '--help'] }); expect(bunIsolationArgs('linux')).toEqual(['--no-env-file', '--config=/dev/null']); expect(bunIsolationArgs('win32')).toEqual(['--no-env-file', `--config=${emptyBunConfig('win32')}`]); + expect(browseEmptyBunConfig('linux')).toBe(emptyBunConfig('linux')); + expect(fs.statSync(browseEmptyBunConfig('win32')).size).toBe(0); + expect(fs.statSync(emptyBunConfig('win32')).size).toBe(0); }); test('every bun build --compile disables .env and bunfig autoload', () => {