diff --git a/src/lib/onboard/docker-gpu-patch.test.ts b/src/lib/onboard/docker-gpu-patch.test.ts index c660a6cc5de..36badd180d8 100644 --- a/src/lib/onboard/docker-gpu-patch.test.ts +++ b/src/lib/onboard/docker-gpu-patch.test.ts @@ -142,6 +142,48 @@ describe("docker-gpu-patch", () => { expect(args).not.toEqual(expect.arrayContaining(["--env", "NVIDIA_VISIBLE_DEVICES=void"])); }); + it("adds SYS_PTRACE to the GPU clone when the baseline container lacks it", () => { + const inspect = inspectFixture(); + inspect.HostConfig!.CapAdd = ["SYS_ADMIN", "NET_ADMIN"]; + + const args = buildDockerGpuCloneRunArgs(inspect, buildDockerGpuMode("gpus")); + + expect(args).toEqual(expect.arrayContaining(["--cap-add", "SYS_PTRACE"])); + // The baseline caps are preserved alongside SYS_PTRACE. + expect(args).toEqual(expect.arrayContaining(["--cap-add", "SYS_ADMIN"])); + expect(args).toEqual(expect.arrayContaining(["--cap-add", "NET_ADMIN"])); + }); + + it("does not duplicate SYS_PTRACE when the baseline container already has it", () => { + const inspect = inspectFixture(); + inspect.HostConfig!.CapAdd = ["SYS_ADMIN", "SYS_PTRACE"]; + + const args = buildDockerGpuCloneRunArgs(inspect, buildDockerGpuMode("gpus")); + + const sysPtraceCount = args.filter((arg) => arg === "SYS_PTRACE").length; + expect(sysPtraceCount).toBe(1); + }); + + it("injects apparmor=unconfined when the baseline container has no apparmor profile", () => { + const inspect = inspectFixture(); + inspect.HostConfig!.SecurityOpt = []; + + const args = buildDockerGpuCloneRunArgs(inspect, buildDockerGpuMode("gpus")); + + expect(args).toEqual(expect.arrayContaining(["--security-opt", "apparmor=unconfined"])); + }); + + it("respects a baseline-pinned apparmor profile instead of overriding it", () => { + const inspect = inspectFixture(); + inspect.HostConfig!.SecurityOpt = ["apparmor=docker-default", "no-new-privileges"]; + + const args = buildDockerGpuCloneRunArgs(inspect, buildDockerGpuMode("gpus")); + + expect(args).toEqual(expect.arrayContaining(["--security-opt", "apparmor=docker-default"])); + expect(args).toEqual(expect.arrayContaining(["--security-opt", "no-new-privileges"])); + expect(args).not.toEqual(expect.arrayContaining(["--security-opt", "apparmor=unconfined"])); + }); + it("formats sanitized network diagnostics without dumping provider secrets", () => { const inspect = inspectFixture(); inspect.Config?.Env?.push("NVIDIA_API_KEY=secret"); diff --git a/src/lib/onboard/docker-gpu-patch.ts b/src/lib/onboard/docker-gpu-patch.ts index 85840c4341c..c154c706f04 100644 --- a/src/lib/onboard/docker-gpu-patch.ts +++ b/src/lib/onboard/docker-gpu-patch.ts @@ -426,9 +426,27 @@ export function buildDockerGpuCloneRunArgs( args.push("--restart", value); } - for (const cap of stringArray(host.CapAdd)) args.push("--cap-add", cap); + // GPU bring-up requires writing to /proc//task//comm (see + // PROC_COMM_WRITE_PROBE in initial-policy.ts). On some Docker/distro + // baselines, the OpenShell-created container that we inspect here lacks + // SYS_PTRACE and/or apparmor=unconfined, which the kernel/LSM combination + // requires for that write. Augment the recreate flags to make the + // GPU-capable container self-sufficient for the operations the GPU proof + // checks, regardless of what the non-GPU baseline happened to set (#3511). + const capAdd = new Set(stringArray(host.CapAdd)); + capAdd.add("SYS_PTRACE"); + for (const cap of capAdd) args.push("--cap-add", cap); for (const cap of stringArray(host.CapDrop)) args.push("--cap-drop", cap); - for (const opt of stringArray(host.SecurityOpt)) args.push("--security-opt", opt); + const securityOpt = new Set(stringArray(host.SecurityOpt)); + // Only inject apparmor=unconfined when the baseline did not pin a specific + // apparmor profile. Docker rejects multiple `--security-opt apparmor=...` + // entries, and a baseline that explicitly chose `apparmor=docker-default` + // (or similar) should be respected — we are scoped to the GPU recreate + // path, not to overriding deliberate operator choices. + if (![...securityOpt].some((entry) => entry.startsWith("apparmor"))) { + securityOpt.add("apparmor=unconfined"); + } + for (const opt of securityOpt) args.push("--security-opt", opt); if (networkMode !== "host") { for (const hostEntry of stringArray(host.ExtraHosts)) args.push("--add-host", hostEntry); }