From b4d200c5d208a7cf2b9c10ce3604e7941a037310 Mon Sep 17 00:00:00 2001 From: Jesse Merhi <79823012+jesse-merhi@users.noreply.github.com> Date: Tue, 18 Aug 2026 00:38:04 +1000 Subject: [PATCH] fix(scripts): resolve cgroup-namespace-relative records to their mount ClawSweeper P1 on d6fe49dd3f4: 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/"). 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) --- scripts/tsdown-build.mts | 6 ++++++ test/scripts/tsdown-build.test.ts | 32 +++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/scripts/tsdown-build.mts b/scripts/tsdown-build.mts index c621dae0faad..e987782be503 100644 --- a/scripts/tsdown-build.mts +++ b/scripts/tsdown-build.mts @@ -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 "/"; } diff --git a/test/scripts/tsdown-build.test.ts b/test/scripts/tsdown-build.test.ts index 99a6acc291e7..5933afa3326b 100644 --- a/test/scripts/tsdown-build.test.ts +++ b/test/scripts/tsdown-build.test.ts @@ -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