revert(sandbox): withdraw the credential-document read denial

The `readDenyPaths` policy field shipped in the previous commit broke Linux
confinement outright. bwrap has to create the `/dev/null` bind's mount point
inside a tree its own profile has already made read-only, so it refused the
entire confinement whenever the parent directory was absent — every host that
has not stored a credential yet, including a fresh install:

  bwrap: Can't mkdir parents for /home/runner/.dsh/.env: Read-only file system

which the executor correctly classifies as SANDBOX_UNAVAILABLE, so every
confined bash call failed closed. Landlock cannot subtract from its own `/`
read grant, so it reported `partial` enforcement on every confined call for a
file it never hid, with no way to switch the denial off (schemastery fills an
omitted array with `[]`, so empty and omitted were indistinguishable).

A protection that breaks confinement where it works and misreports it where it
does not is worse than a documented absence. Revert the field, both expressible
backends, the enforcement downgrade, and the policy default; state the residue
plainly in the credentials-local READMEs — file mode stops other OS users, not
the model — and keep the OS-keychain provider recorded as the real answer.

The narrower discipline stands: no surface hoists the credential document into
`process.env`, and the model is never handed a resolved path to it.
This commit is contained in:
Yichen Jiang
2026-07-30 17:09:42 +08:00
parent 52ae578982
commit a90ccc4453
27 changed files with 38 additions and 262 deletions

View File

@@ -62,27 +62,6 @@ describe('profile dialects', () => {
])
})
it('bwrap read denial: /dev/null over each denied path, after any workspace bind', () => {
expect(bwrapProfileArgs({ ...WW, readDenyPaths: ['/ws/secret.env'] })).toEqual([
'--ro-bind', '/', '/', '--dev', '/dev', '--proc', '/proc', '--die-with-parent',
'--tmpfs', '/tmp', '--bind', '/ws', '/ws',
// The workspace bind above would otherwise re-expose the file.
'--ro-bind-try', '/dev/null', '/ws/secret.env',
])
})
it('landlock ignores read denials: a `/` read grant cannot subtract from itself', () => {
expect(landlockProfileArgs({ ...RO, readDenyPaths: ['/ws/secret.env'] }))
.toEqual(landlockProfileArgs(RO))
})
it('seatbelt read denial: a trailing deny naming the path as both a file and a directory', () => {
expect(seatbeltProfileArgs({ ...RO, readDenyPaths: ['/ws/secret.env'] })).toEqual([
'-p',
`${SEATBELT_RO_PROFILE} (deny file-read* file-write* (literal "/ws/secret.env") (subpath "/ws/secret.env"))`,
])
})
it('landlock read-only: readable tree plus a writable /dev/null, nothing else', () => {
// /dev/null specifically, NOT /dev: a whole-/dev grant would let confined
// commands write real host paths beneath it (/dev/shm) under read-only.
@@ -329,15 +308,6 @@ describe('the default landlock probe (launcher CLI contract)', () => {
expect(sandbox.confine(['true'], RO).enforcement).toBe('partial')
})
it('reports partial enforcement when a read denial is requested it cannot express', async () => {
const launcher = fakeLauncher()
const { sandbox } = await setup({}, { platform: 'linux', probeBwrap: () => false, landlockLauncher: launcher })
// Fully enforced for the write policy, yet the read denial is
// unexpressible in an allow-list that already grants `/` for reads.
expect(sandbox.confine(['true'], RO).enforcement).toBe('full')
expect(sandbox.confine(['true'], { ...RO, readDenyPaths: ['/ws/secret.env'] }).enforcement).toBe('partial')
})
it('reads a failing launcher as unusable: the chain ends and fails closed', async () => {
const dir = mkdtempSync(join(tmpdir(), 'dsh-fake-landlock-'))
const launcher = join(dir, 'landlock-run')

View File

@@ -1,6 +1,6 @@
import { spawnSync } from 'node:child_process'
import { existsSync, readFileSync } from 'node:fs'
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { mkdtemp, rm } from 'node:fs/promises'
import { homedir, tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
@@ -70,37 +70,6 @@ describe.skipIf(!seatbeltUsable)('sandbox-local: real Seatbelt confinement throu
expect(result.stdout).toBe('dev-ok\n')
})
it('denies reading a credential document the mode would otherwise allow', async () => {
// The harness's own secret store: readable to the user, and the model's
// bash runs as that user — only the confinement can take it away.
const workdir = await tempDir(tmpdir())
const secret = join(workdir, '.env')
await writeFile(secret, 'DEEPSEEK_API_KEY=sk-must-not-leak\n', { mode: 0o600 })
const sandbox = await provider()
const allowed = runConfined(sandbox, `cat ${secret}`, { mode: 'read-only', workspaceRoot: workdir })
expect(allowed.result.stdout).toContain('sk-must-not-leak')
const denied = runConfined(sandbox, `cat ${secret}`, {
mode: 'read-only',
workspaceRoot: workdir,
readDenyPaths: [secret],
})
expect(denied.result.stdout).not.toContain('sk-must-not-leak')
expect(denied.result.status).not.toBe(0)
expect(denied.confined.enforcement).toBe('full')
// Everything else under the same directory stays readable: the denial is
// the credential document, not the harness home.
const sibling = join(workdir, 'notes.txt')
await writeFile(sibling, 'ordinary\n')
const neighbour = runConfined(sandbox, `cat ${sibling}`, {
mode: 'read-only',
workspaceRoot: workdir,
readDenyPaths: [secret],
})
expect(neighbour.result.stdout).toBe('ordinary\n')
})
it('read-only grants no temp area: a write under the user temp dir is denied too', async () => {
// The per-user darwin temp dir is a workspace-write grant, not a
// read-only one — under read-only the only write-shaped path is /dev/null.