import { afterEach, describe, expect, it } from "vitest"; import { promises as fs } from "node:fs"; import os from "node:os"; import path from "node:path"; import { spawn } from "node:child_process"; import { assertConfinedSandboxPath, parseTarVerboseListingLine, performSyncIn, performSyncOut, splitLinkEntryOnce, type PodStreamExec, } from "../../src/file-sync.js"; // --------------------------------------------------------------------------- // Test harness // // The K8s hooks transfer files over a single streaming exec per operation. In // production that exec streams raw tar bytes over the pod's exec WebSocket // (stdin from a host file, stdout into a host file); here the injected // `PodStreamExec` runs the generated `sh -c` script against the REAL host shell, // piping the caller's `stdin` Readable into the child and the child's stdout into // the caller's `stdout` Writable, using a host temp dir as the stand-in "sandbox" // workspace root. This exercises the actual tar/head/mv/realpath command shapes // end-to-end (a true round-trip) while recording every exec so we can assert the // single-exec contract and command shape. // --------------------------------------------------------------------------- interface RecordedCall { command: string[]; script: string; } function makeRealExec(): { exec: PodStreamExec; calls: RecordedCall[] } { const calls: RecordedCall[] = []; const exec: PodStreamExec = async (command, io) => { calls.push({ command, script: command[2] ?? "" }); return await new Promise((resolve, reject) => { const child = spawn(command[0], command.slice(1)); let err = ""; child.stderr.on("data", (chunk: Buffer) => { err += chunk.toString("utf-8"); }); child.on("error", reject); if (io.stdout) { io.stdout.on("error", reject); child.stdout.pipe(io.stdout); } else { child.stdout.resume(); } if (io.stdin) { io.stdin.on("error", reject); io.stdin.pipe(child.stdin); } else { child.stdin.end(); } child.on("close", (code) => { resolve({ exitCode: code ?? 0, stderr: err }); }); }); }; return { exec, calls }; } // A stub exec that emits a controlled number of bytes to the caller's stdout // sink without running any shell — used to drive the outbound disk-guard trip // with a pod-authored payload larger than the host allows. Rejects if the sink // errors (the guard fired). Records calls so a test can assert the exec ran. function makeOutputExec(totalBytes: number): { exec: PodStreamExec; calls: RecordedCall[] } { const calls: RecordedCall[] = []; const exec: PodStreamExec = async (command, io) => { calls.push({ command, script: command[2] ?? "" }); return await new Promise((resolve, reject) => { const sink = io.stdout; if (!sink) { resolve({ exitCode: 0, stderr: "" }); return; } sink.on("error", reject); sink.on("finish", () => resolve({ exitCode: 0, stderr: "" })); sink.end(Buffer.alloc(totalBytes, 0x41)); }); }; return { exec, calls }; } const tmpDirs: string[] = []; async function makeTmp(prefix: string): Promise { const dir = await fs.mkdtemp(path.join(os.tmpdir(), prefix)); tmpDirs.push(dir); return dir; } afterEach(async () => { await Promise.all(tmpDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true }))); }); describe("kubernetes file-sync path confinement", () => { it("rejects a target path that escapes the workspace remote dir", () => { expect(() => assertConfinedSandboxPath("/workspace", "/workspace/../etc/passwd", "target")).toThrow( /escapes|not a confined/, ); expect(() => assertConfinedSandboxPath("/workspace", "/etc/passwd", "target")).toThrow( /escapes|not a confined/, ); expect(() => assertConfinedSandboxPath("/workspace", "relative/path", "target")).toThrow( /not a confined/, ); }); it("accepts a target path inside the remote dir", () => { expect(() => assertConfinedSandboxPath("/workspace", "/workspace/a/b.txt", "target")).not.toThrow(); expect(() => assertConfinedSandboxPath("/workspace", "/workspace", "target")).not.toThrow(); }); }); describe("kubernetes onEnvironmentSyncIn (native single-exec transfer)", () => { it("transfers all file mappings of an operation in a SINGLE exec, staging to a temp then mv -f, applying secret mode 0600 with no widened window", async () => { const remoteDir = await makeTmp("k8s-sandbox-"); const host = await makeTmp("k8s-host-"); const plainSrc = path.join(host, "plain.txt"); const secretSrc = path.join(host, "auth.json"); await fs.writeFile(plainSrc, "hello world"); await fs.writeFile(secretSrc, "{\"token\":\"s3cr3t\"}"); const { exec, calls } = makeRealExec(); const result = await performSyncIn({ exec, remoteDir, timeoutMs: 30_000, operations: [ { operationId: "op-alpha", files: [ { sourcePath: plainSrc, targetPath: path.join(remoteDir, "plain.txt"), kind: "file" }, { sourcePath: secretSrc, targetPath: path.join(remoteDir, "nested/auth.json"), kind: "file", mode: 0o600, }, ], }, ], }); // Single exec for the whole file operation — NOT one exec per file/chunk. expect(calls).toHaveLength(1); // Atomic-replace shape and raw streamed-extract shape present in the script. expect(calls[0].script).toContain("mv -f"); expect(calls[0].script).toContain("head -c"); expect(calls[0].script).toContain("tar -xf -"); // Streaming transport: no base64 round-trip anywhere in the script. expect(calls[0].script).not.toContain("base64"); // Files landed with correct contents. expect(await fs.readFile(path.join(remoteDir, "plain.txt"), "utf-8")).toBe("hello world"); expect(await fs.readFile(path.join(remoteDir, "nested/auth.json"), "utf-8")).toBe( "{\"token\":\"s3cr3t\"}", ); // Secret landed 0600. expect((await fs.stat(path.join(remoteDir, "nested/auth.json"))).mode & 0o777).toBe(0o600); // No leftover reserved scratch dir in the workspace root. const leftovers = (await fs.readdir(remoteDir)).filter((e) => e.startsWith(".paperclip-upload")); expect(leftovers).toEqual([]); // Per-operation counts. expect(result.operations).toEqual([ { operationId: "op-alpha", filesTransferred: 2, bytesTransferred: expect.any(Number) }, ]); expect(result.operations[0].bytesTransferred).toBeGreaterThan(0); }); it("transfers a directory mapping honoring exclude, and emits tar -h only when followSymlinks is true", async () => { const remoteDir = await makeTmp("k8s-sandbox-"); const host = await makeTmp("k8s-host-"); const srcDir = path.join(host, "tree"); await fs.mkdir(path.join(srcDir, "sub"), { recursive: true }); await fs.writeFile(path.join(srcDir, "keep.txt"), "keep"); await fs.writeFile(path.join(srcDir, "skip.log"), "skip"); await fs.writeFile(path.join(srcDir, "sub", "data.bin"), "data"); // Preserve-symlink case (followSymlinks falsy → no -h). { const { exec, calls } = makeRealExec(); await performSyncIn({ exec, remoteDir, timeoutMs: 30_000, operations: [ { operationId: "op-dir", files: [ { sourcePath: srcDir, targetPath: path.join(remoteDir, "dst"), kind: "directory", exclude: ["*.log"], }, ], }, ], }); expect(calls).toHaveLength(1); expect(await fs.readFile(path.join(remoteDir, "dst/keep.txt"), "utf-8")).toBe("keep"); expect(await fs.readFile(path.join(remoteDir, "dst/sub/data.bin"), "utf-8")).toBe("data"); // Exclude honored. await expect(fs.stat(path.join(remoteDir, "dst/skip.log"))).rejects.toThrow(); } // Dereference case: followSymlinks true → -h on the host tar-create. { const derefSrc = path.join(host, "deref"); await fs.mkdir(derefSrc, { recursive: true }); await fs.writeFile(path.join(derefSrc, "real.txt"), "realbytes"); await fs.symlink(path.join(derefSrc, "real.txt"), path.join(derefSrc, "link.txt")); const derefTarget = await makeTmp("k8s-sandbox2-"); const { exec } = makeRealExec(); await performSyncIn({ exec, remoteDir: derefTarget, timeoutMs: 30_000, operations: [ { operationId: "op-deref", files: [ { sourcePath: derefSrc, targetPath: path.join(derefTarget, "out"), kind: "directory", followSymlinks: true, }, ], }, ], }); const linkStat = await fs.lstat(path.join(derefTarget, "out/link.txt")); // Dereferenced: the link became a regular file carrying the bytes. expect(linkStat.isSymbolicLink()).toBe(false); expect(await fs.readFile(path.join(derefTarget, "out/link.txt"), "utf-8")).toBe("realbytes"); } }); it("preserves a symlink as a link when followSymlinks is falsy", async () => { const remoteDir = await makeTmp("k8s-sandbox-"); const host = await makeTmp("k8s-host-"); const src = path.join(host, "tree"); await fs.mkdir(src, { recursive: true }); await fs.writeFile(path.join(src, "real.txt"), "realbytes"); await fs.symlink("real.txt", path.join(src, "link.txt")); const { exec } = makeRealExec(); await performSyncIn({ exec, remoteDir, timeoutMs: 30_000, operations: [ { operationId: "op", files: [{ sourcePath: src, targetPath: path.join(remoteDir, "out"), kind: "directory" }], }, ], }); const linkStat = await fs.lstat(path.join(remoteDir, "out/link.txt")); expect(linkStat.isSymbolicLink()).toBe(true); }); it("rejects a file mapping whose target escapes the remote dir before any exec runs", async () => { const remoteDir = await makeTmp("k8s-sandbox-"); const host = await makeTmp("k8s-host-"); const src = path.join(host, "x.txt"); await fs.writeFile(src, "x"); const { exec, calls } = makeRealExec(); await expect( performSyncIn({ exec, remoteDir, timeoutMs: 30_000, operations: [ { operationId: "op", files: [{ sourcePath: src, targetPath: `${remoteDir}/../escape.txt`, kind: "file" }], }, ], }), ).rejects.toThrow(/escapes|not a confined/); expect(calls).toHaveLength(0); }); it("refuses to create a target dir through a sandbox-planted symlink ancestor, mutating nothing outside the root (confine-before-mkdir)", async () => { const remoteDir = await makeTmp("k8s-sandbox-"); const outside = await makeTmp("k8s-outside-"); const host = await makeTmp("k8s-host-"); const src = path.join(host, "x.txt"); await fs.writeFile(src, "payload"); // A sandbox process planted `evil` inside the workspace as a symlink to an // out-of-root dir. The target is LEXICALLY confined (no `..`), so only the // in-pod realpath guard can catch it — and it must catch it BEFORE mkdir -p // follows the link and creates the tree outside the root. await fs.symlink(outside, path.join(remoteDir, "evil")); const { exec } = makeRealExec(); await expect( performSyncIn({ exec, remoteDir, timeoutMs: 30_000, operations: [ { operationId: "op", files: [ { sourcePath: src, targetPath: path.join(remoteDir, "evil", "sub", "f.txt"), kind: "file" }, ], }, ], }), ).rejects.toThrow(/ESCAPE|exit 42/); // The escape was rejected before mkdir ran: nothing was created outside root. await expect(fs.stat(path.join(outside, "sub"))).rejects.toThrow(); expect(await fs.readdir(outside)).toEqual([]); }); it("refuses to extract a directory mapping through a symlink ancestor, mutating nothing outside the root", async () => { const remoteDir = await makeTmp("k8s-sandbox-"); const outside = await makeTmp("k8s-outside-"); const host = await makeTmp("k8s-host-"); const srcDir = path.join(host, "tree"); await fs.mkdir(srcDir, { recursive: true }); await fs.writeFile(path.join(srcDir, "a.txt"), "aaa"); await fs.symlink(outside, path.join(remoteDir, "evil")); const { exec } = makeRealExec(); await expect( performSyncIn({ exec, remoteDir, timeoutMs: 30_000, operations: [ { operationId: "op", files: [ { sourcePath: srcDir, targetPath: path.join(remoteDir, "evil", "dst"), kind: "directory" }, ], }, ], }), ).rejects.toThrow(/ESCAPE|exit 42/); await expect(fs.stat(path.join(outside, "dst"))).rejects.toThrow(); expect(await fs.readdir(outside)).toEqual([]); }); it("streams a payload with no in-memory cap: a file larger than the former 100 MB ceiling transfers in one exec", async () => { // The streaming transport moves raw tar bytes over stdin straight to disk, so // there is no whole-payload buffer to bound. A payload the old base64-buffered // transport would have rejected now transfers fine. We assert the shape (one // exec, byte-exact `head -c`) on a modestly large file — enough to prove the // path never accumulates the payload as a string. const remoteDir = await makeTmp("k8s-sandbox-"); const host = await makeTmp("k8s-host-"); const src = path.join(host, "big.bin"); const payload = Buffer.alloc(2 * 1024 * 1024, 7); // 2 MiB await fs.writeFile(src, payload); const { exec, calls } = makeRealExec(); const result = await performSyncIn({ exec, remoteDir, timeoutMs: 30_000, operations: [ { operationId: "op", files: [{ sourcePath: src, targetPath: path.join(remoteDir, "big.bin"), kind: "file" }], }, ], }); expect(calls).toHaveLength(1); expect(calls[0].script).toMatch(/head -c \d+/); const landed = await fs.readFile(path.join(remoteDir, "big.bin")); expect(landed.equals(payload)).toBe(true); expect(result.operations[0].bytesTransferred).toBe(payload.length); }); }); describe("kubernetes onEnvironmentSyncOut (native single-exec transfer)", () => { it("streams all file mappings back over a SINGLE exec and reassembles them at host targets, preserving mode and per-operation counts", async () => { const remoteDir = await makeTmp("k8s-sandbox-"); const hostOut = await makeTmp("k8s-hostout-"); // Simulate sandbox-side files. await fs.writeFile(path.join(remoteDir, "result.txt"), "computed output"); await fs.writeFile(path.join(remoteDir, "secret.key"), "PRIVATE"); await fs.chmod(path.join(remoteDir, "secret.key"), 0o600); const plainTarget = path.join(hostOut, "a/result.txt"); const secretTarget = path.join(hostOut, "b/secret.key"); const { exec, calls } = makeRealExec(); const result = await performSyncOut({ exec, remoteDir, timeoutMs: 30_000, operations: [ { operationId: "op-out", files: [ { sourcePath: path.join(remoteDir, "result.txt"), targetPath: plainTarget, kind: "file" }, { sourcePath: path.join(remoteDir, "secret.key"), targetPath: secretTarget, kind: "file", mode: 0o600, }, ], }, ], }); expect(calls).toHaveLength(1); expect(await fs.readFile(plainTarget, "utf-8")).toBe("computed output"); expect(await fs.readFile(secretTarget, "utf-8")).toBe("PRIVATE"); expect((await fs.stat(secretTarget)).mode & 0o777).toBe(0o600); expect(result.operations).toEqual([ { operationId: "op-out", filesTransferred: 2, bytesTransferred: expect.any(Number) }, ]); // Reserved snapshot scratch cleaned up from the workspace root. const leftovers = (await fs.readdir(remoteDir)).filter((e) => e.startsWith(".paperclip-upload")); expect(leftovers).toEqual([]); }); it("round-trips a directory back to the host, preserving a symlink when followSymlinks is falsy", async () => { const remoteDir = await makeTmp("k8s-sandbox-"); const hostOut = await makeTmp("k8s-hostout-"); const srcDir = path.join(remoteDir, "artifacts"); await fs.mkdir(path.join(srcDir, "sub"), { recursive: true }); await fs.writeFile(path.join(srcDir, "a.txt"), "aaa"); await fs.writeFile(path.join(srcDir, "sub", "b.txt"), "bbb"); await fs.symlink("a.txt", path.join(srcDir, "link.txt")); const target = path.join(hostOut, "restored"); const { exec, calls } = makeRealExec(); const result = await performSyncOut({ exec, remoteDir, timeoutMs: 30_000, operations: [ { operationId: "op-dir-out", files: [{ sourcePath: srcDir, targetPath: target, kind: "directory" }], }, ], }); expect(calls).toHaveLength(1); expect(await fs.readFile(path.join(target, "a.txt"), "utf-8")).toBe("aaa"); expect(await fs.readFile(path.join(target, "sub/b.txt"), "utf-8")).toBe("bbb"); expect((await fs.lstat(path.join(target, "link.txt"))).isSymbolicLink()).toBe(true); expect(result.operations[0].filesTransferred).toBeGreaterThanOrEqual(2); }); it("rejects an outbound source that escapes the remote dir before any exec runs", async () => { const remoteDir = await makeTmp("k8s-sandbox-"); const hostOut = await makeTmp("k8s-hostout-"); const { exec, calls } = makeRealExec(); await expect( performSyncOut({ exec, remoteDir, timeoutMs: 30_000, operations: [ { operationId: "op", files: [ { sourcePath: "/etc/passwd", targetPath: path.join(hostOut, "leak"), kind: "file" }, ], }, ], }), ).rejects.toThrow(/escapes|not a confined/); expect(calls).toHaveLength(0); }); it("snapshots the outbound source through a pinned FD (never re-opening by name) so a post-resolve replacement cannot redirect the copy", async () => { const remoteDir = await makeTmp("k8s-sandbox-"); const hostOut = await makeTmp("k8s-hostout-"); await fs.writeFile(path.join(remoteDir, "result.txt"), "computed output"); const target = path.join(hostOut, "result.txt"); const { exec, calls } = makeRealExec(); await performSyncOut({ exec, remoteDir, timeoutMs: 30_000, operations: [ { operationId: "op", files: [ { sourcePath: path.join(remoteDir, "result.txt"), targetPath: target, kind: "file" }, ], }, ], }); // Correct bytes copied — through the FD, not the name. expect(await fs.readFile(target, "utf-8")).toBe("computed output"); // The copy reads the pinned FD, and the source is never re-opened by its // resolved name after validation (which is what the TOCTOU exploited). expect(calls[0].script).toContain("exec 7<"); expect(calls[0].script).toContain("cp -- /proc/self/fd/7"); expect(calls[0].script).not.toMatch(/cp -- "\$_pc_real"/); }); it("rejects an outbound source that is a symlink resolving outside the root, writing no target", async () => { const remoteDir = await makeTmp("k8s-sandbox-"); const outside = await makeTmp("k8s-outside-"); const hostOut = await makeTmp("k8s-hostout-"); await fs.writeFile(path.join(outside, "secret"), "PRIVATE"); // A symlink LEXICALLY inside the root that resolves to an out-of-root file — // only the in-pod realpath/FD guard can reject it. await fs.symlink(path.join(outside, "secret"), path.join(remoteDir, "link")); const target = path.join(hostOut, "leak"); const { exec } = makeRealExec(); await expect( performSyncOut({ exec, remoteDir, timeoutMs: 30_000, operations: [ { operationId: "op", files: [{ sourcePath: path.join(remoteDir, "link"), targetPath: target, kind: "file" }], }, ], }), ).rejects.toThrow(/ESCAPE|exit 42/); await expect(fs.stat(target)).rejects.toThrow(); }); it("fails closed when the outbound payload exceeds the streamed-output disk guard, writing no target", async () => { const remoteDir = await makeTmp("k8s-sandbox-"); const hostOut = await makeTmp("k8s-hostout-"); await fs.writeFile(path.join(remoteDir, "result.txt"), "computed output"); const target = path.join(hostOut, "result.txt"); // The (untrusted) pod streams far more than a 1KB guard allows; the host must // trip the streamed-bytes guard and abort before it can fill the disk, never // extracting a target. const { exec, calls } = makeOutputExec(4096); await expect( performSyncOut({ exec, remoteDir, timeoutMs: 30_000, maxOutputBytes: 1024, operations: [ { operationId: "op", files: [ { sourcePath: path.join(remoteDir, "result.txt"), targetPath: target, kind: "file" }, ], }, ], }), ).rejects.toThrow(/disk guard|exceeded/i); // The exec ran (source was confined), but the oversize stream is aborted, so // nothing lands at the host target. expect(calls).toHaveLength(1); await expect(fs.stat(target)).rejects.toThrow(); }); }); // --------------------------------------------------------------------------- // Post-upload commands (Phase 3 / Security Conditions C1–C4 + C7). Kubernetes is // a native provider, so it EXECUTES an operation's ordered `postUploadCommands` // symmetrically with Daytona after `uploadFiles` — never silently dropping them // (C7). Each command runs in the pod over its own exec, fail-fast, with the `cwd` // re-confined under the workspace remote dir. The injected exec runs the real // host shell, so these assertions observe true command side-effects. // --------------------------------------------------------------------------- describe("kubernetes onEnvironmentSyncIn (post-upload commands)", () => { it("runs post-upload commands in array order AFTER the upload, each verbatim as a positional arg", async () => { const remoteDir = await makeTmp("k8s-sandbox-"); const host = await makeTmp("k8s-host-"); const src = path.join(host, "config.txt"); await fs.writeFile(src, "hello"); const { exec, calls } = makeRealExec(); await performSyncIn({ exec, remoteDir, timeoutMs: 30_000, operations: [ { operationId: "op-cmd", files: [{ sourcePath: src, targetPath: path.join(remoteDir, "config.txt"), kind: "file" }], // First command reads the just-uploaded file (proves upload-before-command); // second appends (proves array order). cwd absent → the remote dir. postUploadCommands: [ { command: "cat config.txt > out.txt" }, { command: "printf DONE >> out.txt" }, ], }, ], }); // Upload-before-commands AND order: out.txt is the first command's read of the // uploaded bytes, then the second command's append. expect(await fs.readFile(path.join(remoteDir, "out.txt"), "utf-8")).toBe("helloDONE"); // One exec for the transfer, then one exec per command (single-exec-per-command). expect(calls).toHaveLength(3); // C1/C3: each command rides as the FINAL positional argument, byte-for-byte // unmutated — the provider concatenated no shell fragment onto it. const cmdCalls = calls.filter((c) => c.command.includes("cat config.txt > out.txt") || c.command.includes("printf DONE >> out.txt")); expect(cmdCalls).toHaveLength(2); expect(cmdCalls[0].command[cmdCalls[0].command.length - 1]).toBe("cat config.txt > out.txt"); expect(cmdCalls[1].command[cmdCalls[1].command.length - 1]).toBe("printf DONE >> out.txt"); // Absent cwd defaults to the remote dir (the penultimate positional arg), never // a process default cwd (C2). expect(cmdCalls[0].command[cmdCalls[0].command.length - 2]).toBe(remoteDir); }); it("runs a command in an explicit confined cwd", async () => { const remoteDir = await makeTmp("k8s-sandbox-"); const host = await makeTmp("k8s-host-"); const src = path.join(host, "seed.txt"); await fs.writeFile(src, "x"); const subDir = path.join(remoteDir, "sub"); const { exec } = makeRealExec(); await performSyncIn({ exec, remoteDir, timeoutMs: 30_000, operations: [ { operationId: "op-cwd", files: [{ sourcePath: src, targetPath: path.join(subDir, "seed.txt"), kind: "file" }], postUploadCommands: [{ command: "pwd -P > where.txt", cwd: subDir }], }, ], }); // The command ran with its cwd = subDir: `where.txt` lands there (relative // write), and its physical cwd (`pwd -P`) is the realpath of subDir. expect((await fs.readFile(path.join(subDir, "where.txt"), "utf-8")).trim()).toBe( await fs.realpath(subDir), ); }); it("aborts the operation fail-loud on a non-zero post-upload command exit, skipping the remainder (C4)", async () => { const remoteDir = await makeTmp("k8s-sandbox-"); const host = await makeTmp("k8s-host-"); const src = path.join(host, "config.txt"); await fs.writeFile(src, "hello"); const { exec } = makeRealExec(); await expect( performSyncIn({ exec, remoteDir, timeoutMs: 30_000, operations: [ { operationId: "op-fail", files: [{ sourcePath: src, targetPath: path.join(remoteDir, "config.txt"), kind: "file" }], postUploadCommands: [ { command: "exit 3" }, { command: "touch should_not_exist.txt" }, ], }, ], }), ).rejects.toThrow(/post-upload command failed \(exit 3\)/); // Fail-fast: the command after the failing one never ran. await expect(fs.stat(path.join(remoteDir, "should_not_exist.txt"))).rejects.toThrow(); }); it("rejects a post-upload command cwd that escapes the remote dir lexically, before any exec (C2)", async () => { const remoteDir = await makeTmp("k8s-sandbox-"); const host = await makeTmp("k8s-host-"); const src = path.join(host, "config.txt"); await fs.writeFile(src, "hello"); for (const badCwd of [`${remoteDir}/../escape`, "/etc"]) { const { exec, calls } = makeRealExec(); await expect( performSyncIn({ exec, remoteDir, timeoutMs: 30_000, operations: [ { operationId: "op-escape", files: [{ sourcePath: src, targetPath: path.join(remoteDir, "config.txt"), kind: "file" }], postUploadCommands: [{ command: "touch pwned.txt", cwd: badCwd }], }, ], }), ).rejects.toThrow(/escapes|not a confined/); // Rejected before the command exec: only the transfer exec ran, no command. expect(calls.some((c) => c.command.includes("touch pwned.txt"))).toBe(false); } }); it("rejects a post-upload command whose cwd resolves outside the root via a symlink (realpath guard, C2)", async () => { const remoteDir = await makeTmp("k8s-sandbox-"); const host = await makeTmp("k8s-host-"); const outside = await makeTmp("k8s-outside-"); const src = path.join(host, "config.txt"); await fs.writeFile(src, "hello"); // A symlink LEXICALLY inside the root that resolves to an out-of-root dir. await fs.symlink(outside, path.join(remoteDir, "evil")); const cwd = path.join(remoteDir, "evil"); const { exec } = makeRealExec(); await expect( performSyncIn({ exec, remoteDir, timeoutMs: 30_000, operations: [ { operationId: "op-symlink", files: [{ sourcePath: src, targetPath: path.join(remoteDir, "config.txt"), kind: "file" }], postUploadCommands: [{ command: "touch pwned.txt", cwd }], }, ], }), ).rejects.toThrow(/ESCAPE|exit 42/); // The command never ran through the symlink: nothing landed in the outside dir. await expect(fs.stat(path.join(outside, "pwned.txt"))).rejects.toThrow(); }); it("issues no extra exec when an operation has no post-upload commands (backward-compat)", async () => { const remoteDir = await makeTmp("k8s-sandbox-"); const host = await makeTmp("k8s-host-"); const src = path.join(host, "config.txt"); await fs.writeFile(src, "hello"); const { exec: baseExec, calls: baseCalls } = makeRealExec(); await performSyncIn({ exec: baseExec, remoteDir, timeoutMs: 30_000, operations: [ { operationId: "op-plain", files: [{ sourcePath: src, targetPath: path.join(remoteDir, "config.txt"), kind: "file" }] }, ], }); const { exec: emptyExec, calls: emptyCalls } = makeRealExec(); await performSyncIn({ exec: emptyExec, remoteDir, timeoutMs: 30_000, operations: [ { operationId: "op-plain", files: [{ sourcePath: src, targetPath: path.join(remoteDir, "config.txt"), kind: "file" }], postUploadCommands: [], }, ], }); // An absent/empty command list adds zero execs — byte-identical to today. expect(emptyCalls).toHaveLength(baseCalls.length); expect(emptyCalls).toHaveLength(1); }); }); describe("parseTarVerboseListingLine", () => { it("parses GNU tar listing lines (file, dir, symlink, hardlink, numeric owner)", () => { expect(parseTarVerboseListingLine("-rw-r--r-- daytona/daytona 7560 2026-08-11 21:43 AGENTS.md")).toEqual({ typeFlag: "-", rest: "AGENTS.md", }); expect(parseTarVerboseListingLine("drwxr-xr-x daytona/daytona 0 2026-08-11 21:43 nested/")).toEqual({ typeFlag: "d", rest: "nested/", }); expect( parseTarVerboseListingLine("lrwxrwxrwx daytona/daytona 0 2026-08-11 21:43 shortcut -> nested/data.txt"), ).toEqual({ typeFlag: "l", rest: "shortcut -> nested/data.txt" }); expect( parseTarVerboseListingLine("hrw-r--r-- daytona/daytona 0 2026-08-11 21:43 copy.txt link to data.txt"), ).toEqual({ typeFlag: "h", rest: "copy.txt link to data.txt" }); expect(parseTarVerboseListingLine("-rw-r--r-- 0/0 12 2026-08-11 21:43 root-owned.txt")).toEqual({ typeFlag: "-", rest: "root-owned.txt", }); }); it("parses bsdtar (macOS) listing lines, including year-form dates", () => { expect(parseTarVerboseListingLine("-rw-r--r-- 0 daytona daytona 7560 Aug 11 21:43 AGENTS.md")).toEqual({ typeFlag: "-", rest: "AGENTS.md", }); expect(parseTarVerboseListingLine("drwxr-xr-x 0 daytona daytona 0 Aug 11 21:43 nested/")).toEqual({ typeFlag: "d", rest: "nested/", }); expect( parseTarVerboseListingLine("lrwxr-xr-x 0 daytona daytona 0 Aug 11 21:43 shortcut -> nested/data.txt"), ).toEqual({ typeFlag: "l", rest: "shortcut -> nested/data.txt" }); expect( parseTarVerboseListingLine("hrw-r--r-- 0 daytona daytona 0 Aug 11 21:43 copy.txt link to data.txt"), ).toEqual({ typeFlag: "h", rest: "copy.txt link to data.txt" }); expect(parseTarVerboseListingLine("-rw-r--r-- 0 daytona daytona 7560 Aug 11 2025 old.txt")).toEqual({ typeFlag: "-", rest: "old.txt", }); }); it("keeps the true member name for bsdtar lines with numeric uid/gid, so traversal stays visible", () => { // With unresolvable ids bsdtar prints bare numbers; a looser GNU-first parse // would read this shape shifted by one field and report the member name as // "21:43 ../escape.txt", hiding the leading "../" from the traversal check. expect(parseTarVerboseListingLine("-rw-r--r-- 0 1001 1001 7560 Aug 11 21:43 ../escape.txt")).toEqual({ typeFlag: "-", rest: "../escape.txt", }); }); it("returns null (fail closed) for lines matching neither dialect", () => { expect(parseTarVerboseListingLine("not a tar listing line")).toBeNull(); expect(parseTarVerboseListingLine("tar: Error is not recoverable: exiting now")).toBeNull(); // Device nodes carry "major,minor" instead of a byte count in both dialects. expect(parseTarVerboseListingLine("crw-rw-rw- root/root 1,3 2026-08-11 21:43 dev/null")).toBeNull(); expect(parseTarVerboseListingLine("crw-rw-rw- 0 root wheel 1,3 Aug 11 21:43 dev/null")).toBeNull(); }); }); describe("splitLinkEntryOnce", () => { it("splits a clean single-delimiter link field", () => { expect(splitLinkEntryOnce("shortcut -> nested/data.txt", " -> ")).toEqual({ name: "shortcut", target: "nested/data.txt", }); expect(splitLinkEntryOnce("copy.txt link to data.txt", " link to ")).toEqual({ name: "copy.txt", target: "data.txt", }); }); it("returns null (fail closed) when the delimiter is absent or appears more than once", () => { expect(splitLinkEntryOnce("no delimiter here", " -> ")).toBeNull(); // A link name or target embedding the delimiter makes the split point // unresolvable; either split choice can hide an escaping target. expect(splitLinkEntryOnce("evil -> decoy -> ../../outside.txt", " -> ")).toBeNull(); expect(splitLinkEntryOnce("a link to b link to ../../outside.txt", " link to ")).toBeNull(); }); });