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
20 changes: 20 additions & 0 deletions packages/codev-sandbox/src/socket.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { expect, test } from "bun:test"
import { join } from "node:path"
import { SANDBOX_SUN_PATH_MAX, sandboxSocketDir } from "./socket"

// The failure this guards against is silent: an over-long AF_UNIX path is
// truncated at bind time, so the socket appears where nobody is listening.
test("socket paths stay inside the AF_UNIX limit for realistic sandbox names", () => {
const appData = "/Users/someone/Library/Application Support/ai.opencode.desktop.dev/sandboxes"
for (const name of ["a", "codev-work", "a-rather-long-sandbox-name-someone-might-type"]) {
for (const file of ["server.sock", "broker.sock"]) {
const path = join(sandboxSocketDir(join(appData, name)), file)
expect(path.length).toBeLessThanOrEqual(SANDBOX_SUN_PATH_MAX)
}
}
})

test("each sandbox gets its own socket directory", () => {
expect(sandboxSocketDir("/data/one")).not.toBe(sandboxSocketDir("/data/two"))
expect(sandboxSocketDir("/data/one")).toBe(sandboxSocketDir("/data/one"))
})
25 changes: 25 additions & 0 deletions packages/codev-sandbox/src/socket.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { createHash } from "node:crypto"
import { mkdir } from "node:fs/promises"
import { tmpdir } from "node:os"
import { join } from "node:path"

// AF_UNIX paths are capped at 104 bytes on macOS (108 on Linux) — and the
// limit is enforced by silent truncation, so an over-long path binds
// somewhere unintended and the peer then fails with ENOENT. The app's data
// directory alone is ~74 characters, so a sandbox named "codev-work" already
// overflows. Sockets therefore live under a short per-user directory keyed by
// a hash of the sandbox's data dir, which keeps them unique per sandbox and
// well clear of the limit.
export const SANDBOX_SUN_PATH_MAX = 103

export function sandboxSocketDir(sandboxDir: string) {
return join(tmpdir(), `codev-sbx-${createHash("sha256").update(sandboxDir).digest("hex").slice(0, 10)}`)
}

export async function createSandboxSocketDir(sandboxDir: string) {
const dir = sandboxSocketDir(sandboxDir)
// 0700: on Linux the system temp dir is shared, so the sockets inside must
// not be reachable by other users.
await mkdir(dir, { recursive: true, mode: 0o700 })
return dir
}
10 changes: 7 additions & 3 deletions packages/codev-sandbox/src/vz/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { runCommand, unpackRootfs } from "../bwrap/rootfs"
import { readImageMarker, stageAsset, writeImageMarker } from "../image"
import type { SandboxAsset } from "../manifest"
import { macosSandboxAvailability } from "../policy"
import { createSandboxSocketDir } from "../socket"
import type { SandboxAvailability, SandboxConfig, SandboxJobOptions, SandboxProvider } from "../provider"
import { createVzHelperEventReader, type VzHelperEvent, type VzVmConfig } from "./protocol"

Expand Down Expand Up @@ -123,6 +124,9 @@ export function createVzProvider(options: VzProviderOptions): SandboxProvider {
await rm(dirs.runtime, { recursive: true, force: true })
await mkdir(dirs.runtime, { recursive: true })
await mkdir(dirs.guestConfig, { recursive: true })
// Sockets live outside the data dir: their absolute path must fit the
// AF_UNIX limit, which the data dir alone nearly exhausts.
const socketDir = await createSandboxSocketDir(dirs.base)

// The broker must be listening before guest traffic flows: codev-vmm
// dials runtime/broker.sock per guest connection and refuses when
Expand All @@ -133,7 +137,7 @@ export function createVzProvider(options: VzProviderOptions): SandboxProvider {
const broker = egress?.routes.length
? createBrokerServer({ routes: egress.routes, fetchImpl: egress.fetchImpl ?? fetchImpl, auth: egress.auth })
: null
if (broker) await broker.listenUnix(join(dirs.runtime, "broker.sock"))
if (broker) await broker.listenUnix(join(socketDir, "broker.sock"))
if (egress?.guestConfig) await writeFile(join(dirs.guestConfig, "codev.json"), egress.guestConfig)

const credentials = generateSandboxCredentials()
Expand All @@ -147,7 +151,7 @@ export function createVzProvider(options: VzProviderOptions): SandboxProvider {
},
}),
)
const serverSocket = join(dirs.runtime, "server.sock")
const serverSocket = join(socketDir, "server.sock")
const initrd = await stat(join(dirs.image, VZ_INITRD_FILE)).catch(() => null)
const vmConfig: VzVmConfig = {
kernel: join(dirs.image, VZ_KERNEL_FILE),
Expand All @@ -164,7 +168,7 @@ export function createVzProvider(options: VzProviderOptions): SandboxProvider {
],
forwards: [
{ direction: "host-to-guest", vsockPort: VZ_SERVER_VSOCK_PORT, socketPath: serverSocket },
{ direction: "guest-to-host", vsockPort: VZ_BROKER_VSOCK_PORT, socketPath: join(dirs.runtime, "broker.sock") },
{ direction: "guest-to-host", vsockPort: VZ_BROKER_VSOCK_PORT, socketPath: join(socketDir, "broker.sock") },
],
}
const configPath = join(dirs.runtime, "vm.json")
Expand Down
Loading