fix(scripts): reject inherited cgroup mount views instead of guessing

ClawSweeper P1 on b4d200c5d2: the previous commit resolved a namespace-relative
record against any mount root, including the inherited views cgroup_namespaces(7)
documents, whose field-4 root reads "/..". Which cgroup such a view exposes is not
derivable from mountinfo, so probing it can size the build from an unrelated
cgroup's limit.

Reject non-canonical mount roots outright. An undecidable view now falls back to
host sizing, which is current main's behavior, rather than silently adopting the
wrong budget.

Regression test covers the "/.." inherited mount: it must yield host MemTotal
sizing, not the 5 GiB limit sitting behind that mount.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jesse Merhi
2026-08-18 00:51:42 +10:00
parent b4d200c5d2
commit 731d3bbc8e
2 changed files with 39 additions and 3 deletions
+9 -3
View File
@@ -539,9 +539,15 @@ function relativeCgroupPath(mountRoot: string, cgroupPath: string) {
if (mountRoot === "/") {
return cgroupPath;
}
// Inside a cgroup namespace the record is namespace-relative and reads "/", while the
// mount root stays the host subtree it was mounted from. That namespace root is exactly
// what this mount exposes at its mount point, so it resolves rather than failing closed.
// cgroup_namespaces(7): a mount inherited across a namespace can report a field-4 root of
// "/.." or another non-canonical path. Which cgroup such a view exposes is not derivable
// here, and guessing would size the build from an unrelated cgroup's limit.
if (mountRoot.split("/").includes("..")) {
return null;
}
// Otherwise a namespace-relative record reads "/" while the mount root stays the host
// subtree it was mounted from. That namespace root is what this mount exposes at its
// mount point, so it resolves rather than failing closed.
if (cgroupPath === "/") {
return "/";
}
+30
View File
@@ -453,6 +453,36 @@ describe("resolveTsdownBuildInvocation", () => {
expect(result.options.env.NODE_OPTIONS).toBe("--max-old-space-size=4352");
});
it("ignores an inherited namespace mount whose root is not derivable", () => {
// cgroup_namespaces(7): an inherited mount can report a field-4 root of "/..". Which
// cgroup it exposes is not derivable, so it must not be probed as if it were the
// process's own; the resolver falls back rather than sizing from an unrelated limit.
const cgroupFiles = new Map([
["/proc/self/cgroup", "0::/\n"],
["/proc/self/mountinfo", "30 25 0:26 /.. /sys/fs/cgroup rw,nosuid - cgroup2 cgroup2 rw\n"],
["/sys/fs/cgroup/memory.max", `${5 * 1024 * 1024 * 1024}\n`],
["/proc/meminfo", `MemTotal: ${16 * 1024 * 1024} kB\n`],
]);
const result = resolveTsdownBuildInvocation({
nodeExecPath: "/usr/bin/node",
npmExecPath: "/tmp/pnpm.cjs",
env: {},
fs: {
readFileSync(filePath: string) {
const contents = cgroupFiles.get(filePath);
if (contents === undefined) {
throw new Error(`ENOENT: ${filePath}`);
}
return contents;
},
},
});
// Host MemTotal sizing, not the unrelated 5 GiB limit behind the inherited mount.
expect(result.options.env.NODE_OPTIONS).toBe("--max-old-space-size=12288");
});
it("caps the tsdown heap from a cgroup-namespace-relative record", () => {
// Inside a container the record is namespace-relative ("/") while the mount root stays
// the host subtree. Failing to resolve that pair skips the limit and falls back to