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
42 changes: 42 additions & 0 deletions src/lib/onboard/docker-gpu-patch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
22 changes: 20 additions & 2 deletions src/lib/onboard/docker-gpu-patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/<pid>/task/<tid>/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);
}
Expand Down
Loading