mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-10 01:29:17 +00:00
fix(cli): respect sandbox path boundaries (#5375)
This commit is contained in:
parent
cb82a917bb
commit
de7e7557dc
3 changed files with 77 additions and 15 deletions
32
packages/cli/src/utils/sandbox-path.ts
Normal file
32
packages/cli/src/utils/sandbox-path.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import path from 'node:path';
|
||||
|
||||
export function isContainerPathWithinWorkdir(
|
||||
containerWorkdir: string,
|
||||
containerPath: string,
|
||||
): boolean {
|
||||
const normalize = (value: string) => {
|
||||
const normalized = path.posix
|
||||
.normalize(value.replace(/\\/g, '/'))
|
||||
.replace(/\/+$/, '')
|
||||
.toLowerCase();
|
||||
return normalized || '/';
|
||||
};
|
||||
|
||||
const normalizedWorkdir = normalize(containerWorkdir);
|
||||
const normalizedPath = normalize(containerPath);
|
||||
|
||||
if (normalizedWorkdir === '/') {
|
||||
return normalizedPath.startsWith('/');
|
||||
}
|
||||
|
||||
return (
|
||||
normalizedPath === normalizedWorkdir ||
|
||||
normalizedPath.startsWith(`${normalizedWorkdir}/`)
|
||||
);
|
||||
}
|
||||
|
|
@ -5,8 +5,42 @@
|
|||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { isContainerPathWithinWorkdir } from './sandbox-path.js';
|
||||
import { parseSandboxImageName } from './sandboxImageName.js';
|
||||
|
||||
describe('isContainerPathWithinWorkdir', () => {
|
||||
it('allows the workdir itself', () => {
|
||||
expect(isContainerPathWithinWorkdir('/repo/app', '/repo/app')).toBe(true);
|
||||
});
|
||||
|
||||
it('allows paths under the workdir', () => {
|
||||
expect(isContainerPathWithinWorkdir('/repo/app', '/repo/app/bin')).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects sibling paths with the same prefix', () => {
|
||||
expect(
|
||||
isContainerPathWithinWorkdir('/repo/app', '/repo/app-tools/bin'),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('allows absolute paths under the filesystem root workdir', () => {
|
||||
expect(isContainerPathWithinWorkdir('/', '/bin')).toBe(true);
|
||||
});
|
||||
|
||||
it('normalizes trailing slashes and case for container paths', () => {
|
||||
expect(
|
||||
isContainerPathWithinWorkdir('/C/Repo/App/', '/c/repo/app/bin'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('handles converted Windows drive roots without matching sibling drives', () => {
|
||||
expect(isContainerPathWithinWorkdir('/c', '/c/tools')).toBe(true);
|
||||
expect(isContainerPathWithinWorkdir('/c', '/c2/tools')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseSandboxImageName', () => {
|
||||
it('uses the image basename and tag for container names', () => {
|
||||
expect(parseSandboxImageName('ghcr.io/qwenlm/qwen-code:0.18.3')).toBe(
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import {
|
|||
import { randomBytes } from 'node:crypto';
|
||||
import { writeStderrLine } from './stdioHelpers.js';
|
||||
import { parseSandboxImageName } from './sandboxImageName.js';
|
||||
import { isContainerPathWithinWorkdir } from './sandbox-path.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
|
|
@ -121,9 +122,7 @@ function entrypoint(workdir: string, cliArgs: string[]): string[] {
|
|||
const paths = process.env['PATH'].split(pathSeparator);
|
||||
for (const p of paths) {
|
||||
const containerPath = getContainerPath(p);
|
||||
if (
|
||||
containerPath.toLowerCase().startsWith(containerWorkdir.toLowerCase())
|
||||
) {
|
||||
if (isContainerPathWithinWorkdir(containerWorkdir, containerPath)) {
|
||||
pathSuffix += `:${containerPath}`;
|
||||
}
|
||||
}
|
||||
|
|
@ -137,9 +136,7 @@ function entrypoint(workdir: string, cliArgs: string[]): string[] {
|
|||
const paths = process.env['PYTHONPATH'].split(pathSeparator);
|
||||
for (const p of paths) {
|
||||
const containerPath = getContainerPath(p);
|
||||
if (
|
||||
containerPath.toLowerCase().startsWith(containerWorkdir.toLowerCase())
|
||||
) {
|
||||
if (isContainerPathWithinWorkdir(containerWorkdir, containerPath)) {
|
||||
pythonPathSuffix += `:${containerPath}`;
|
||||
}
|
||||
}
|
||||
|
|
@ -723,8 +720,13 @@ export async function start_sandbox(
|
|||
// also mount-replace VIRTUAL_ENV directory with <project_settings>/sandbox.venv
|
||||
// sandbox can then set up this new VIRTUAL_ENV directory using sandbox.bashrc (see below)
|
||||
// directory will be empty if not set up, which is still preferable to having host binaries
|
||||
const virtualEnv = process.env['VIRTUAL_ENV'];
|
||||
if (
|
||||
process.env['VIRTUAL_ENV']?.toLowerCase().startsWith(workdir.toLowerCase())
|
||||
virtualEnv &&
|
||||
isContainerPathWithinWorkdir(
|
||||
getContainerPath(workdir),
|
||||
getContainerPath(virtualEnv),
|
||||
)
|
||||
) {
|
||||
const sandboxVenvPath = path.resolve(
|
||||
SETTINGS_DIRECTORY_NAME,
|
||||
|
|
@ -733,14 +735,8 @@ export async function start_sandbox(
|
|||
if (!fs.existsSync(sandboxVenvPath)) {
|
||||
fs.mkdirSync(sandboxVenvPath, { recursive: true });
|
||||
}
|
||||
args.push(
|
||||
'--volume',
|
||||
`${sandboxVenvPath}:${getContainerPath(process.env['VIRTUAL_ENV'])}`,
|
||||
);
|
||||
args.push(
|
||||
'--env',
|
||||
`VIRTUAL_ENV=${getContainerPath(process.env['VIRTUAL_ENV'])}`,
|
||||
);
|
||||
args.push('--volume', `${sandboxVenvPath}:${getContainerPath(virtualEnv)}`);
|
||||
args.push('--env', `VIRTUAL_ENV=${getContainerPath(virtualEnv)}`);
|
||||
}
|
||||
|
||||
// copy additional environment variables from SANDBOX_ENV
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue