fix(scripts): resolve cgroup-namespace-relative records to their mount

ClawSweeper P1 on d6fe49dd3f: inside a cgroup namespace /proc/self/cgroup
reports the namespace root ("0::/") while mountinfo field 4 stays the host
subtree the cgroupfs was mounted from ("/docker/<id>"). relativeCgroupPath then
found no prefix match and returned null; because a memory record had already
been seen, the root probe was skipped and the build fell back to host MemTotal.
A constrained container therefore missed its own budget entirely.

That namespace root is exactly what the mount exposes at its mount point, so it
resolves to "/" rather than failing closed.

Regression test fails pre-fix: a "0::/" record against a /docker/2f1a9c mount
root with a 5 GiB memory.max yields --max-old-space-size=12288 before the fix
and 4352 after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jesse Merhi
2026-08-18 00:38:04 +10:00
parent d6fe49dd3f
commit b4d200c5d2
2 changed files with 38 additions and 0 deletions
+6
View File
@@ -539,6 +539,12 @@ 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.
if (cgroupPath === "/") {
return "/";
}
if (cgroupPath === mountRoot) {
return "/";
}
+32
View File
@@ -453,6 +453,38 @@ describe("resolveTsdownBuildInvocation", () => {
expect(result.options.env.NODE_OPTIONS).toBe("--max-old-space-size=4352");
});
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
// host memory, which is the opposite of what a constrained container needs.
const cgroupFiles = new Map([
["/proc/self/cgroup", "0::/\n"],
[
"/proc/self/mountinfo",
"30 25 0:26 /docker/2f1a9c /sys/fs/cgroup rw,nosuid - cgroup2 cgroup2 rw\n",
],
["/sys/fs/cgroup/memory.max", `${5 * 1024 * 1024 * 1024}\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;
},
},
});
// 5 GiB container budget minus the 768 MiB build headroom.
expect(result.options.env.NODE_OPTIONS).toBe("--max-old-space-size=4352");
});
it("caps the tsdown heap when the cgroup mount point is octal-escaped in mountinfo", () => {
const slicePath = "/user.slice/user-999.slice/user@999.service";
// The kernel escapes a space in the mount point as \040. Matching the field