Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 14 additions & 13 deletions README.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export default [
allowDefaultProject: [
'eslint.config.js',
'test/utils/which-node-test.mjs',
'test/helpers/isolated/dist-cli.mjs',
'vendor/build-common.ts',
'vendor/seccomp/build.ts',
'vendor/srt-win/build.ts',
Expand Down
447 changes: 324 additions & 123 deletions src/sandbox/bwrap-mount-manifests.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/sandbox/linux-sandbox-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3095,7 +3095,7 @@ async function generateFilesystemArgs(
// it pinned. Emitted after every allow and deny bind, like the two stores
// below.
//
// Each is bound where it really is, not at the name the environment gave:
// Each is bound where it really is, not at the name it was worked out as:
// bubblewrap makes a bind's destination by name inside the new root, and a
// link with an absolute target on the way leads nowhere from there.
const names = [...mountPoints, ...mountPointsReliedOn()]
Expand Down
5 changes: 3 additions & 2 deletions test/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'

/**
* Get the path to the CLI entry point
* Get the path to the CLI entry point, kept out of the user's own mount point
* manifests
*/
function getCliPath(): string {
return join(process.cwd(), 'src', 'cli.ts')
return join(process.cwd(), 'test', 'helpers', 'isolated', 'cli.ts')
}

/**
Expand Down
4 changes: 2 additions & 2 deletions test/control-fd.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import * as path from 'path'
import { type Writable } from 'stream'
import { isLinux, isWindows } from './helpers/platform.js'

// Get the path to the built CLI
const CLI_PATH = path.join(process.cwd(), 'dist', 'cli.js')
// Get the path to the built CLI, kept out of the user's own mount point manifests
const CLI_PATH = path.join(process.cwd(), 'test/helpers/isolated/dist-cli.mjs')

// srt is expected to exit on its own shortly after the wrapped command
// (which runs for well under a second) finishes; a hang is a failure, not
Expand Down
2 changes: 2 additions & 0 deletions test/helpers/isolated/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import './places.js'
import '../../../src/cli.js'
5 changes: 5 additions & 0 deletions test/helpers/isolated/dist-cli.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// places.ts, for the built command line tool.
import { setMountPointManifestPlacesForTesting } from '../../../dist/sandbox/bwrap-mount-manifests.js'

setMountPointManifestPlacesForTesting('/nonexistent')
await import('../../../dist/cli.js')
2 changes: 2 additions & 0 deletions test/helpers/isolated/library.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import './places.js'
export * from '../../../src/sandbox/linux-sandbox-utils.js'
2 changes: 2 additions & 0 deletions test/helpers/isolated/manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import './places.js'
export * from '../../../src/sandbox/sandbox-manager.js'
2 changes: 2 additions & 0 deletions test/helpers/isolated/manifests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import './places.js'
export * from '../../../src/sandbox/bwrap-mount-manifests.js'
9 changes: 9 additions & 0 deletions test/helpers/isolated/places.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { setMountPointManifestPlacesForTesting } from '../../../src/sandbox/bwrap-mount-manifests.js'

// The first places the mount point manifests are kept in follow from the user
// id, so no environment keeps a test out of the ones the user's own processes
// keep. Whatever loads this looks for them under a root that is not there, finds
// none, and goes by $XDG_RUNTIME_DIR and the temp dir, which a test can give it.
// The preload loads it for the test process. A child process loads the library
// through one of the modules beside this one.
setMountPointManifestPlacesForTesting('/nonexistent')
61 changes: 56 additions & 5 deletions test/helpers/private-manifest-directory.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { afterAll, afterEach, beforeAll } from 'bun:test'
import {
chmodSync,
existsSync,
lstatSync,
mkdirSync,
mkdtempSync,
Expand All @@ -9,7 +11,7 @@ import {
rmSync,
} from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { dirname, join } from 'node:path'
import { forgetMountPointManifestDirectory } from '../../src/sandbox/bwrap-mount-manifests.js'
import { cleanupBwrapMountPoints } from '../../src/sandbox/linux-sandbox-utils.js'

Expand Down Expand Up @@ -58,17 +60,66 @@ function endSandboxesOnRecord(dir: string): void {
}
}

/**
* Makes `/var/tmp`, `/run/user/UID` and `/tmp` under `root`, as a system has
* them. Returns the places the manifests are kept in there, in the order they
* are tried, none of them made: for `setMountPointManifestPlacesForTesting`, or
* {@link inPrivateNamespace}.
*/
export function makePlaces(root: string): [string, string, string] {
const uid = process.getuid?.() ?? 0
const places = [
join(root, 'var/tmp', `srt-mount-points-${uid}`),
join(root, 'run/user', String(uid), 'srt-mount-points'),
join(root, 'tmp', `srt-mount-points-${uid}`),
] as const
places.forEach((place, i) => {
mkdirSync(dirname(place), { recursive: true })
chmodSync(dirname(place), i === 1 ? 0o700 : 0o1777)
})
return [...places]
}

/**
* The start of a command line that runs what follows it in a mount namespace of
* its own, where what {@link makePlaces} made under `root` stands at its real
* name: for a child process that is to work the places out for itself, with
* nothing put in its way, and not reach the user's own. `keep`, this repository
* and the runtime are bound back where they are: they may lie under one of the
* three. No PID namespace. Needs `bwrapCanNamespace`.
*/
export function inPrivateNamespace(root: string, keep: string[]): string[] {
return [
'bwrap',
'--die-with-parent',
'--dev-bind',
'/',
'/',
// Where one of them is not there, there is nothing under it to reach.
...['/var/tmp', '/run/user', '/tmp']
.filter(dir => existsSync(dir))
.flatMap(dir => ['--bind', join(root, dir), dir]),
...[
join(import.meta.dir, '../..'),
dirname(process.execPath),
...keep,
].flatMap(dir => ['--bind', dir, dir]),
'--',
]
}

/**
* Gives the enclosing `describe` a runtime directory and a temp dir of its own,
* for as long as its tests run.
*
* The manifests of every srt process of a user live in one directory, and a
* collect reads, judges and removes all of them, so a test that lists or
* attacks that directory would otherwise act on whatever else the user is
* running. The library looks under `$XDG_RUNTIME_DIR` first, and every wrap
* that restricts writes makes the directory under the temp dir as well, so both
* are replaced, before anything is cleaned up: nothing is made or removed in
* the directories the user's own processes keep.
* running. With the places that follow from the user id out of reach (see
* isolated/places.ts) the library looks under `$XDG_RUNTIME_DIR` first, and
* every wrap that restricts writes makes the directory under the temp dir as
* well, so both are replaced, before anything is cleaned up: nothing is made or
* removed in the directories the user's own processes keep.
*
* It also starts the `describe` with no wrap of this process outstanding, and
* leaves it so: the library's count of wraps is shared by every test file of a
Expand Down
12 changes: 7 additions & 5 deletions test/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import { afterAll } from 'bun:test'
import { mkdirSync, mkdtempSync, realpathSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import './helpers/isolated/places.js'

// Every wrap that restricts writes makes, binds and collects in the per-user
// directories the mount point manifests are kept in, under $XDG_RUNTIME_DIR and
// the temp dir. The run is given both of its own before any test file is
// loaded, so that no suite acts on what else the user is running. A `describe`
// that lists or attacks the manifests takes its own on top of these (see
// helpers/private-manifest-directory.ts).
// directories the mount point manifests are kept in. The ones that follow from
// the user id are put out of reach (see helpers/isolated/places.ts), which
// leaves those under $XDG_RUNTIME_DIR and the temp dir. The run is given both of
// its own before any test file is loaded, so that no suite acts on what else
// the user is running. A `describe` that lists or attacks the manifests takes
// its own on top of these (see helpers/private-manifest-directory.ts).
if (process.platform === 'linux') {
// Short names: suites keep Unix sockets beneath these, and a socket's path is
// limited to 108 bytes.
Expand Down
2 changes: 1 addition & 1 deletion test/sandbox/default-write-paths.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ describe.if(isLinux || isMacOS)(
mkdirSync(join(fakeHome, NPM_LOGS), { recursive: true })
const { allowOnly, wrapped, reopened } = runWithHome(
fakeHome,
`const { SandboxManager } = await import('./src/sandbox/sandbox-manager.ts')
`const { SandboxManager } = await import('./test/helpers/isolated/manager.ts')
const filesystem = { denyRead: ['~/.npm'], allowWrite: [], denyWrite: [] }
await SandboxManager.initialize({
network: { allowedDomains: [], deniedDomains: [] },
Expand Down
5 changes: 1 addition & 4 deletions test/sandbox/linux-bwrap-args-file.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,7 @@ describe.if(isLinux)('bwrap --args for over-long profiles', () => {
`^${literally(STEP_SHELL)} '(?:${literally(RECORD_STEP)} && )?exec 9<"\\$1" && shift && exec "\\$@"' srt-args (?:\\S+\\.started )?(\\S+) bwrap (.*?) ?--args 9 (.*)$`,
's',
)
const MODULE = join(
import.meta.dir,
'../../src/sandbox/linux-sandbox-utils.ts',
)
const MODULE = join(import.meta.dir, '../helpers/isolated/library.ts')

let BASE: string
const savedCwd = process.cwd()
Expand Down
Loading
Loading