Skip to content

Commit 1b71ae7

Browse files
committed
feat(supervisor): gate the runner seccomp profile by runtime scope
1 parent 84c4a91 commit 1b71ae7

6 files changed

Lines changed: 88 additions & 25 deletions

File tree

apps/supervisor/src/env.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,14 @@ export const Env = z
210210

211211
KUBERNETES_MEMORY_OVERHEAD_GB: z.coerce.number().min(0).optional(), // Optional memory overhead to add to the limit in GB
212212
KUBERNETES_SCHEDULER_NAME: z.string().optional(), // Custom scheduler name for pods
213-
KUBERNETES_RUNNER_SECCOMP_PROFILE_PATH: z.string().default("profiles/block-io-uring.json"),
213+
KUBERNETES_RUNNER_SECCOMP_PROFILE_PATH: z
214+
.string()
215+
.trim()
216+
.min(1)
217+
.default("profiles/block-io-uring.json"),
218+
KUBERNETES_RUNNER_SECCOMP_PROFILE_RUNTIMES: z
219+
.enum(["none", "node-24-plus", "all"])
220+
.default("node-24-plus"),
214221

215222
// Pod DNS config — override the cluster default ndots to `KUBERNETES_POD_DNS_NDOTS`.
216223
// Default k8s ndots is 5: any name with fewer than 5 dots (e.g. `api.example.com`, 2 dots) is first walked

