fix(sandbox): reject overlapping Windows temp roots
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/sandbox/sandbox-local/README.md
|
||||
README.md: eebb0df957f66585d8bbe35fee158776231f80d5
|
||||
README.zh.md: d4a9cef722134b3ffab238b43b73d83e13d4ee6d
|
||||
README.md: e43133c7c5b64d7779162b790ee6cab7806fd100
|
||||
README.zh.md: 32743d1b0aba5bed41ee53d90c4ed44dc936161c
|
||||
|
||||
@@ -12,7 +12,7 @@ Policy is per call; the provider stores only the mechanism and cached runner ver
|
||||
|
||||
The Seatbelt profile is allow-default with `(deny file-write*)` plus write allow-lists, so exactly the mode's promised file effects are governed: `read-only` grants the `/dev/null` literal alone; `workspace-write` adds the workspace root, `/tmp`, and the per-user darwin temp dir (`os.tmpdir()` — the platform's real temp area for mkstemp-family tools), every root canonicalized because Seatbelt matches resolved paths (`/tmp` IS `/private/tmp`). Apple marks the `sandbox-exec` CLI deprecated but ships it on every macOS; the functional probe is what fails closed if that ever changes.
|
||||
|
||||
The Windows rung keeps one deterministic write SID and standing ACE per workspace, but gives every live session/workspace pair a random private temp directory with a distinct SID and revocable ACE. Sessions sharing a workspace therefore share its intended write authority without inheriting one another's temp authority. A fresh provider always chooses a new temp path and SID, so crash residue cannot block or authorize a resumed session; agentless calls receive the same per-invocation isolation from the runner.
|
||||
The Windows rung keeps one deterministic write SID and standing ACE per workspace, but gives every live session/workspace pair a random private temp directory with a distinct SID and revocable ACE. Sessions sharing a workspace therefore share its intended write authority without inheriting one another's temp authority. A fresh provider always chooses a new temp path and SID, so crash residue cannot block or authorize a resumed session; agentless calls receive the same per-invocation isolation from the runner. A workspace equal to or containing the platform temp root fails before any ACL mutation because its inheritable workspace ACE would otherwise reach every private temp child.
|
||||
|
||||
[`@deepseek-ai/node-addon-landlock-run`](https://www.npmjs.com/package/@deepseek-ai/node-addon-landlock-run) supplies the platform launcher, functional probe, and CLI argument vocabulary. This provider owns only mode-to-grant mapping and runner selection. Keeping path resolution and probe parsing with the versioned binary prevents contract drift.
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
Seatbelt profile 默认允许,但带 `(deny file-write*)` 和写入 allow-list,因此恰好约束相应模式承诺的文件操作:`read-only` 只授予 `/dev/null` 字面路径;`workspace-write` 另加工作区根目录、`/tmp` 和逐用户 darwin 临时目录(`os.tmpdir()`,即平台供 mkstemp 家族工具使用的真实临时区域)。每个根目录都经过规范化,因为 Seatbelt 匹配解析后的路径(`/tmp` 就是 `/private/tmp`)。Apple 将 `sandbox-exec` CLI(命令行界面)标为 deprecated,但所有 macOS 系统仍会提供它;若情况发生变化,功能探测会使执行被拒绝。
|
||||
|
||||
Windows 档为每个工作区保留一个确定性写入 SID 和常驻 ACE,但为每个活跃的会话/工作区对分配一个随机私有临时目录,以及不同的 SID 和可回收 ACE。因此,共享工作区的会话会共享预期的写权限,却不会继承彼此的临时目录权限。新的提供方总会选择新的临时路径和 SID,因此崩溃残留既无法阻止恢复的会话,也无法向其授权;runner 会为无 agent(智能体)的调用提供同样的逐调用隔离。
|
||||
Windows 档为每个工作区保留一个确定性写入 SID 和常驻 ACE,但为每个活跃的会话/工作区对分配一个随机私有临时目录,以及不同的 SID 和可回收 ACE。因此,共享工作区的会话会共享预期的写权限,却不会继承彼此的临时目录权限。新的提供方总会选择新的临时路径和 SID,因此崩溃残留既无法阻止恢复的会话,也无法向其授权;runner 会为无 agent(智能体)的调用提供同样的逐调用隔离。如果工作区等于或包含平台临时根目录,调用会在任何 ACL 改动发生前失败,因为否则其可继承的工作区 ACE 会延伸到每个私有临时子目录。
|
||||
|
||||
[`@deepseek-ai/node-addon-landlock-run`](https://www.npmjs.com/package/@deepseek-ai/node-addon-landlock-run)提供平台 launcher、功能探测和 CLI 参数词汇。该提供方只负责模式到授权的映射与 runner 选择。把路径解析和探测解析保留在带版本的 binary 中,可防止约定漂移。
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ import { assertNever } from '@deepseek-ai/dsh-llm'
|
||||
import { SandboxProvider, SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox'
|
||||
import type { ConfinedArgv, ConfinedSandboxMode, RunnerFailureRule, SandboxEnforcement, SandboxPolicy } from '@deepseek-ai/dsh-sandbox'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { AclWriteGrant, tempWriteSid, workspaceWriteSid } from '@deepseek-ai/dsh-sandbox-windows-acl'
|
||||
import { AclWriteGrant, assertTempRootOutsideWorkspace, tempWriteSid, workspaceWriteSid } from '@deepseek-ai/dsh-sandbox-windows-acl'
|
||||
import { bwrapProfileArgs, landlockProfileArgs, seatbeltProfileArgs } from './profiles.ts'
|
||||
|
||||
/** Plugin config. All optional — `static Config` supplies the defaults. */
|
||||
@@ -390,6 +390,7 @@ export class LocalSandboxProvider extends SandboxProvider {
|
||||
* @returns the pair's private temp directory and write capability.
|
||||
*/
|
||||
private materializeAclGrant(sessionId: SessionId, workspaceRoot: string): AclTempCapability {
|
||||
assertTempRootOutsideWorkspace(workspaceRoot, tmpdir())
|
||||
const writeSid = workspaceWriteSid(workspaceRoot)
|
||||
if (!this.workspaceGrants.has(workspaceRoot)) {
|
||||
const grant = AclWriteGrant.create(writeSid)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* is mocked; native access checks live in sandbox-windows-acl's runner suite.
|
||||
*/
|
||||
|
||||
import { existsSync, mkdtempSync, rmSync } from 'node:fs'
|
||||
import { existsSync, mkdtempSync, realpathSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { basename, join } from 'node:path'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
@@ -51,6 +51,13 @@ vi.mock('@deepseek-ai/dsh-sandbox-windows-acl', () => {
|
||||
}
|
||||
return {
|
||||
AclWriteGrant: MockAclWriteGrant,
|
||||
assertTempRootOutsideWorkspace: (workspaceRoot: string, tempRoot: string) => {
|
||||
const workspace = realpathSync.native(workspaceRoot)
|
||||
const temp = realpathSync.native(tempRoot)
|
||||
if (temp === workspace || temp.startsWith(`${workspace}${process.platform === 'win32' ? '\\' : '/'}`)) {
|
||||
throw new Error(`Windows ACL temp root must be outside the workspace: workspace=${workspaceRoot}; temp=${tempRoot}`)
|
||||
}
|
||||
},
|
||||
workspaceWriteSid: () => 'S-1-4-42-42',
|
||||
tempWriteSid: (path: string) => `TEMP:${path}`,
|
||||
}
|
||||
@@ -238,6 +245,14 @@ describe('windows-acl write grants (LocalSandboxProvider)', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('rejects a workspace containing the ambient temp root before any ACL mutation', async () => {
|
||||
const { sandbox } = await setup()
|
||||
expect(() => sandbox.confine(['true'], {
|
||||
mode: 'workspace-write', workspaceRoot: realpathSync.native(tmpdir()), sessionId: SessionId('overlap'),
|
||||
})).toThrow(/temp root must be outside the workspace/u)
|
||||
expect(mockState.grants).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('temp grant creation/add failures remove the random directory; cleanup failures aggregate', async () => {
|
||||
try {
|
||||
const { sandbox } = await setup()
|
||||
|
||||
Reference in New Issue
Block a user