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