apps/supervisor/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,7 @@ class ManagedSupervisor {
653653
projectId: message.project.id,
654654
deploymentFriendlyId: message.deployment.friendlyId,
655655
deploymentVersion: message.backgroundWorker.version,
656+
runtime: message.backgroundWorker.runtime,
656657
deploymentToken,
657658
runId: message.run.id,
658659
runFriendlyId: message.run.friendlyId,

apps/supervisor/src/workloadManager/kubernetes.test.ts

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -100,24 +100,56 @@ describe("withNodeSelector", () => {
100100
});
101101

102102
describe("withRunnerSeccompProfile", () => {
103-
it("applies the profile for every runtime, preserving pod security defaults", () => {
104-
const podSpec = withRunnerSeccompProfile(basePodSpec, "profiles/example.json");
103+
const base = {
104+
profilePath: "profiles/example.json",
105+
runtimes: "node-24-plus" as const,
106+
runtime: "node-24",
107+
checkpointsEnabled: true,
108+
};
109+
110+
const withProfile = {
111+
...basePodSpec,
112+
securityContext: {
113+
...basePodSpec.securityContext,
114+
seccompProfile: { type: "Localhost", localhostProfile: "profiles/example.json" },
115+
},
116+
};
117+
118+
it("applies the profile to node-24 and above under the default scope", () => {
119+
for (const runtime of ["node-24", "node-26", "node-30", "experimental-node-24"]) {
120+
expect(withRunnerSeccompProfile(basePodSpec, { ...base, runtime })).toMatchObject(
121+
withProfile
122+
);
123+
}
124+
});
105125

106-
expect(podSpec).toMatchObject({
107-
...basePodSpec,
108-
securityContext: {
109-
...basePodSpec.securityContext,
110-
seccompProfile: {
111-
type: "Localhost",
112-
localhostProfile: "profiles/example.json",
113-
},
114-
},
115-
});
126+
it("skips older runtimes under the default scope", () => {
127+
for (const runtime of ["node", "node-22", "bun", undefined, null, ""]) {
128+
expect(withRunnerSeccompProfile(basePodSpec, { ...base, runtime })).toBe(basePodSpec);
129+
}
130+
});
131+
132+
it("applies the profile to every runtime under the all scope", () => {
133+
for (const runtime of ["node", "node-22", "bun", "node-24", undefined]) {
134+
expect(
135+
withRunnerSeccompProfile(basePodSpec, { ...base, runtimes: "all", runtime })
136+
).toMatchObject(withProfile);
137+
}
138+
});
139+
140+
it("applies nothing under the none scope, whatever the runtime", () => {
141+
for (const runtime of ["node-24", "bun", "node-22"]) {
142+
expect(withRunnerSeccompProfile(basePodSpec, { ...base, runtimes: "none", runtime })).toBe(
143+
basePodSpec
144+
);
145+
}
116146
});
117147

118-
it("leaves the pod spec untouched when no profile is configured", () => {
119-
for (const profilePath of [undefined, ""]) {
120-
expect(withRunnerSeccompProfile(basePodSpec, profilePath)).toBe(basePodSpec);
148+
it("applies nothing when checkpoints are disabled", () => {
149+
for (const runtimes of ["none", "node-24-plus", "all"] as const) {
150+
expect(
151+
withRunnerSeccompProfile(basePodSpec, { ...base, runtimes, checkpointsEnabled: false })
152+
).toBe(basePodSpec);
121153
}
122154
});
123155
});

apps/supervisor/src/workloadManager/kubernetes.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,12 @@ export class KubernetesWorkloadManager implements WorkloadManager {
135135
);
136136
}
137137
}
138-
const podSpec = this.opts.checkpointsEnabled
139-
? withRunnerSeccompProfile(basePodSpec, env.KUBERNETES_RUNNER_SECCOMP_PROFILE_PATH)
140-
: basePodSpec;
138+
const podSpec = withRunnerSeccompProfile(basePodSpec, {
139+
profilePath: env.KUBERNETES_RUNNER_SECCOMP_PROFILE_PATH,
140+
runtimes: env.KUBERNETES_RUNNER_SECCOMP_PROFILE_RUNTIMES,
141+
runtime: opts.runtime,
142+
checkpointsEnabled: this.opts.checkpointsEnabled,
143+
});
141144

142145
await this.k8s.core.createNamespacedPod({
143146
namespace: this.namespace,

apps/supervisor/src/workloadManager/kubernetesPodSpec.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,26 +54,44 @@ export function withNodeSelector(
5454
};
5555
}
5656

57+
export type RunnerSeccompProfileOptions = {
58+
profilePath: string;
59+
runtimes: "none" | "node-24-plus" | "all";
60+
runtime: string | null | undefined;
61+
checkpointsEnabled: boolean | undefined;
62+
};
63+
5764
/**
58-
* Applies the runner seccomp profile. The profile is a node-local file installed
59-
* outside this repo, so an empty path leaves the pod on the runtime default -
60-
* pointing at a profile the nodes don't have fails pod creation.
65+
* Applies the runner seccomp profile, which is a node-local file installed outside
66+
* this repo - pointing a pod at a profile its node doesn't have fails pod creation,
67+
* so every condition for skipping it lives here.
68+
*
69+
* "node-24-plus" matches the original rollout: node >= 24 always creates io_uring
70+
* fds, which can't be checkpointed, and blocking io_uring_setup makes libuv fall
71+
* back to epoll. Tolerates an "experimental-" prefix. "bun" matches only under "all".
6172
*/
6273
export function withRunnerSeccompProfile(
6374
podSpec: Omit<k8s.V1PodSpec, "containers">,
64-
profilePath: string | undefined
75+
options: RunnerSeccompProfileOptions
6576
): Omit<k8s.V1PodSpec, "containers"> {
66-
if (!profilePath) {
77+
if (!options.checkpointsEnabled || options.runtimes === "none") {
6778
return podSpec;
6879
}
6980

81+
if (options.runtimes === "node-24-plus") {
82+
const match = options.runtime ? /^(?:experimental-)?node-(\d+)$/.exec(options.runtime) : null;
83+
if (!match || Number(match[1]) < 24) {
84+
return podSpec;
85+
}
86+
}
87+
7088
return {
7189
...podSpec,
7290
securityContext: {
7391
...podSpec.securityContext,
7492
seccompProfile: {
7593
type: "Localhost",
76-
localhostProfile: profilePath,
94+
localhostProfile: options.profilePath,
7795
},
7896
},
7997
};

apps/supervisor/src/workloadManager/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ export interface WorkloadManagerCreateOptions {
4242
projectId: string;
4343
deploymentFriendlyId: string;
4444
deploymentVersion: string;
45+
// Canonical runtime identifier (e.g. "node", "node-22", "node-24")
46+
runtime?: string;
4547
// When set, overrides the TRIGGER_DEPLOYMENT_ID value the runner forwards as its identity header.
4648
deploymentToken?: string;
4749
runId: string;

0 commit comments

Comments
 (0)