fix(sandbox): centralize async git worktree resolution and enforce read-only security (#25040)

This commit is contained in:
Emily Hedlund
2026-04-09 15:04:16 -07:00
committed by GitHub
parent 0f7f7be4ef
commit 451edb3ea6
11 changed files with 459 additions and 208 deletions
@@ -339,4 +339,61 @@ describe.skipIf(os.platform() === 'win32')('buildBwrapArgs', () => {
const envIndex = args.indexOf(`${includeDir}/.env`);
expect(args[envIndex - 2]).toBe('--bind');
});
it('binds git worktree directories if present', async () => {
const worktreeGitDir = '/path/to/worktree/.git';
const mainGitDir = '/path/to/main/.git';
const args = await buildBwrapArgs({
...defaultOptions,
resolvedPaths: createResolvedPaths({
gitWorktree: {
worktreeGitDir,
mainGitDir,
},
}),
});
expect(args).toContain(worktreeGitDir);
expect(args).toContain(mainGitDir);
expect(args[args.indexOf(worktreeGitDir) - 1]).toBe('--ro-bind-try');
expect(args[args.indexOf(mainGitDir) - 1]).toBe('--ro-bind-try');
});
it('enforces read-only binding for git worktrees even if workspaceWrite is true', async () => {
const worktreeGitDir = '/path/to/worktree/.git';
const args = await buildBwrapArgs({
...defaultOptions,
workspaceWrite: true,
resolvedPaths: createResolvedPaths({
gitWorktree: {
worktreeGitDir,
},
}),
});
expect(args[args.indexOf(worktreeGitDir) - 1]).toBe('--ro-bind-try');
});
it('git worktree read-only bindings should override previous policyWrite bindings', async () => {
const worktreeGitDir = '/custom/worktree/.git';
const args = await buildBwrapArgs({
...defaultOptions,
resolvedPaths: createResolvedPaths({
policyWrite: ['/custom/worktree'],
gitWorktree: {
worktreeGitDir,
},
}),
});
const writeBindIndex = args.indexOf('/custom/worktree');
const worktreeBindIndex = args.lastIndexOf(worktreeGitDir);
expect(writeBindIndex).toBeGreaterThan(-1);
expect(worktreeBindIndex).toBeGreaterThan(-1);
expect(worktreeBindIndex).toBeGreaterThan(writeBindIndex);
});
});
@@ -11,7 +11,7 @@ import {
getSecretFileFindArgs,
type ResolvedSandboxPaths,
} from '../../services/sandboxManager.js';
import { resolveGitWorktreePaths, isErrnoException } from '../utils/fsUtils.js';
import { isErrnoException } from '../utils/fsUtils.js';
import { spawnAsync } from '../../utils/shell-utils.js';
import { debugLogger } from '../../utils/debugLogger.js';
@@ -70,16 +70,6 @@ export async function buildBwrapArgs(
bwrapArgs.push(bindFlag, workspace.resolved, workspace.resolved);
}
const { worktreeGitDir, mainGitDir } = resolveGitWorktreePaths(
workspace.resolved,
);
if (worktreeGitDir) {
bwrapArgs.push(bindFlag, worktreeGitDir, worktreeGitDir);
}
if (mainGitDir) {
bwrapArgs.push(bindFlag, mainGitDir, mainGitDir);
}
for (const includeDir of resolvedPaths.globalIncludes) {
bwrapArgs.push('--ro-bind-try', includeDir, includeDir);
}
@@ -113,6 +103,18 @@ export async function buildBwrapArgs(
}
}
// Grant read-only access to git worktrees/submodules. We do this last in order to
// ensure that these rules aren't overwritten by broader write policies.
if (resolvedPaths.gitWorktree) {
const { worktreeGitDir, mainGitDir } = resolvedPaths.gitWorktree;
if (worktreeGitDir) {
bwrapArgs.push('--ro-bind-try', worktreeGitDir, worktreeGitDir);
}
if (mainGitDir) {
bwrapArgs.push('--ro-bind-try', mainGitDir, mainGitDir);
}
}
for (const p of resolvedPaths.forbidden) {
if (!fs.existsSync(p)) continue;
try {