@exercode/problem-utils 2.3.1 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -0
- package/dist/cli/checkAllProblems.cjs +1 -1
- package/dist/cli/checkAllProblems.js +1 -1
- package/dist/cli/runSingleHarness.cjs +1 -1
- package/dist/cli/runSingleHarness.js +1 -1
- package/dist/helpers/cleanWorkingDirectory.cjs +1 -1
- package/dist/helpers/cleanWorkingDirectory.js +1 -1
- package/dist/helpers/compareExpectedOutputFiles.cjs +2 -0
- package/dist/helpers/compareExpectedOutputFiles.cjs.map +1 -0
- package/dist/helpers/compareExpectedOutputFiles.d.ts +45 -0
- package/dist/helpers/compareExpectedOutputFiles.js +2 -0
- package/dist/helpers/compareExpectedOutputFiles.js.map +1 -0
- package/dist/helpers/compareStdoutAsSpaceSeparatedTokens.cjs +1 -1
- package/dist/helpers/compareStdoutAsSpaceSeparatedTokens.cjs.map +1 -1
- package/dist/helpers/compareStdoutAsSpaceSeparatedTokens.js +1 -1
- package/dist/helpers/compareStdoutAsSpaceSeparatedTokens.js.map +1 -1
- package/dist/helpers/defaultStdioHarness.cjs +1 -1
- package/dist/helpers/defaultStdioHarness.js +1 -1
- package/dist/helpers/findModelAnswerDirs.cjs +1 -1
- package/dist/helpers/findModelAnswerDirs.js +1 -1
- package/dist/helpers/judgeByStaticAnalysis.cjs +1 -1
- package/dist/helpers/judgeByStaticAnalysis.js +1 -1
- package/dist/helpers/readOutputFiles.cjs +1 -1
- package/dist/helpers/readOutputFiles.js +1 -1
- package/dist/helpers/readProblemMarkdownFrontMatter.cjs +1 -1
- package/dist/helpers/readProblemMarkdownFrontMatter.js +1 -1
- package/dist/helpers/readTestCases.cjs +1 -1
- package/dist/helpers/readTestCases.cjs.map +1 -1
- package/dist/helpers/readTestCases.d.ts +6 -0
- package/dist/helpers/readTestCases.js +1 -1
- package/dist/helpers/readTestCases.js.map +1 -1
- package/dist/helpers/runCommandInTemporaryPackageManagerProject.cjs +1 -1
- package/dist/helpers/runCommandInTemporaryPackageManagerProject.js +1 -1
- package/dist/helpers/runCommandInTemporaryPackageManagerProject.js.map +1 -1
- package/dist/helpers/safeFs.cjs +1 -1
- package/dist/helpers/safeFs.js +1 -1
- package/dist/helpers/sandboxUser.cjs +2 -2
- package/dist/helpers/sandboxUser.cjs.map +1 -1
- package/dist/helpers/sandboxUser.d.ts +5 -0
- package/dist/helpers/sandboxUser.js +2 -2
- package/dist/helpers/sandboxUser.js.map +1 -1
- package/dist/helpers/startHttpServer.cjs +1 -1
- package/dist/helpers/startHttpServer.js +1 -1
- package/dist/helpers/temporaryProblemDirCopy.cjs +1 -1
- package/dist/helpers/temporaryProblemDirCopy.cjs.map +1 -1
- package/dist/helpers/temporaryProblemDirCopy.d.ts +4 -1
- package/dist/helpers/temporaryProblemDirCopy.js +1 -1
- package/dist/helpers/temporaryProblemDirCopy.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -1
- package/dist/presets/command.cjs +1 -1
- package/dist/presets/command.cjs.map +1 -1
- package/dist/presets/command.d.ts +35 -3
- package/dist/presets/command.js +1 -1
- package/dist/presets/command.js.map +1 -1
- package/dist/presets/evaluation.cjs +3 -3
- package/dist/presets/evaluation.js +3 -3
- package/dist/presets/guiCommand.cjs +3 -3
- package/dist/presets/guiCommand.cjs.map +1 -1
- package/dist/presets/guiCommand.d.ts +14 -2
- package/dist/presets/guiCommand.js +3 -3
- package/dist/presets/guiCommand.js.map +1 -1
- package/dist/presets/llm.cjs +1 -1
- package/dist/presets/llm.cjs.map +1 -1
- package/dist/presets/llm.d.ts +12 -10
- package/dist/presets/llm.js +1 -1
- package/dist/presets/llm.js.map +1 -1
- package/dist/presets/stdio.cjs +1 -1
- package/dist/presets/stdio.cjs.map +1 -1
- package/dist/presets/stdio.d.ts +3 -1
- package/dist/presets/stdio.js +1 -1
- package/dist/presets/stdio.js.map +1 -1
- package/dist/types/language.cjs +2 -2
- package/dist/types/language.js +2 -2
- package/package.json +1 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{forceRemove as e,getSandboxUserEnvOverrides as t,killSandboxUserProcesses as n,makeAccessibleToSandboxUser as r,prependInsideSandboxWrapper as i,sandboxUserName as a,startSandboxTimeoutWatchdog as o,wrapCommandWithSandboxUser as s}from"./sandboxUser.js";import{copyWithoutFollowingSymlinks as c,createDirectoryWithoutFollowingSymlinks as l}from"./safeFs.js";import u from"node:fs
|
|
1
|
+
import{forceRemove as e,getSandboxUserEnvOverrides as t,killSandboxUserProcesses as n,makeAccessibleToSandboxUser as r,prependInsideSandboxWrapper as i,sandboxUserName as a,startSandboxTimeoutWatchdog as o,wrapCommandWithSandboxUser as s}from"./sandboxUser.js";import{copyWithoutFollowingSymlinks as c,createDirectoryWithoutFollowingSymlinks as l}from"./safeFs.js";import u from"node:fs";import d from"node:path";import f from"node:child_process";import p from"node:os";import m from"node:fs/promises";const h={bun:[`package.json`,`bun.lock`,`bun.lockb`],cargo:[`Cargo.toml`,`Cargo.lock`],go:[`go.mod`,`go.sum`],gradle:[`build.gradle`,`build.gradle.kts`,`settings.gradle`,`settings.gradle.kts`,`gradle.properties`,`gradle.lockfile`,`buildscript-gradle.lockfile`,`gradle`,`gradlew`,`gradlew.bat`],maven:[`pom.xml`,`.mvn`,`mvnw`,`mvnw.cmd`],npm:[`package.json`,`package-lock.json`],pnpm:[`package.json`,`pnpm-lock.yaml`,`pnpm-workspace.yaml`],ruby:[`Gemfile`,`Gemfile.lock`,`.ruby-version`],uv:[`pyproject.toml`,`uv.lock`],yarn:[`package.json`,`yarn.lock`,`.yarnrc`,`.yarnrc.yml`,`.yarn`]},g={bun:S,cargo:C,go:w,gradle:T,maven:E,npm:D,pnpm:O,ruby:k,uv:A,yarn:j},_=z();async function v(t){let i=await m.mkdtemp(d.join(p.tmpdir(),t.tempDirPrefix??`exercode-`));try{await m.cp(t.cwd,i,{recursive:!0}),await I({packageManager:t.packageManager,projectDir:t.projectDir,runDir:i,projectFilePaths:t.projectFilePaths}),r(i);let e=t.env?{...process.env,...t.env}:process.env,n=t.prepareDependencies===!1?void 0:await b(t.packageManager,i),a=typeof t.command==`function`?t.command({runDir:i}):t.command,o=Date.now(),s=t.outputLimitBytes??52428800,c;if(n&&(c=await R(n,{cwd:i,env:e,outputLimitBytes:s,stdin:``,timeLimitSeconds:t.timeLimitSeconds}),x(c)))return y({elapsedTimeSeconds:(Date.now()-o)/1e3,options:t,result:c});let l=t.timeLimitSeconds-(Date.now()-o)/1e3;if(l<=0)return{stdin:t.stdin??``,stdout:c?.stdout??``,stderr:c?.stderr??``,status:0,timeSeconds:t.timeLimitSeconds+.001,memoryBytes:c?.memoryBytes??0,timedOut:!0,signal:c?.signal,outputLimitExceeded:!1};let u=await R(a,{cwd:i,env:e,outputLimitBytes:s,stdin:t.stdin??``,timeLimitSeconds:l}),d=(Date.now()-o)/1e3;return y(c?{elapsedTimeSeconds:d,options:t,result:{...u,timeSeconds:c.timeSeconds+u.timeSeconds,memoryBytes:Math.max(c.memoryBytes,u.memoryBytes)}}:{elapsedTimeSeconds:d,options:t,result:u})}finally{try{a&&n()}finally{await e(i)}}}function y(e){return{stdin:e.options.stdin??``,stdout:e.result.stdout,stderr:e.result.stderr,status:e.result.timedOut||e.result.outputLimitExceeded?0:e.result.status,timeSeconds:e.result.timedOut?e.options.timeLimitSeconds+.001:e.result.timeSeconds||e.elapsedTimeSeconds,memoryBytes:e.result.memoryBytes,timedOut:e.result.timedOut,signal:e.result.signal,outputLimitExceeded:e.result.outputLimitExceeded}}function b(e,t){return g[e](t)}function x(e){return e.status!==0||e.timedOut||e.outputLimitExceeded}async function S(e){if(await P(d.join(e,`package.json`)))return await N(e,[`bun.lock`,`bun.lockb`])?[`bun`,`install`,`--frozen-lockfile`,`--silent`]:[`bun`,`install`,`--silent`]}async function C(e){if(await P(d.join(e,`Cargo.toml`)))return await P(d.join(e,`Cargo.lock`))?[`cargo`,`fetch`,`--locked`]:[`cargo`,`fetch`]}async function w(e){if(await P(d.join(e,`go.mod`)))return[`go`,`mod`,`download`]}async function T(e){if(!await N(e,[`build.gradle`,`build.gradle.kts`,`settings.gradle`,`settings.gradle.kts`,`gradlew`,`gradlew.bat`]))return;let t=[`--no-daemon`,`--quiet`,`dependencies`];return process.platform===`win32`?await P(d.join(e,`gradlew.bat`))?[`cmd.exe`,`/c`,`gradlew.bat`,...t]:[`gradle`,...t]:await P(d.join(e,`gradlew`))?[`sh`,`./gradlew`,...t]:[`gradle`,...t]}async function E(e){if(!await P(d.join(e,`pom.xml`)))return;let t=[`-q`,`dependency:go-offline`];return process.platform===`win32`?await P(d.join(e,`mvnw.cmd`))?[`cmd.exe`,`/c`,`mvnw.cmd`,...t]:[`mvn`,...t]:await P(d.join(e,`mvnw`))?[`sh`,`./mvnw`,...t]:[`mvn`,...t]}async function D(e){if(await P(d.join(e,`package.json`)))return await P(d.join(e,`package-lock.json`))?[`npm`,`ci`,`--silent`]:[`npm`,`install`,`--silent`]}async function O(e){if(await P(d.join(e,`package.json`)))return await P(d.join(e,`pnpm-lock.yaml`))?[`pnpm`,`install`,`--frozen-lockfile`,`--silent`]:[`pnpm`,`install`,`--silent`]}async function k(e){if(await P(d.join(e,`Gemfile`)))return await P(d.join(e,`Gemfile.lock`))?[`bundle`,`install`,`--frozen`,`--quiet`]:[`bundle`,`install`,`--quiet`]}async function A(){}async function j(e){if(!await P(d.join(e,`package.json`)))return;let t=await M(e),n=await P(d.join(e,`yarn.lock`));return t?n?[`yarn`,`install`,`--immutable`]:[`yarn`,`install`]:n?[`yarn`,`install`,`--frozen-lockfile`,`--silent`]:[`yarn`,`install`,`--silent`]}async function M(e){if(await P(d.join(e,`.yarnrc.yml`)))return!0;let t=await F(d.join(e,`package.json`)),n=typeof t.packageManager==`string`?t.packageManager:void 0,r=/^yarn@(\d+)/.exec(n??``)?.[1];return r!==void 0&&Number(r)>=2}async function N(e,t){for(let n of t)if(await P(d.join(e,n)))return!0;return!1}async function P(e){try{return await m.access(e),!0}catch(e){if((typeof e==`object`&&e&&`code`in e?e.code:void 0)!==`ENOENT`)throw e;return!1}}async function F(e){try{let t=JSON.parse(await m.readFile(e,`utf8`));return typeof t==`object`&&t&&!Array.isArray(t)?t:{}}catch(e){if(e instanceof SyntaxError||(typeof e==`object`&&e&&`code`in e?e.code:void 0)===`ENOENT`)return{};throw e}}async function I(e){for(let t of e.projectFilePaths??h[e.packageManager])await L(d.join(e.projectDir,t),d.join(e.runDir,t),e.runDir)}async function L(e,t,n){try{await m.lstat(e),await l(n,d.dirname(t)),await c(e,t)}catch(e){if((typeof e==`object`&&e&&`code`in e?e.code:void 0)!==`ENOENT`)throw e}}async function R(e,n){let r=_===void 0?void 0:d.join(n.cwd,`.exercode-time-result`),a=s(e),c=_===void 0?a:i(a,[..._,`--output=${r}`]),l=o(n.timeLimitSeconds),u=Date.now()+(Math.ceil(n.timeLimitSeconds)+5)*1e3,p;try{p=f.spawn(c[0],c.slice(1),{cwd:n.cwd,detached:process.platform!==`win32`,env:{...n.env,...t(n.env)},stdio:[`pipe`,`pipe`,`pipe`]})}catch(e){throw l.cancel(),e}let m=[],h=[],g=0,v=!1,y=!1,b,x,S=e=>{try{B(p,e)}catch(e){b??=e instanceof Error?e:Error(String(e)),x?.()}},C=(e,t)=>{if(g>=n.outputLimitBytes){t.byteLength>0&&(y=!0,S(`SIGKILL`));return}let r=n.outputLimitBytes-g,i=t.byteLength>r?t.subarray(0,r):t;e.push(i),g+=i.byteLength,t.byteLength>r&&(y=!0,S(`SIGKILL`))};p.stdout.on(`data`,e=>C(m,e)),p.stderr.on(`data`,e=>C(h,e));let w=setTimeout(()=>{v=!0,S(`SIGTERM`)},n.timeLimitSeconds*1e3),T=setTimeout(()=>{v&&S(`SIGKILL`)},n.timeLimitSeconds*1e3+1e3);T.unref();let E,D=!1,{status:O,signal:k}=await new Promise((e,t)=>{let r=!1,i,a=e=>{r||(i=e,S(`SIGKILL`),p.pid===void 0&&(r=!0,t(e)))};x=()=>{E??=setTimeout(()=>{r||(r=!0,t(b??Error(`failed to terminate the submission`)))},Math.max(0,u-Date.now())+2e3)},p.on(`error`,a),p.stdin.on(`error`,e=>{e.code!==`EPIPE`&&a(e)}),p.on(`close`,(n,a)=>{if(!r){if(r=!0,D=!0,i){t(i);return}e({status:n??void 0,signal:a??void 0})}}),p.stdin.end(n.stdin)}).finally(()=>{clearTimeout(w),clearTimeout(T),E&&clearTimeout(E),(D||!b)&&l.cancel()}),{timeSeconds:A,memoryBytes:j}=r===void 0?{timeSeconds:0,memoryBytes:0}:await H(r);return{stdout:Buffer.concat(m).toString(),stderr:Buffer.concat(h).toString(),status:O,timeSeconds:A,memoryBytes:j,timedOut:v,signal:k,outputLimitExceeded:y}}function z(){let e=p.platform()===`darwin`?`gtime`:`/usr/bin/time`,t=f.spawnSync(e,[`--version`],{stdio:`ignore`});if(!(t.error||t.status!==0))return[e,`--format`,`%e %M`]}function B(e,t){if(e.pid!==void 0){if(a){n([t===`SIGKILL`?`KILL`:`TERM`]);return}try{if(process.platform===`win32`){e.kill(t);return}process.kill(-e.pid,t)}catch(e){let t=typeof e==`object`&&e&&`code`in e?e.code:void 0;if(t!==`ESRCH`&&t!==`EPERM`)throw e}}}function V(e,t){return typeof e==`object`&&!!e&&`code`in e&&e.code===t}async function H(e){let t,n;try{if(n=await m.open(e,u.constants.O_RDONLY|u.constants.O_NOFOLLOW|u.constants.O_NONBLOCK),!(await n.stat()).isFile())throw Error(`${e} is not a regular file`);t=await n.readFile(`utf8`)}catch(t){if(V(t,`ENOENT`))return{timeSeconds:0,memoryBytes:0};throw Error(`failed to read the time measurement at ${e}`,{cause:t})}finally{await n?.close()}let r=/(\d+(?:[.,]\d+)?) (\d+)\s*$/.exec(t);return r?{timeSeconds:Number(r[1].replace(`,`,`.`)),memoryBytes:Number(r[2])*1024}:{timeSeconds:0,memoryBytes:0}}export{I as copyPackageManagerProjectFiles,v as runCommandInTemporaryPackageManagerProject};
|
|
2
2
|
//# sourceMappingURL=runCommandInTemporaryPackageManagerProject.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runCommandInTemporaryPackageManagerProject.js","names":["childProcess","nodeFs"],"sources":["../../src/helpers/runCommandInTemporaryPackageManagerProject.ts"],"sourcesContent":["import childProcess from 'node:child_process';\nimport nodeFs from 'node:fs';\nimport fs from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\n\nimport {\n forceRemove,\n getSandboxUserEnvOverrides,\n killSandboxUserProcesses,\n makeAccessibleToSandboxUser,\n prependInsideSandboxWrapper,\n SANDBOX_WATCHDOG_GRACE_SECONDS,\n sandboxUserName,\n startSandboxTimeoutWatchdog,\n wrapCommandWithSandboxUser,\n} from './sandboxUser.js';\nimport { copyWithoutFollowingSymlinks, createDirectoryWithoutFollowingSymlinks } from './safeFs.js';\n\nexport type PackageManager = 'bun' | 'cargo' | 'go' | 'gradle' | 'maven' | 'npm' | 'pnpm' | 'ruby' | 'uv' | 'yarn';\ntype PackageManagerInstallCommand = readonly [string, ...string[]];\n\nexport interface PackageManagerCommandRunResult {\n stdin: string;\n stdout: string;\n stderr: string;\n status: number | undefined;\n timeSeconds: number;\n memoryBytes: number;\n timedOut: boolean;\n signal: NodeJS.Signals | undefined;\n outputLimitExceeded: boolean;\n}\n\nexport interface RunCommandInTemporaryPackageManagerProjectOptions {\n cwd: string;\n projectDir: string;\n packageManager: PackageManager;\n command: readonly [string, ...string[]] | ((context: { runDir: string }) => readonly [string, ...string[]]);\n /**\n * Set to false when `command` prepares the dependencies it needs.\n * Defaults to true.\n */\n prepareDependencies?: boolean;\n stdin?: string;\n env?: NodeJS.ProcessEnv;\n timeLimitSeconds: number;\n outputLimitBytes?: number;\n tempDirPrefix?: string;\n projectFilePaths?: readonly string[];\n}\n\nconst packageManagerProjectFilePaths = {\n bun: ['package.json', 'bun.lock', 'bun.lockb'],\n cargo: ['Cargo.toml', 'Cargo.lock'],\n go: ['go.mod', 'go.sum'],\n gradle: [\n 'build.gradle',\n 'build.gradle.kts',\n 'settings.gradle',\n 'settings.gradle.kts',\n 'gradle.properties',\n 'gradle.lockfile',\n 'buildscript-gradle.lockfile',\n 'gradle',\n 'gradlew',\n 'gradlew.bat',\n ],\n maven: ['pom.xml', '.mvn', 'mvnw', 'mvnw.cmd'],\n npm: ['package.json', 'package-lock.json'],\n pnpm: ['package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml'],\n ruby: ['Gemfile', 'Gemfile.lock', '.ruby-version'],\n uv: ['pyproject.toml', 'uv.lock'],\n yarn: ['package.json', 'yarn.lock', '.yarnrc', '.yarnrc.yml', '.yarn'],\n} as const satisfies Record<PackageManager, readonly string[]>;\n\nconst packageManagerInstallCommandResolvers = {\n bun: resolveBunInstallCommand,\n cargo: resolveCargoInstallCommand,\n go: resolveGoInstallCommand,\n gradle: resolveGradleInstallCommand,\n maven: resolveMavenInstallCommand,\n npm: resolveNpmInstallCommand,\n pnpm: resolvePnpmInstallCommand,\n ruby: resolveRubyInstallCommand,\n uv: resolveUvInstallCommand,\n yarn: resolveYarnInstallCommand,\n} as const satisfies Record<PackageManager, (runDir: string) => Promise<PackageManagerInstallCommand | undefined>>;\n\nconst defaultOutputLimitBytes = 50 * 1024 * 1024;\nconst killGracePeriodMilliseconds = 1000;\n// Added to the watchdog's own deadline so its kill has time to land and `close` to arrive.\nconst watchdogSettleMarginMilliseconds = 2000;\nconst timeCommand = resolveTimeCommand();\n\n/**\n * Copies a submission directory to a temporary directory, overlays package\n * manager project files from the problem directory, prepares dependencies,\n * runs a command, and then removes the temporary directory.\n *\n * Under `EXERCODE_SANDBOX_USER` delegation, cleanup kills every process of the sandbox user, so do\n * not run multiple invocations concurrently in that mode — one finishing would kill the others.\n */\nexport async function runCommandInTemporaryPackageManagerProject(\n options: RunCommandInTemporaryPackageManagerProjectOptions\n): Promise<PackageManagerCommandRunResult> {\n const runDir = await fs.mkdtemp(path.join(os.tmpdir(), options.tempDirPrefix ?? 'exercode-'));\n try {\n await fs.cp(options.cwd, runDir, { recursive: true });\n await copyPackageManagerProjectFiles({\n packageManager: options.packageManager,\n projectDir: options.projectDir,\n runDir,\n projectFilePaths: options.projectFilePaths,\n });\n // The sandbox user (if any) runs the install/run commands below and must read the copied\n // sources and create outputs (e.g. `node_modules`, `.venv`) next to them.\n makeAccessibleToSandboxUser(runDir);\n\n const env = options.env ? { ...process.env, ...options.env } : process.env;\n const installCommand =\n options.prepareDependencies === false ? undefined : await resolveInstallCommand(options.packageManager, runDir);\n const command = typeof options.command === 'function' ? options.command({ runDir }) : options.command;\n const startedAt = Date.now();\n const outputLimitBytes = options.outputLimitBytes ?? defaultOutputLimitBytes;\n let installResult: Awaited<ReturnType<typeof spawnWithInput>> | undefined;\n\n if (installCommand) {\n installResult = await spawnWithInput(installCommand, {\n cwd: runDir,\n env,\n outputLimitBytes,\n stdin: '',\n timeLimitSeconds: options.timeLimitSeconds,\n });\n if (isFailedSpawnResult(installResult)) {\n return toPackageManagerCommandRunResult({\n elapsedTimeSeconds: (Date.now() - startedAt) / 1000,\n options,\n result: installResult,\n });\n }\n }\n\n const remainingTimeLimitSeconds = options.timeLimitSeconds - (Date.now() - startedAt) / 1000;\n if (remainingTimeLimitSeconds <= 0) {\n return {\n stdin: options.stdin ?? '',\n stdout: installResult?.stdout ?? '',\n stderr: installResult?.stderr ?? '',\n status: 0,\n timeSeconds: options.timeLimitSeconds + 1e-3,\n memoryBytes: installResult?.memoryBytes ?? 0,\n timedOut: true,\n signal: installResult?.signal,\n outputLimitExceeded: false,\n };\n }\n\n const result = await spawnWithInput(command, {\n cwd: runDir,\n env,\n outputLimitBytes,\n stdin: options.stdin ?? '',\n timeLimitSeconds: remainingTimeLimitSeconds,\n });\n const elapsedTimeSeconds = (Date.now() - startedAt) / 1000;\n\n if (installResult) {\n return toPackageManagerCommandRunResult({\n elapsedTimeSeconds,\n options,\n result: {\n ...result,\n timeSeconds: installResult.timeSeconds + result.timeSeconds,\n memoryBytes: Math.max(installResult.memoryBytes, result.memoryBytes),\n },\n });\n }\n\n return toPackageManagerCommandRunResult({ elapsedTimeSeconds, options, result });\n } finally {\n try {\n // Daemonized children of the sandboxed command would otherwise outlive the run.\n if (sandboxUserName) killSandboxUserProcesses();\n } finally {\n // Must run even when the sweep fails closed, or the temporary submission copy leaks.\n await forceRemove(runDir);\n }\n }\n}\n\nfunction toPackageManagerCommandRunResult(context: {\n elapsedTimeSeconds: number;\n options: RunCommandInTemporaryPackageManagerProjectOptions;\n result: Awaited<ReturnType<typeof spawnWithInput>>;\n}): PackageManagerCommandRunResult {\n return {\n stdin: context.options.stdin ?? '',\n stdout: context.result.stdout,\n stderr: context.result.stderr,\n status: context.result.timedOut || context.result.outputLimitExceeded ? 0 : context.result.status,\n timeSeconds: context.result.timedOut\n ? context.options.timeLimitSeconds + 1e-3\n : context.result.timeSeconds || context.elapsedTimeSeconds,\n memoryBytes: context.result.memoryBytes,\n timedOut: context.result.timedOut,\n signal: context.result.signal,\n outputLimitExceeded: context.result.outputLimitExceeded,\n };\n}\n\nfunction resolveInstallCommand(\n packageManager: PackageManager,\n runDir: string\n): Promise<PackageManagerInstallCommand | undefined> {\n return packageManagerInstallCommandResolvers[packageManager](runDir);\n}\n\nfunction isFailedSpawnResult(result: Awaited<ReturnType<typeof spawnWithInput>>): boolean {\n return result.status !== 0 || result.timedOut || result.outputLimitExceeded;\n}\n\nasync function resolveBunInstallCommand(runDir: string): Promise<PackageManagerInstallCommand | undefined> {\n if (!(await pathExists(path.join(runDir, 'package.json')))) return undefined;\n // Bun supports --silent and it keeps successful preparation output out of judge output buffers.\n return (await hasAnyPath(runDir, ['bun.lock', 'bun.lockb']))\n ? ['bun', 'install', '--frozen-lockfile', '--silent']\n : ['bun', 'install', '--silent'];\n}\n\nasync function resolveCargoInstallCommand(runDir: string): Promise<PackageManagerInstallCommand | undefined> {\n if (!(await pathExists(path.join(runDir, 'Cargo.toml')))) return undefined;\n return (await pathExists(path.join(runDir, 'Cargo.lock'))) ? ['cargo', 'fetch', '--locked'] : ['cargo', 'fetch'];\n}\n\nasync function resolveGoInstallCommand(runDir: string): Promise<PackageManagerInstallCommand | undefined> {\n if (!(await pathExists(path.join(runDir, 'go.mod')))) return undefined;\n return ['go', 'mod', 'download'];\n}\n\nasync function resolveGradleInstallCommand(runDir: string): Promise<PackageManagerInstallCommand | undefined> {\n if (\n !(await hasAnyPath(runDir, [\n 'build.gradle',\n 'build.gradle.kts',\n 'settings.gradle',\n 'settings.gradle.kts',\n 'gradlew',\n 'gradlew.bat',\n ]))\n )\n return undefined;\n const args = ['--no-daemon', '--quiet', 'dependencies'] as const;\n if (process.platform === 'win32') {\n return (await pathExists(path.join(runDir, 'gradlew.bat')))\n ? ['cmd.exe', '/c', 'gradlew.bat', ...args]\n : ['gradle', ...args];\n }\n return (await pathExists(path.join(runDir, 'gradlew'))) ? ['sh', './gradlew', ...args] : ['gradle', ...args];\n}\n\nasync function resolveMavenInstallCommand(runDir: string): Promise<PackageManagerInstallCommand | undefined> {\n if (!(await pathExists(path.join(runDir, 'pom.xml')))) return undefined;\n const args = ['-q', 'dependency:go-offline'] as const;\n if (process.platform === 'win32') {\n return (await pathExists(path.join(runDir, 'mvnw.cmd')))\n ? ['cmd.exe', '/c', 'mvnw.cmd', ...args]\n : ['mvn', ...args];\n }\n return (await pathExists(path.join(runDir, 'mvnw'))) ? ['sh', './mvnw', ...args] : ['mvn', ...args];\n}\n\nasync function resolveNpmInstallCommand(runDir: string): Promise<PackageManagerInstallCommand | undefined> {\n if (!(await pathExists(path.join(runDir, 'package.json')))) return undefined;\n return (await pathExists(path.join(runDir, 'package-lock.json')))\n ? ['npm', 'ci', '--silent']\n : ['npm', 'install', '--silent'];\n}\n\nasync function resolvePnpmInstallCommand(runDir: string): Promise<PackageManagerInstallCommand | undefined> {\n if (!(await pathExists(path.join(runDir, 'package.json')))) return undefined;\n return (await pathExists(path.join(runDir, 'pnpm-lock.yaml')))\n ? ['pnpm', 'install', '--frozen-lockfile', '--silent']\n : ['pnpm', 'install', '--silent'];\n}\n\nasync function resolveRubyInstallCommand(runDir: string): Promise<PackageManagerInstallCommand | undefined> {\n if (!(await pathExists(path.join(runDir, 'Gemfile')))) return undefined;\n return (await pathExists(path.join(runDir, 'Gemfile.lock')))\n ? ['bundle', 'install', '--frozen', '--quiet']\n : ['bundle', 'install', '--quiet'];\n}\n\nasync function resolveUvInstallCommand(): Promise<undefined> {\n return undefined;\n}\n\nasync function resolveYarnInstallCommand(runDir: string): Promise<PackageManagerInstallCommand | undefined> {\n if (!(await pathExists(path.join(runDir, 'package.json')))) return undefined;\n const isBerry = await isYarnBerryProject(runDir);\n const hasLockfile = await pathExists(path.join(runDir, 'yarn.lock'));\n if (isBerry) return hasLockfile ? ['yarn', 'install', '--immutable'] : ['yarn', 'install'];\n return hasLockfile ? ['yarn', 'install', '--frozen-lockfile', '--silent'] : ['yarn', 'install', '--silent'];\n}\n\nasync function isYarnBerryProject(runDir: string): Promise<boolean> {\n if (await pathExists(path.join(runDir, '.yarnrc.yml'))) return true;\n\n const packageJson = await readJson(path.join(runDir, 'package.json'));\n const packageManager = typeof packageJson.packageManager === 'string' ? packageJson.packageManager : undefined;\n const yarnMajorVersion = /^yarn@(\\d+)/.exec(packageManager ?? '')?.[1];\n return yarnMajorVersion !== undefined && Number(yarnMajorVersion) >= 2;\n}\n\nasync function hasAnyPath(directoryPath: string, relativePaths: readonly string[]): Promise<boolean> {\n for (const relativePath of relativePaths) {\n if (await pathExists(path.join(directoryPath, relativePath))) return true;\n }\n return false;\n}\n\nasync function pathExists(filePath: string): Promise<boolean> {\n try {\n await fs.access(filePath);\n return true;\n } catch (error) {\n const code =\n typeof error === 'object' && error !== null && 'code' in error ? (error as { code: unknown }).code : undefined;\n if (code !== 'ENOENT') throw error;\n return false;\n }\n}\n\nasync function readJson(filePath: string): Promise<Record<string, unknown>> {\n try {\n const parsed = JSON.parse(await fs.readFile(filePath, 'utf8')) as unknown;\n return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {};\n } catch (error) {\n if (error instanceof SyntaxError) return {};\n const code =\n typeof error === 'object' && error !== null && 'code' in error ? (error as { code: unknown }).code : undefined;\n if (code === 'ENOENT') return {};\n throw error;\n }\n}\n\nexport async function copyPackageManagerProjectFiles(options: {\n packageManager: PackageManager;\n projectDir: string;\n runDir: string;\n projectFilePaths?: readonly string[];\n}): Promise<void> {\n for (const projectFilePath of options.projectFilePaths ?? packageManagerProjectFilePaths[options.packageManager]) {\n await copyPathIfExists(\n path.join(options.projectDir, projectFilePath),\n path.join(options.runDir, projectFilePath),\n options.runDir\n );\n }\n}\n\nasync function copyPathIfExists(sourcePath: string, destinationPath: string, runDir: string): Promise<void> {\n try {\n // Before touching the destination: creating the parent levels replaces whatever the submission\n // put there, which must not happen for a project file the problem does not even ship.\n await fs.lstat(sourcePath);\n // No-follow copy: the destination was seeded from the (sandbox-writable) submission tree, so a\n // planted symlink there — at the destination itself or at any directory level of a nested\n // project file path — must not redirect this trusted project-file overlay outside runDir.\n await createDirectoryWithoutFollowingSymlinks(runDir, path.dirname(destinationPath));\n await copyWithoutFollowingSymlinks(sourcePath, destinationPath);\n } catch (error) {\n const code =\n typeof error === 'object' && error !== null && 'code' in error ? (error as { code: unknown }).code : undefined;\n if (code !== 'ENOENT') throw error;\n }\n}\n\nasync function spawnWithInput(\n command: readonly [string, ...string[]],\n context: {\n cwd: string;\n env: NodeJS.ProcessEnv;\n outputLimitBytes: number;\n stdin: string;\n timeLimitSeconds: number;\n }\n): Promise<{\n stdout: string;\n stderr: string;\n status: number | undefined;\n timeSeconds: number;\n memoryBytes: number;\n timedOut: boolean;\n signal: NodeJS.Signals | undefined;\n outputLimitExceeded: boolean;\n}> {\n const timeOutputPath = timeCommand === undefined ? undefined : path.join(context.cwd, '.exercode-time-result');\n // `wrapCommandWithSandboxUser` is idempotent, so a command the presets already wrapped is not\n // nested; `time` is then spliced INSIDE that wrapper so it runs as the sandbox user, never as the\n // harness user writing `--output` into this sandbox-writable directory.\n const wrappedCommand = wrapCommandWithSandboxUser(command);\n const spawnedCommand =\n timeCommand === undefined\n ? wrappedCommand\n : prependInsideSandboxWrapper(wrappedCommand, [...timeCommand, `--output=${timeOutputPath}`]);\n // Started BEFORE the submission: `killSubprocessGroup` cannot spawn its `sudo` once a submission\n // has exhausted the PID cgroup, and a submission spawned first could exhaust it before this\n // watchdog exists. It is the deadline of last resort on this path.\n const watchdog = startSandboxTimeoutWatchdog(context.timeLimitSeconds);\n const watchdogDeadlineAt = Date.now() + (Math.ceil(context.timeLimitSeconds) + SANDBOX_WATCHDOG_GRACE_SECONDS) * 1000;\n let subprocess: childProcess.ChildProcessWithoutNullStreams;\n try {\n subprocess = childProcess.spawn(spawnedCommand[0], spawnedCommand.slice(1), {\n cwd: context.cwd,\n detached: process.platform !== 'win32',\n env: { ...context.env, ...getSandboxUserEnvOverrides(context.env) },\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n } catch (error) {\n watchdog.cancel();\n throw error;\n }\n\n const stdoutChunks: Buffer[] = [];\n const stderrChunks: Buffer[] = [];\n let outputBytes = 0;\n let timedOut = false;\n let outputLimitExceeded = false;\n // Set when terminating the submission failed (the cleanup `sudo` could not be spawned). The run\n // then waits for the watchdog to end the submission rather than throwing out of an EventEmitter\n // callback, which would be an uncaught exception that kills the harness before any cleanup.\n let killError: Error | undefined;\n let onKillFailure: (() => void) | undefined;\n const killOrRecordFailure = (signal: NodeJS.Signals): void => {\n try {\n killSubprocessGroup(subprocess, signal);\n } catch (error) {\n killError ??= error instanceof Error ? error : new Error(String(error));\n onKillFailure?.();\n }\n };\n\n const appendOutputChunk = (chunks: Buffer[], chunk: Buffer): void => {\n if (outputBytes >= context.outputLimitBytes) {\n if (chunk.byteLength > 0) {\n outputLimitExceeded = true;\n killOrRecordFailure('SIGKILL');\n }\n return;\n }\n\n const remainingBytes = context.outputLimitBytes - outputBytes;\n const appendedChunk = chunk.byteLength > remainingBytes ? chunk.subarray(0, remainingBytes) : chunk;\n chunks.push(appendedChunk);\n outputBytes += appendedChunk.byteLength;\n\n if (chunk.byteLength > remainingBytes) {\n outputLimitExceeded = true;\n killOrRecordFailure('SIGKILL');\n }\n };\n\n subprocess.stdout.on('data', (chunk: Buffer) => appendOutputChunk(stdoutChunks, chunk));\n subprocess.stderr.on('data', (chunk: Buffer) => appendOutputChunk(stderrChunks, chunk));\n\n const timeout = setTimeout(() => {\n timedOut = true;\n killOrRecordFailure('SIGTERM');\n }, context.timeLimitSeconds * 1000);\n const killTimeout = setTimeout(\n () => {\n if (timedOut) killOrRecordFailure('SIGKILL');\n },\n context.timeLimitSeconds * 1000 + killGracePeriodMilliseconds\n );\n killTimeout.unref();\n // Bounds the wait after a kill failure: the watchdog force-kills the sandbox user at its own\n // deadline, so `close` should arrive; if even that fails, give up rather than hang.\n let killFailureTimeout: ReturnType<typeof setTimeout> | undefined;\n let closeObserved = false;\n\n const { status, signal } = await new Promise<{ status: number | undefined; signal: NodeJS.Signals | undefined }>(\n (resolve, reject) => {\n let settled = false;\n let pendingError: Error | undefined;\n const failAfterClose = (error: Error): void => {\n if (settled) return;\n pendingError = error;\n killOrRecordFailure('SIGKILL');\n if (subprocess.pid === undefined) {\n settled = true;\n reject(error);\n }\n };\n // Terminating the submission failed. Keep waiting: the watchdog (started before the\n // submission) force-kills the sandbox user at its own deadline, so `close` still arrives and\n // the run reports its real verdict. Give up only once that deadline has passed without the\n // submission ending, so the run can neither hang nor abandon a still-running submission\n // while its watchdog could still fire.\n onKillFailure = () => {\n killFailureTimeout ??= setTimeout(\n () => {\n if (settled) return;\n settled = true;\n reject(killError ?? new Error('failed to terminate the submission'));\n },\n Math.max(0, watchdogDeadlineAt - Date.now()) + watchdogSettleMarginMilliseconds\n );\n };\n subprocess.on('error', failAfterClose);\n subprocess.stdin.on('error', (error: NodeJS.ErrnoException) => {\n if (error.code !== 'EPIPE') failAfterClose(error);\n });\n subprocess.on('close', (code, closeSignal) => {\n if (settled) return;\n settled = true;\n // The submission ended, so the watchdog has nothing left to kill and must not outlive this\n // run. A kill that failed once but succeeded on retry (or was superseded by the watchdog)\n // is not an error: the timeout/output-limit verdict below is the correct one to report.\n closeObserved = true;\n if (pendingError) {\n reject(pendingError);\n return;\n }\n resolve({ status: code ?? undefined, signal: closeSignal ?? undefined });\n });\n subprocess.stdin.end(context.stdin);\n }\n ).finally(() => {\n clearTimeout(timeout);\n clearTimeout(killTimeout);\n if (killFailureTimeout) clearTimeout(killFailureTimeout);\n // Cancel unless the submission is still running after a failed kill: the watchdog is then the\n // only remaining mechanism able to stop it, and cancelling would leave it running unchecked.\n if (closeObserved || !killError) watchdog.cancel();\n });\n\n const { timeSeconds, memoryBytes } =\n timeOutputPath === undefined ? { timeSeconds: 0, memoryBytes: 0 } : await readTimeResult(timeOutputPath);\n\n return {\n stdout: Buffer.concat(stdoutChunks).toString(),\n stderr: Buffer.concat(stderrChunks).toString(),\n status,\n timeSeconds,\n memoryBytes,\n timedOut,\n signal,\n outputLimitExceeded,\n };\n}\n\nfunction resolveTimeCommand(): readonly [string, ...string[]] | undefined {\n const command = os.platform() === 'darwin' ? 'gtime' : '/usr/bin/time';\n const result = childProcess.spawnSync(command, ['--version'], { stdio: 'ignore' });\n if (result.error || result.status !== 0) return undefined;\n\n return [command, '--format', '%e %M'];\n}\n\nfunction killSubprocessGroup(subprocess: childProcess.ChildProcess, signal: NodeJS.Signals): void {\n if (subprocess.pid === undefined) return;\n\n // The current user cannot signal the root-owned sudo wrapper nor the sandbox user's processes,\n // so go through sudo with the requested signal only; the callers' existing timers provide the\n // TERM → grace → KILL escalation.\n // Throws when the cleanup `sudo` cannot even be spawned (a submission can exhaust the PID\n // cgroup). Only `killOrRecordFailure` may call this: it records the failure and lets the\n // watchdog (or, past its deadline, a bounded timer) settle the run instead of letting the throw\n // escape an EventEmitter callback.\n if (sandboxUserName) {\n killSandboxUserProcesses([signal === 'SIGKILL' ? 'KILL' : 'TERM']);\n return;\n }\n\n try {\n if (process.platform === 'win32') {\n subprocess.kill(signal);\n return;\n }\n process.kill(-subprocess.pid, signal);\n } catch (error) {\n const code =\n typeof error === 'object' && error !== null && 'code' in error ? (error as { code: unknown }).code : undefined;\n if (code !== 'ESRCH' && code !== 'EPERM') throw error;\n }\n}\n\nfunction isErrorWithCode(error: unknown, code: string): boolean {\n return typeof error === 'object' && error !== null && 'code' in error && (error as { code: unknown }).code === code;\n}\n\nasync function readTimeResult(timeOutputPath: string): Promise<{ timeSeconds: number; memoryBytes: number }> {\n // The submission owns this directory and can put anything at this path, so decide what was\n // opened from the handle itself rather than from a prior `lstat` it could race:\n // - `O_NOFOLLOW` refuses a symlink to a harness-readable file (e.g. the problem's expected\n // outputs), which would otherwise report that file's trailing numbers as the submission's own\n // time and memory usage;\n // - `O_NONBLOCK` keeps a planted FIFO from blocking the harness forever waiting for a writer;\n // - the `fstat` then rejects anything that is not a regular file.\n //\n // Only an absent file means \"no measurement\" (the command was killed before `time` wrote one).\n // Anything else at this path is the submission tampering with its own accounting — reporting zero\n // memory would let it slip past a memory limit — so that fails the run instead.\n let content: string;\n let fileHandle: Awaited<ReturnType<typeof fs.open>> | undefined;\n try {\n fileHandle = await fs.open(\n timeOutputPath,\n nodeFs.constants.O_RDONLY | nodeFs.constants.O_NOFOLLOW | nodeFs.constants.O_NONBLOCK\n );\n const stats = await fileHandle.stat();\n if (!stats.isFile()) throw new Error(`${timeOutputPath} is not a regular file`);\n content = await fileHandle.readFile('utf8');\n } catch (error) {\n if (isErrorWithCode(error, 'ENOENT')) return { timeSeconds: 0, memoryBytes: 0 };\n throw new Error(`failed to read the time measurement at ${timeOutputPath}`, { cause: error });\n } finally {\n await fileHandle?.close();\n }\n\n const match = /(\\d+(?:[.,]\\d+)?) (\\d+)\\s*$/.exec(content);\n if (!match) return { timeSeconds: 0, memoryBytes: 0 };\n\n return { timeSeconds: Number(match[1]!.replace(',', '.')), memoryBytes: Number(match[2]) * 1024 };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"runCommandInTemporaryPackageManagerProject.js","names":["fs","childProcess","nodeFs"],"sources":["../../src/helpers/runCommandInTemporaryPackageManagerProject.ts"],"sourcesContent":["import childProcess from 'node:child_process';\nimport nodeFs from 'node:fs';\nimport fs from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\n\nimport {\n forceRemove,\n getSandboxUserEnvOverrides,\n killSandboxUserProcesses,\n makeAccessibleToSandboxUser,\n prependInsideSandboxWrapper,\n SANDBOX_WATCHDOG_GRACE_SECONDS,\n sandboxUserName,\n startSandboxTimeoutWatchdog,\n wrapCommandWithSandboxUser,\n} from './sandboxUser.js';\nimport { copyWithoutFollowingSymlinks, createDirectoryWithoutFollowingSymlinks } from './safeFs.js';\n\nexport type PackageManager = 'bun' | 'cargo' | 'go' | 'gradle' | 'maven' | 'npm' | 'pnpm' | 'ruby' | 'uv' | 'yarn';\ntype PackageManagerInstallCommand = readonly [string, ...string[]];\n\nexport interface PackageManagerCommandRunResult {\n stdin: string;\n stdout: string;\n stderr: string;\n status: number | undefined;\n timeSeconds: number;\n memoryBytes: number;\n timedOut: boolean;\n signal: NodeJS.Signals | undefined;\n outputLimitExceeded: boolean;\n}\n\nexport interface RunCommandInTemporaryPackageManagerProjectOptions {\n cwd: string;\n projectDir: string;\n packageManager: PackageManager;\n command: readonly [string, ...string[]] | ((context: { runDir: string }) => readonly [string, ...string[]]);\n /**\n * Set to false when `command` prepares the dependencies it needs.\n * Defaults to true.\n */\n prepareDependencies?: boolean;\n stdin?: string;\n env?: NodeJS.ProcessEnv;\n timeLimitSeconds: number;\n outputLimitBytes?: number;\n tempDirPrefix?: string;\n projectFilePaths?: readonly string[];\n}\n\nconst packageManagerProjectFilePaths = {\n bun: ['package.json', 'bun.lock', 'bun.lockb'],\n cargo: ['Cargo.toml', 'Cargo.lock'],\n go: ['go.mod', 'go.sum'],\n gradle: [\n 'build.gradle',\n 'build.gradle.kts',\n 'settings.gradle',\n 'settings.gradle.kts',\n 'gradle.properties',\n 'gradle.lockfile',\n 'buildscript-gradle.lockfile',\n 'gradle',\n 'gradlew',\n 'gradlew.bat',\n ],\n maven: ['pom.xml', '.mvn', 'mvnw', 'mvnw.cmd'],\n npm: ['package.json', 'package-lock.json'],\n pnpm: ['package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml'],\n ruby: ['Gemfile', 'Gemfile.lock', '.ruby-version'],\n uv: ['pyproject.toml', 'uv.lock'],\n yarn: ['package.json', 'yarn.lock', '.yarnrc', '.yarnrc.yml', '.yarn'],\n} as const satisfies Record<PackageManager, readonly string[]>;\n\nconst packageManagerInstallCommandResolvers = {\n bun: resolveBunInstallCommand,\n cargo: resolveCargoInstallCommand,\n go: resolveGoInstallCommand,\n gradle: resolveGradleInstallCommand,\n maven: resolveMavenInstallCommand,\n npm: resolveNpmInstallCommand,\n pnpm: resolvePnpmInstallCommand,\n ruby: resolveRubyInstallCommand,\n uv: resolveUvInstallCommand,\n yarn: resolveYarnInstallCommand,\n} as const satisfies Record<PackageManager, (runDir: string) => Promise<PackageManagerInstallCommand | undefined>>;\n\nconst defaultOutputLimitBytes = 50 * 1024 * 1024;\nconst killGracePeriodMilliseconds = 1000;\n// Added to the watchdog's own deadline so its kill has time to land and `close` to arrive.\nconst watchdogSettleMarginMilliseconds = 2000;\nconst timeCommand = resolveTimeCommand();\n\n/**\n * Copies a submission directory to a temporary directory, overlays package\n * manager project files from the problem directory, prepares dependencies,\n * runs a command, and then removes the temporary directory.\n *\n * Under `EXERCODE_SANDBOX_USER` delegation, cleanup kills every process of the sandbox user, so do\n * not run multiple invocations concurrently in that mode — one finishing would kill the others.\n */\nexport async function runCommandInTemporaryPackageManagerProject(\n options: RunCommandInTemporaryPackageManagerProjectOptions\n): Promise<PackageManagerCommandRunResult> {\n const runDir = await fs.mkdtemp(path.join(os.tmpdir(), options.tempDirPrefix ?? 'exercode-'));\n try {\n await fs.cp(options.cwd, runDir, { recursive: true });\n await copyPackageManagerProjectFiles({\n packageManager: options.packageManager,\n projectDir: options.projectDir,\n runDir,\n projectFilePaths: options.projectFilePaths,\n });\n // The sandbox user (if any) runs the install/run commands below and must read the copied\n // sources and create outputs (e.g. `node_modules`, `.venv`) next to them.\n makeAccessibleToSandboxUser(runDir);\n\n const env = options.env ? { ...process.env, ...options.env } : process.env;\n const installCommand =\n options.prepareDependencies === false ? undefined : await resolveInstallCommand(options.packageManager, runDir);\n const command = typeof options.command === 'function' ? options.command({ runDir }) : options.command;\n const startedAt = Date.now();\n const outputLimitBytes = options.outputLimitBytes ?? defaultOutputLimitBytes;\n let installResult: Awaited<ReturnType<typeof spawnWithInput>> | undefined;\n\n if (installCommand) {\n installResult = await spawnWithInput(installCommand, {\n cwd: runDir,\n env,\n outputLimitBytes,\n stdin: '',\n timeLimitSeconds: options.timeLimitSeconds,\n });\n if (isFailedSpawnResult(installResult)) {\n return toPackageManagerCommandRunResult({\n elapsedTimeSeconds: (Date.now() - startedAt) / 1000,\n options,\n result: installResult,\n });\n }\n }\n\n const remainingTimeLimitSeconds = options.timeLimitSeconds - (Date.now() - startedAt) / 1000;\n if (remainingTimeLimitSeconds <= 0) {\n return {\n stdin: options.stdin ?? '',\n stdout: installResult?.stdout ?? '',\n stderr: installResult?.stderr ?? '',\n status: 0,\n timeSeconds: options.timeLimitSeconds + 1e-3,\n memoryBytes: installResult?.memoryBytes ?? 0,\n timedOut: true,\n signal: installResult?.signal,\n outputLimitExceeded: false,\n };\n }\n\n const result = await spawnWithInput(command, {\n cwd: runDir,\n env,\n outputLimitBytes,\n stdin: options.stdin ?? '',\n timeLimitSeconds: remainingTimeLimitSeconds,\n });\n const elapsedTimeSeconds = (Date.now() - startedAt) / 1000;\n\n if (installResult) {\n return toPackageManagerCommandRunResult({\n elapsedTimeSeconds,\n options,\n result: {\n ...result,\n timeSeconds: installResult.timeSeconds + result.timeSeconds,\n memoryBytes: Math.max(installResult.memoryBytes, result.memoryBytes),\n },\n });\n }\n\n return toPackageManagerCommandRunResult({ elapsedTimeSeconds, options, result });\n } finally {\n try {\n // Daemonized children of the sandboxed command would otherwise outlive the run.\n if (sandboxUserName) killSandboxUserProcesses();\n } finally {\n // Must run even when the sweep fails closed, or the temporary submission copy leaks.\n await forceRemove(runDir);\n }\n }\n}\n\nfunction toPackageManagerCommandRunResult(context: {\n elapsedTimeSeconds: number;\n options: RunCommandInTemporaryPackageManagerProjectOptions;\n result: Awaited<ReturnType<typeof spawnWithInput>>;\n}): PackageManagerCommandRunResult {\n return {\n stdin: context.options.stdin ?? '',\n stdout: context.result.stdout,\n stderr: context.result.stderr,\n status: context.result.timedOut || context.result.outputLimitExceeded ? 0 : context.result.status,\n timeSeconds: context.result.timedOut\n ? context.options.timeLimitSeconds + 1e-3\n : context.result.timeSeconds || context.elapsedTimeSeconds,\n memoryBytes: context.result.memoryBytes,\n timedOut: context.result.timedOut,\n signal: context.result.signal,\n outputLimitExceeded: context.result.outputLimitExceeded,\n };\n}\n\nfunction resolveInstallCommand(\n packageManager: PackageManager,\n runDir: string\n): Promise<PackageManagerInstallCommand | undefined> {\n return packageManagerInstallCommandResolvers[packageManager](runDir);\n}\n\nfunction isFailedSpawnResult(result: Awaited<ReturnType<typeof spawnWithInput>>): boolean {\n return result.status !== 0 || result.timedOut || result.outputLimitExceeded;\n}\n\nasync function resolveBunInstallCommand(runDir: string): Promise<PackageManagerInstallCommand | undefined> {\n if (!(await pathExists(path.join(runDir, 'package.json')))) return undefined;\n // Bun supports --silent and it keeps successful preparation output out of judge output buffers.\n return (await hasAnyPath(runDir, ['bun.lock', 'bun.lockb']))\n ? ['bun', 'install', '--frozen-lockfile', '--silent']\n : ['bun', 'install', '--silent'];\n}\n\nasync function resolveCargoInstallCommand(runDir: string): Promise<PackageManagerInstallCommand | undefined> {\n if (!(await pathExists(path.join(runDir, 'Cargo.toml')))) return undefined;\n return (await pathExists(path.join(runDir, 'Cargo.lock'))) ? ['cargo', 'fetch', '--locked'] : ['cargo', 'fetch'];\n}\n\nasync function resolveGoInstallCommand(runDir: string): Promise<PackageManagerInstallCommand | undefined> {\n if (!(await pathExists(path.join(runDir, 'go.mod')))) return undefined;\n return ['go', 'mod', 'download'];\n}\n\nasync function resolveGradleInstallCommand(runDir: string): Promise<PackageManagerInstallCommand | undefined> {\n if (\n !(await hasAnyPath(runDir, [\n 'build.gradle',\n 'build.gradle.kts',\n 'settings.gradle',\n 'settings.gradle.kts',\n 'gradlew',\n 'gradlew.bat',\n ]))\n )\n return undefined;\n const args = ['--no-daemon', '--quiet', 'dependencies'] as const;\n if (process.platform === 'win32') {\n return (await pathExists(path.join(runDir, 'gradlew.bat')))\n ? ['cmd.exe', '/c', 'gradlew.bat', ...args]\n : ['gradle', ...args];\n }\n return (await pathExists(path.join(runDir, 'gradlew'))) ? ['sh', './gradlew', ...args] : ['gradle', ...args];\n}\n\nasync function resolveMavenInstallCommand(runDir: string): Promise<PackageManagerInstallCommand | undefined> {\n if (!(await pathExists(path.join(runDir, 'pom.xml')))) return undefined;\n const args = ['-q', 'dependency:go-offline'] as const;\n if (process.platform === 'win32') {\n return (await pathExists(path.join(runDir, 'mvnw.cmd')))\n ? ['cmd.exe', '/c', 'mvnw.cmd', ...args]\n : ['mvn', ...args];\n }\n return (await pathExists(path.join(runDir, 'mvnw'))) ? ['sh', './mvnw', ...args] : ['mvn', ...args];\n}\n\nasync function resolveNpmInstallCommand(runDir: string): Promise<PackageManagerInstallCommand | undefined> {\n if (!(await pathExists(path.join(runDir, 'package.json')))) return undefined;\n return (await pathExists(path.join(runDir, 'package-lock.json')))\n ? ['npm', 'ci', '--silent']\n : ['npm', 'install', '--silent'];\n}\n\nasync function resolvePnpmInstallCommand(runDir: string): Promise<PackageManagerInstallCommand | undefined> {\n if (!(await pathExists(path.join(runDir, 'package.json')))) return undefined;\n return (await pathExists(path.join(runDir, 'pnpm-lock.yaml')))\n ? ['pnpm', 'install', '--frozen-lockfile', '--silent']\n : ['pnpm', 'install', '--silent'];\n}\n\nasync function resolveRubyInstallCommand(runDir: string): Promise<PackageManagerInstallCommand | undefined> {\n if (!(await pathExists(path.join(runDir, 'Gemfile')))) return undefined;\n return (await pathExists(path.join(runDir, 'Gemfile.lock')))\n ? ['bundle', 'install', '--frozen', '--quiet']\n : ['bundle', 'install', '--quiet'];\n}\n\nasync function resolveUvInstallCommand(): Promise<undefined> {\n return undefined;\n}\n\nasync function resolveYarnInstallCommand(runDir: string): Promise<PackageManagerInstallCommand | undefined> {\n if (!(await pathExists(path.join(runDir, 'package.json')))) return undefined;\n const isBerry = await isYarnBerryProject(runDir);\n const hasLockfile = await pathExists(path.join(runDir, 'yarn.lock'));\n if (isBerry) return hasLockfile ? ['yarn', 'install', '--immutable'] : ['yarn', 'install'];\n return hasLockfile ? ['yarn', 'install', '--frozen-lockfile', '--silent'] : ['yarn', 'install', '--silent'];\n}\n\nasync function isYarnBerryProject(runDir: string): Promise<boolean> {\n if (await pathExists(path.join(runDir, '.yarnrc.yml'))) return true;\n\n const packageJson = await readJson(path.join(runDir, 'package.json'));\n const packageManager = typeof packageJson.packageManager === 'string' ? packageJson.packageManager : undefined;\n const yarnMajorVersion = /^yarn@(\\d+)/.exec(packageManager ?? '')?.[1];\n return yarnMajorVersion !== undefined && Number(yarnMajorVersion) >= 2;\n}\n\nasync function hasAnyPath(directoryPath: string, relativePaths: readonly string[]): Promise<boolean> {\n for (const relativePath of relativePaths) {\n if (await pathExists(path.join(directoryPath, relativePath))) return true;\n }\n return false;\n}\n\nasync function pathExists(filePath: string): Promise<boolean> {\n try {\n await fs.access(filePath);\n return true;\n } catch (error) {\n const code =\n typeof error === 'object' && error !== null && 'code' in error ? (error as { code: unknown }).code : undefined;\n if (code !== 'ENOENT') throw error;\n return false;\n }\n}\n\nasync function readJson(filePath: string): Promise<Record<string, unknown>> {\n try {\n const parsed = JSON.parse(await fs.readFile(filePath, 'utf8')) as unknown;\n return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {};\n } catch (error) {\n if (error instanceof SyntaxError) return {};\n const code =\n typeof error === 'object' && error !== null && 'code' in error ? (error as { code: unknown }).code : undefined;\n if (code === 'ENOENT') return {};\n throw error;\n }\n}\n\nexport async function copyPackageManagerProjectFiles(options: {\n packageManager: PackageManager;\n projectDir: string;\n runDir: string;\n projectFilePaths?: readonly string[];\n}): Promise<void> {\n for (const projectFilePath of options.projectFilePaths ?? packageManagerProjectFilePaths[options.packageManager]) {\n await copyPathIfExists(\n path.join(options.projectDir, projectFilePath),\n path.join(options.runDir, projectFilePath),\n options.runDir\n );\n }\n}\n\nasync function copyPathIfExists(sourcePath: string, destinationPath: string, runDir: string): Promise<void> {\n try {\n // Before touching the destination: creating the parent levels replaces whatever the submission\n // put there, which must not happen for a project file the problem does not even ship.\n await fs.lstat(sourcePath);\n // No-follow copy: the destination was seeded from the (sandbox-writable) submission tree, so a\n // planted symlink there — at the destination itself or at any directory level of a nested\n // project file path — must not redirect this trusted project-file overlay outside runDir.\n await createDirectoryWithoutFollowingSymlinks(runDir, path.dirname(destinationPath));\n await copyWithoutFollowingSymlinks(sourcePath, destinationPath);\n } catch (error) {\n const code =\n typeof error === 'object' && error !== null && 'code' in error ? (error as { code: unknown }).code : undefined;\n if (code !== 'ENOENT') throw error;\n }\n}\n\nasync function spawnWithInput(\n command: readonly [string, ...string[]],\n context: {\n cwd: string;\n env: NodeJS.ProcessEnv;\n outputLimitBytes: number;\n stdin: string;\n timeLimitSeconds: number;\n }\n): Promise<{\n stdout: string;\n stderr: string;\n status: number | undefined;\n timeSeconds: number;\n memoryBytes: number;\n timedOut: boolean;\n signal: NodeJS.Signals | undefined;\n outputLimitExceeded: boolean;\n}> {\n const timeOutputPath = timeCommand === undefined ? undefined : path.join(context.cwd, '.exercode-time-result');\n // `wrapCommandWithSandboxUser` is idempotent, so a command the presets already wrapped is not\n // nested; `time` is then spliced INSIDE that wrapper so it runs as the sandbox user, never as the\n // harness user writing `--output` into this sandbox-writable directory.\n const wrappedCommand = wrapCommandWithSandboxUser(command);\n const spawnedCommand =\n timeCommand === undefined\n ? wrappedCommand\n : prependInsideSandboxWrapper(wrappedCommand, [...timeCommand, `--output=${timeOutputPath}`]);\n // Started BEFORE the submission: `killSubprocessGroup` cannot spawn its `sudo` once a submission\n // has exhausted the PID cgroup, and a submission spawned first could exhaust it before this\n // watchdog exists. It is the deadline of last resort on this path.\n const watchdog = startSandboxTimeoutWatchdog(context.timeLimitSeconds);\n const watchdogDeadlineAt = Date.now() + (Math.ceil(context.timeLimitSeconds) + SANDBOX_WATCHDOG_GRACE_SECONDS) * 1000;\n let subprocess: childProcess.ChildProcessWithoutNullStreams;\n try {\n subprocess = childProcess.spawn(spawnedCommand[0], spawnedCommand.slice(1), {\n cwd: context.cwd,\n detached: process.platform !== 'win32',\n env: { ...context.env, ...getSandboxUserEnvOverrides(context.env) },\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n } catch (error) {\n watchdog.cancel();\n throw error;\n }\n\n const stdoutChunks: Buffer[] = [];\n const stderrChunks: Buffer[] = [];\n let outputBytes = 0;\n let timedOut = false;\n let outputLimitExceeded = false;\n // Set when terminating the submission failed (the cleanup `sudo` could not be spawned). The run\n // then waits for the watchdog to end the submission rather than throwing out of an EventEmitter\n // callback, which would be an uncaught exception that kills the harness before any cleanup.\n let killError: Error | undefined;\n let onKillFailure: (() => void) | undefined;\n const killOrRecordFailure = (signal: NodeJS.Signals): void => {\n try {\n killSubprocessGroup(subprocess, signal);\n } catch (error) {\n killError ??= error instanceof Error ? error : new Error(String(error));\n onKillFailure?.();\n }\n };\n\n const appendOutputChunk = (chunks: Buffer[], chunk: Buffer): void => {\n if (outputBytes >= context.outputLimitBytes) {\n if (chunk.byteLength > 0) {\n outputLimitExceeded = true;\n killOrRecordFailure('SIGKILL');\n }\n return;\n }\n\n const remainingBytes = context.outputLimitBytes - outputBytes;\n const appendedChunk = chunk.byteLength > remainingBytes ? chunk.subarray(0, remainingBytes) : chunk;\n chunks.push(appendedChunk);\n outputBytes += appendedChunk.byteLength;\n\n if (chunk.byteLength > remainingBytes) {\n outputLimitExceeded = true;\n killOrRecordFailure('SIGKILL');\n }\n };\n\n subprocess.stdout.on('data', (chunk: Buffer) => appendOutputChunk(stdoutChunks, chunk));\n subprocess.stderr.on('data', (chunk: Buffer) => appendOutputChunk(stderrChunks, chunk));\n\n const timeout = setTimeout(() => {\n timedOut = true;\n killOrRecordFailure('SIGTERM');\n }, context.timeLimitSeconds * 1000);\n const killTimeout = setTimeout(\n () => {\n if (timedOut) killOrRecordFailure('SIGKILL');\n },\n context.timeLimitSeconds * 1000 + killGracePeriodMilliseconds\n );\n killTimeout.unref();\n // Bounds the wait after a kill failure: the watchdog force-kills the sandbox user at its own\n // deadline, so `close` should arrive; if even that fails, give up rather than hang.\n let killFailureTimeout: ReturnType<typeof setTimeout> | undefined;\n let closeObserved = false;\n\n const { status, signal } = await new Promise<{ status: number | undefined; signal: NodeJS.Signals | undefined }>(\n (resolve, reject) => {\n let settled = false;\n let pendingError: Error | undefined;\n const failAfterClose = (error: Error): void => {\n if (settled) return;\n pendingError = error;\n killOrRecordFailure('SIGKILL');\n if (subprocess.pid === undefined) {\n settled = true;\n reject(error);\n }\n };\n // Terminating the submission failed. Keep waiting: the watchdog (started before the\n // submission) force-kills the sandbox user at its own deadline, so `close` still arrives and\n // the run reports its real verdict. Give up only once that deadline has passed without the\n // submission ending, so the run can neither hang nor abandon a still-running submission\n // while its watchdog could still fire.\n onKillFailure = () => {\n killFailureTimeout ??= setTimeout(\n () => {\n if (settled) return;\n settled = true;\n reject(killError ?? new Error('failed to terminate the submission'));\n },\n Math.max(0, watchdogDeadlineAt - Date.now()) + watchdogSettleMarginMilliseconds\n );\n };\n subprocess.on('error', failAfterClose);\n subprocess.stdin.on('error', (error: NodeJS.ErrnoException) => {\n if (error.code !== 'EPIPE') failAfterClose(error);\n });\n subprocess.on('close', (code, closeSignal) => {\n if (settled) return;\n settled = true;\n // The submission ended, so the watchdog has nothing left to kill and must not outlive this\n // run. A kill that failed once but succeeded on retry (or was superseded by the watchdog)\n // is not an error: the timeout/output-limit verdict below is the correct one to report.\n closeObserved = true;\n if (pendingError) {\n reject(pendingError);\n return;\n }\n resolve({ status: code ?? undefined, signal: closeSignal ?? undefined });\n });\n subprocess.stdin.end(context.stdin);\n }\n ).finally(() => {\n clearTimeout(timeout);\n clearTimeout(killTimeout);\n if (killFailureTimeout) clearTimeout(killFailureTimeout);\n // Cancel unless the submission is still running after a failed kill: the watchdog is then the\n // only remaining mechanism able to stop it, and cancelling would leave it running unchecked.\n if (closeObserved || !killError) watchdog.cancel();\n });\n\n const { timeSeconds, memoryBytes } =\n timeOutputPath === undefined ? { timeSeconds: 0, memoryBytes: 0 } : await readTimeResult(timeOutputPath);\n\n return {\n stdout: Buffer.concat(stdoutChunks).toString(),\n stderr: Buffer.concat(stderrChunks).toString(),\n status,\n timeSeconds,\n memoryBytes,\n timedOut,\n signal,\n outputLimitExceeded,\n };\n}\n\nfunction resolveTimeCommand(): readonly [string, ...string[]] | undefined {\n const command = os.platform() === 'darwin' ? 'gtime' : '/usr/bin/time';\n const result = childProcess.spawnSync(command, ['--version'], { stdio: 'ignore' });\n if (result.error || result.status !== 0) return undefined;\n\n return [command, '--format', '%e %M'];\n}\n\nfunction killSubprocessGroup(subprocess: childProcess.ChildProcess, signal: NodeJS.Signals): void {\n if (subprocess.pid === undefined) return;\n\n // The current user cannot signal the root-owned sudo wrapper nor the sandbox user's processes,\n // so go through sudo with the requested signal only; the callers' existing timers provide the\n // TERM → grace → KILL escalation.\n // Throws when the cleanup `sudo` cannot even be spawned (a submission can exhaust the PID\n // cgroup). Only `killOrRecordFailure` may call this: it records the failure and lets the\n // watchdog (or, past its deadline, a bounded timer) settle the run instead of letting the throw\n // escape an EventEmitter callback.\n if (sandboxUserName) {\n killSandboxUserProcesses([signal === 'SIGKILL' ? 'KILL' : 'TERM']);\n return;\n }\n\n try {\n if (process.platform === 'win32') {\n subprocess.kill(signal);\n return;\n }\n process.kill(-subprocess.pid, signal);\n } catch (error) {\n const code =\n typeof error === 'object' && error !== null && 'code' in error ? (error as { code: unknown }).code : undefined;\n if (code !== 'ESRCH' && code !== 'EPERM') throw error;\n }\n}\n\nfunction isErrorWithCode(error: unknown, code: string): boolean {\n return typeof error === 'object' && error !== null && 'code' in error && (error as { code: unknown }).code === code;\n}\n\nasync function readTimeResult(timeOutputPath: string): Promise<{ timeSeconds: number; memoryBytes: number }> {\n // The submission owns this directory and can put anything at this path, so decide what was\n // opened from the handle itself rather than from a prior `lstat` it could race:\n // - `O_NOFOLLOW` refuses a symlink to a harness-readable file (e.g. the problem's expected\n // outputs), which would otherwise report that file's trailing numbers as the submission's own\n // time and memory usage;\n // - `O_NONBLOCK` keeps a planted FIFO from blocking the harness forever waiting for a writer;\n // - the `fstat` then rejects anything that is not a regular file.\n //\n // Only an absent file means \"no measurement\" (the command was killed before `time` wrote one).\n // Anything else at this path is the submission tampering with its own accounting — reporting zero\n // memory would let it slip past a memory limit — so that fails the run instead.\n let content: string;\n let fileHandle: Awaited<ReturnType<typeof fs.open>> | undefined;\n try {\n fileHandle = await fs.open(\n timeOutputPath,\n nodeFs.constants.O_RDONLY | nodeFs.constants.O_NOFOLLOW | nodeFs.constants.O_NONBLOCK\n );\n const stats = await fileHandle.stat();\n if (!stats.isFile()) throw new Error(`${timeOutputPath} is not a regular file`);\n content = await fileHandle.readFile('utf8');\n } catch (error) {\n if (isErrorWithCode(error, 'ENOENT')) return { timeSeconds: 0, memoryBytes: 0 };\n throw new Error(`failed to read the time measurement at ${timeOutputPath}`, { cause: error });\n } finally {\n await fileHandle?.close();\n }\n\n const match = /(\\d+(?:[.,]\\d+)?) (\\d+)\\s*$/.exec(content);\n if (!match) return { timeSeconds: 0, memoryBytes: 0 };\n\n return { timeSeconds: Number(match[1]!.replace(',', '.')), memoryBytes: Number(match[2]) * 1024 };\n}\n"],"mappings":"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"}
|
package/dist/helpers/safeFs.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("./sandboxUser.cjs");let n=require("node:
|
|
1
|
+
"use strict";const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("./sandboxUser.cjs");let n=require("node:fs");n=e.__toESM(n,1);let r=require("node:path");r=e.__toESM(r,1);async function i(e,t){let a=await n.default.promises.lstat(e);if(a.isSymbolicLink()){await o(t),await n.default.promises.symlink(await n.default.promises.readlink(e),t);return}if(a.isDirectory()){(await l(t))?.isDirectory()||await o(t),await n.default.promises.mkdir(t,{recursive:!0});for(let a of await n.default.promises.readdir(e))await i(r.default.join(e,a),r.default.join(t,a));return}await o(t),await n.default.promises.copyFile(e,t)}async function a(e,t){let i=r.default.relative(e,t);if(!c(e,t))throw Error(`directory must be inside root: ${t} is not inside ${e}`);let a=e;for(let e of i.split(r.default.sep).filter(Boolean)){a=r.default.join(a,e);let t=await l(a);t?.isDirectory()||(t&&await o(a),await n.default.promises.mkdir(a))}}async function o(e){await t.forceRemove(e)}async function s(e,t){await o(e),await n.default.promises.writeFile(e,t,{flag:`wx`})}function c(e,t){let n=r.default.relative(e,t);return n!==`..`&&!n.startsWith(`..${r.default.sep}`)&&!r.default.isAbsolute(n)}async function l(e){try{return await n.default.promises.lstat(e)}catch{return}}exports.copyWithoutFollowingSymlinks=i,exports.createDirectoryWithoutFollowingSymlinks=a,exports.isContainedPath=c,exports.writeFileWithoutFollowingSymlinks=s;
|
|
2
2
|
//# sourceMappingURL=safeFs.cjs.map
|
package/dist/helpers/safeFs.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{forceRemove as e}from"./sandboxUser.js";import t from"node:
|
|
1
|
+
import{forceRemove as e}from"./sandboxUser.js";import t from"node:fs";import n from"node:path";async function r(e,i){let o=await t.promises.lstat(e);if(o.isSymbolicLink()){await a(i),await t.promises.symlink(await t.promises.readlink(e),i);return}if(o.isDirectory()){(await c(i))?.isDirectory()||await a(i),await t.promises.mkdir(i,{recursive:!0});for(let a of await t.promises.readdir(e))await r(n.join(e,a),n.join(i,a));return}await a(i),await t.promises.copyFile(e,i)}async function i(e,r){let i=n.relative(e,r);if(!s(e,r))throw Error(`directory must be inside root: ${r} is not inside ${e}`);let o=e;for(let e of i.split(n.sep).filter(Boolean)){o=n.join(o,e);let r=await c(o);r?.isDirectory()||(r&&await a(o),await t.promises.mkdir(o))}}async function a(t){await e(t)}async function o(e,n){await a(e),await t.promises.writeFile(e,n,{flag:`wx`})}function s(e,t){let r=n.relative(e,t);return r!==`..`&&!r.startsWith(`..${n.sep}`)&&!n.isAbsolute(r)}async function c(e){try{return await t.promises.lstat(e)}catch{return}}export{r as copyWithoutFollowingSymlinks,i as createDirectoryWithoutFollowingSymlinks,s as isContainedPath,o as writeFileWithoutFollowingSymlinks};
|
|
2
2
|
//# sourceMappingURL=safeFs.js.map
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
"use strict";const e=require("../_virtual/_rolldown/runtime.cjs");let t=require("node:
|
|
2
|
-
`).map(e=>e.trim()).filter(e=>e&&!/^\d+\s+Z\S*\s+1\s/.test(e));if(i.length===0)return;if(e>=10)return i.join(`, `)}}function
|
|
1
|
+
"use strict";const e=require("../_virtual/_rolldown/runtime.cjs");let t=require("node:fs");t=e.__toESM(t,1);let n=require("node:path");n=e.__toESM(n,1);let r=require("node:child_process");r=e.__toESM(r,1);let i=require("node:os");i=e.__toESM(i,1);const a=`EXERCODE_SANDBOX_USER`,o=`/usr/bin/sudo`,s=`/bin/chmod`,c={PATH:`/usr/local/bin:/usr/bin:/bin`};function l(e){return u?{...e,PATH:c.PATH}:{...process.env,...e}}const u=process.env.EXERCODE_SANDBOX_USER||void 0;if(u&&u===i.default.userInfo().username)throw Error(`${a} must name a different OS user than the one running the harness (got "${u}"). Leave it unset to run everything as the current user.`);const d=u?`/usr/bin/timeout`:`timeout`;function f(e){return!u||m(e)?[...e]:[...p(u),...e]}function p(e){return[o,`--preserve-env`,`-u`,e,`--`,`sh`,`-c`,`umask 0; if [ -n "$SANDBOX_LD_LIBRARY_PATH" ]; then export LD_LIBRARY_PATH="$SANDBOX_LD_LIBRARY_PATH"; fi; exec "$0" "$@"`]}function m(e){return u?p(u).every((t,n)=>e[n]===t):!1}function h(e,t){if(!m(e))return[...t,...e];let n=p(u).length;return[...e.slice(0,n),...t,...e.slice(n)]}function g(e){if(!u)return{};let t=e??process.env,n=_(t),r=n.LD_LIBRARY_PATH??t.LD_LIBRARY_PATH??process.env.LD_LIBRARY_PATH;return{HOME:`/home/${u}`,...n,...r&&{SANDBOX_LD_LIBRARY_PATH:r}}}function _(e){let t={};for(let[n,r]of Object.entries(e)){if(!n.startsWith(`SANDBOX_ENV_`))continue;let e=n.slice(12);e&&(t[e]=r)}return t}function v(e){u&&r.default.spawnSync(s,[`-R`,`a+rwX`,e],{env:c})}function y(e,t){if(!u)return;let i=[];for(let r=t;i.push(r),r!==e&&n.default.dirname(r)!==r;r=n.default.dirname(r));r.default.spawnSync(s,[`a+X`,...i],{env:c})}function b(e=[`TERM`,`KILL`]){if(!u)return;let t=[];for(let n of e){let e=w([`pkill`,`-${n}`,`-u`,u]);e.error&&t.push(`pkill -${n}: ${e.error.message}`)}if(!e.includes(`KILL`)){if(t.length>0)throw Error(`failed to signal ${u} processes: ${t.join(`; `)}`);return}let n=x();if(n)throw Error(`failed to terminate ${u} processes: ${n}`)}function x(){for(let e=0;;e++){e>0&&(w([`pkill`,`-KILL`,`-u`,u]),Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,100));let t=r.default.spawnSync(`ps`,[`-o`,`pid=,stat=,nlwp=,comm=`,`-u`,u],{env:c,encoding:`utf8`}),n=t.stderr?.trim();if(t.error)return`cannot list processes: ${t.error.message}`;if(t.signal||t.status!==0&&(t.status!==1||n))return`cannot list processes: ${n||`ps exited with ${t.status??t.signal}`}`;let i=t.stdout.split(`
|
|
2
|
+
`).map(e=>e.trim()).filter(e=>e&&!/^\d+\s+Z\S*\s+1\s/.test(e));if(i.length===0)return;if(e>=10)return i.join(`, `)}}function S(e){u&&w([`chmod`,`-R`,`a+rwX`,e])}function C(e){if(!u)return{cancel:()=>{},fired:()=>!1};let t=Math.ceil(e)+5,n=r.default.spawn(`/bin/sh`,[`-c`,`/bin/sleep ${t}; ${o} -u "$1" pkill -KILL -u "$1"`,`sh`,u],{detached:!0,stdio:`ignore`,env:c}),i=!1;n.on(`error`,()=>{i=!0});let a=!1;n.on(`exit`,()=>{a=!0}),n.unref();let s=Date.now(),l=!1;return{cancel:()=>{if(l=!0,!(n.pid===void 0||a))try{process.kill(-n.pid,`SIGKILL`)}catch{}},fired:()=>i||n.pid===void 0||!l&&Date.now()-s>=t*1e3}}function w(e){return r.default.spawnSync(o,[`-u`,u,...e],{env:c})}async function T(e){try{await t.default.promises.rm(e,{force:!0,recursive:!0})}catch(r){if(!u)throw r;S(e),w([`chmod`,`a+rwX`,n.default.dirname(e)]),await t.default.promises.rm(e,{force:!0,recursive:!0})}}exports.SANDBOX_ENV_PREFIX=`SANDBOX_ENV_`,exports.SANDBOX_USER_ENV_NAME=a,exports.SANDBOX_WATCHDOG_GRACE_SECONDS=5,exports.TIMEOUT_COMMAND=d,exports.forceRemove=T,exports.getSandboxUserEnvOverrides=g,exports.getTrustedHelperEnv=l,exports.isSandboxWrappedCommand=m,exports.killSandboxUserProcesses=b,exports.makeAccessibleToSandboxUser=v,exports.makeTraversableBySandboxUser=y,exports.prependInsideSandboxWrapper=h,exports.relaxPermissionsAsSandboxUser=S,exports.sandboxUserName=u,exports.startSandboxTimeoutWatchdog=C,exports.wrapCommandWithSandboxUser=f;
|
|
3
3
|
//# sourceMappingURL=sandboxUser.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandboxUser.cjs","names":["os","child_process","fs","path"],"sources":["../../src/helpers/sandboxUser.ts"],"sourcesContent":["import child_process from 'node:child_process';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\n\n/**\n * Name of the environment variable through which a judge server tells problem-utils to run\n * untrusted submitted programs as the given unprivileged OS user via sudo.\n *\n * The judge server sets this variable only when it runs the judge harness (e.g. `judge.ts`) as its\n * own trusted user, so that problem files (test cases and the harness itself) stay unreadable to\n * submissions while the submissions themselves run under the sandbox user. When the variable is\n * absent or empty (local development, course authoring, or an all-sandbox judge run), commands run\n * as the current user like before.\n *\n * Contract for the delegating judge server (delegation is Linux-only — sudo user separation and\n * `/home/<user>` homes are provisioned in the judge Docker image; never set this on macOS):\n * - The harness process environment is forwarded to sandboxed submissions (sudo runs with\n * `--preserve-env`), so it must not contain secrets beyond what submissions may see. The one\n * exception is the set glibc strips when executing the setuid `sudo` (secure-execution mode:\n * `LD_*`, `TMPDIR`, `LOCPATH`, `NLSPATH`, `TZDIR`, … — see ld.so(8)); no sudoers setting can\n * recover those, and only `LD_LIBRARY_PATH` is restored, by the wrapper below.\n * - sudoers must let the harness user run arbitrary commands as the sandbox user without a\n * password, pass the environment through, and keep sudo off a pseudo-terminal (a pty would merge\n * stderr into stdout and CRLF-mangle output when the harness happens to run from a terminal):\n * e.g. `Defaults:<harness> !env_reset, !env_delete, !env_check, !secure_path, !use_pty` plus\n * `<harness> ALL=(<sandbox>) NOPASSWD:SETENV: ALL`.\n * - Variables meant for the submitted program rather than for the harness must be passed under\n * {@link SANDBOX_ENV_PREFIX}; problem-utils strips that prefix when it builds a submission's\n * environment.\n * - The sandbox user's home directory must exist at `/home/<sandbox user>` and be writable. It is\n * shared across sequential requests, so the server is responsible for resetting whatever\n * cross-request persistence there matters to it.\n */\nexport const SANDBOX_USER_ENV_NAME = 'EXERCODE_SANDBOX_USER';\n\n// Absolute paths so a submission-influenced `PATH` cannot redirect binaries that the trusted\n// harness user executes (`sudo` is also setuid and must be the real one).\nconst SUDO_PATH = '/usr/bin/sudo';\nconst CHMOD_PATH = '/bin/chmod';\nconst SLEEP_PATH = '/bin/sleep';\n// Minimal environment for trusted helper processes; never forward the harness environment to them.\nconst MINIMAL_ENV = { PATH: '/usr/local/bin:/usr/bin:/bin' } as const;\n\n/**\n * Environment for helper processes the *trusted* harness user runs (`ps`, `xwininfo`, `Xvfb`, …).\n * The judge server overlays caller-supplied variables onto the harness environment, and a sandboxed\n * submission can create executables in its persistent home, so resolving these helpers through the\n * inherited `PATH` would hand the submission code execution as the harness user. Node resolves the\n * command through the child environment's `PATH`, so a fixed `PATH` here pins them to system paths.\n */\nexport function getTrustedHelperEnv(extraEnv?: NodeJS.ProcessEnv): NodeJS.ProcessEnv {\n // Without delegation the submission runs as this same user, so there is no boundary to defend and\n // pinning `PATH` would only break setups whose X11/`ps` binaries live elsewhere (e.g. /usr/games,\n // /snap/bin, Nix). Keep the inherited environment there, exactly as before delegation existed.\n if (!sandboxUserName) return { ...process.env, ...extraEnv };\n // `PATH` last: a caller passing a whole environment through would otherwise reinstate the\n // submission-influenced one this function exists to replace.\n return { ...extraEnv, PATH: MINIMAL_ENV.PATH };\n}\n\nexport const sandboxUserName = process.env[SANDBOX_USER_ENV_NAME] || undefined;\n\n// Fail fast on the catastrophic misconfiguration where the sandbox user is the harness's own user:\n// cleanup (`killSandboxUserProcesses`) would then SIGKILL the harness itself on the first run.\nif (sandboxUserName && sandboxUserName === os.userInfo().username) {\n throw new Error(\n `${SANDBOX_USER_ENV_NAME} must name a different OS user than the one running the harness (got \"${sandboxUserName}\"). Leave it unset to run everything as the current user.`\n );\n}\n\n/**\n * The deadline supervisor. Absolute under delegation: the wrapper's `exec \"$0\"` resolves it through\n * the submission's own `PATH` (which a judge server may set via {@link SANDBOX_ENV_PREFIX}), so a\n * bare name would let a submission replace the very process that enforces its time limit. The bare\n * name is kept without delegation, where `timeout` may live elsewhere (Homebrew coreutils).\n */\nexport const TIMEOUT_COMMAND = sandboxUserName ? '/usr/bin/timeout' : 'timeout';\n\n/**\n * Wrap a command so it runs as the sandbox user. `umask 0` makes every file the sandboxed process\n * creates world-writable, so the harness user can clean it up without privileges. The wrapper also\n * restores `LD_LIBRARY_PATH`, which ld.so strips across the setuid `sudo` exec. Put `timeout`\n * inside the wrapped command: the harness user cannot signal the root-owned `sudo` process, so an\n * outer timer alone could not stop a runaway submission.\n */\nexport function wrapCommandWithSandboxUser(command: readonly [string, ...string[]]): [string, ...string[]] {\n if (!sandboxUserName) return [...command];\n // Idempotent; see {@link isSandboxWrappedCommand}.\n if (isSandboxWrappedCommand(command)) return [...command];\n return [...buildSandboxWrapperPrefix(sandboxUserName), ...command];\n}\n\nconst SANDBOX_WRAPPER_SCRIPT =\n 'umask 0; if [ -n \"$SANDBOX_LD_LIBRARY_PATH\" ]; then export LD_LIBRARY_PATH=\"$SANDBOX_LD_LIBRARY_PATH\"; fi; exec \"$0\" \"$@\"';\n\nfunction buildSandboxWrapperPrefix(user: string): [string, ...string[]] {\n return [SUDO_PATH, '--preserve-env', '-u', user, '--', 'sh', '-c', SANDBOX_WRAPPER_SCRIPT];\n}\n\n/**\n * Whether the command was already wrapped by {@link wrapCommandWithSandboxUser}. The presets hand\n * custom runners a wrapped command, and a runner may forward it to another helper that wraps too;\n * a nested wrapper's inner `sudo` would run AS the sandbox user, which sudoers does not authorize,\n * so every test case of such a problem would fail only under delegation.\n *\n * Matches the wrapper prefix exactly rather than looking for `sudo` anywhere: a submission-derived\n * command could otherwise carry a literal `/usr/bin/sudo` argument and skip wrapping entirely,\n * which would run the submission as the trusted harness user.\n */\nexport function isSandboxWrappedCommand(command: readonly string[]): boolean {\n if (!sandboxUserName) return false;\n const prefix = buildSandboxWrapperPrefix(sandboxUserName);\n return prefix.every((argument, index) => command[index] === argument);\n}\n\n/**\n * Insert `innerPrefix` (e.g. a `time` measurement prefix) so that it runs INSIDE the sandbox\n * wrapper when `command` is wrapped, and simply in front otherwise. Prefixing a wrapped command\n * from the outside would run `innerPrefix` as the trusted harness user while its arguments (such\n * as an output path) point into a sandbox-writable directory, where a submission can plant a\n * symlink and have the harness truncate an arbitrary file it owns.\n */\nexport function prependInsideSandboxWrapper(\n command: readonly [string, ...string[]],\n innerPrefix: readonly string[]\n): [string, ...string[]] {\n if (!isSandboxWrappedCommand(command)) return [...innerPrefix, ...command] as [string, ...string[]];\n const prefixLength = buildSandboxWrapperPrefix(sandboxUserName as string).length;\n return [...command.slice(0, prefixLength), ...innerPrefix, ...command.slice(prefixLength)] as [string, ...string[]];\n}\n\n/**\n * Environment overrides for sandboxed processes: a writable home and the `LD_LIBRARY_PATH`\n * smuggled past the setuid `sudo` exec (see {@link wrapCommandWithSandboxUser}). Pass the\n * environment the command will actually run with so a caller-supplied `LD_LIBRARY_PATH` survives\n * the exec; the harness's own value is only the fallback.\n */\nexport function getSandboxUserEnvOverrides(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv {\n if (!sandboxUserName) return {};\n const sourceEnv = env ?? process.env;\n const submissionOnlyEnv = unwrapSubmissionOnlyEnv(sourceEnv);\n // A submission-only `LD_LIBRARY_PATH` must be the one smuggled past the exec, not the harness's.\n const ldLibraryPath = submissionOnlyEnv.LD_LIBRARY_PATH ?? sourceEnv.LD_LIBRARY_PATH ?? process.env.LD_LIBRARY_PATH;\n return {\n HOME: `/home/${sandboxUserName}`,\n ...submissionOnlyEnv,\n // Last: the wrapper reads this one, so a caller must not be able to overwrite it.\n ...(ldLibraryPath && { SANDBOX_LD_LIBRARY_PATH: ldLibraryPath }),\n };\n}\n\n/**\n * Prefix under which a delegating judge server passes variables that belong to the submitted\n * program alone. Applying a request's variables to the harness itself would let a submission point\n * e.g. `PATH` or `NODE_OPTIONS` at its own files and get code executed as the trusted harness user,\n * so the server prefixes them and problem-utils strips the prefix back off here, where the\n * environment of a sandboxed submission is built.\n */\nexport const SANDBOX_ENV_PREFIX = 'SANDBOX_ENV_';\n\nfunction unwrapSubmissionOnlyEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {\n const unwrapped: NodeJS.ProcessEnv = {};\n for (const [name, value] of Object.entries(env)) {\n if (!name.startsWith(SANDBOX_ENV_PREFIX)) continue;\n const unwrappedName = name.slice(SANDBOX_ENV_PREFIX.length);\n if (unwrappedName) unwrapped[unwrappedName] = value;\n }\n return unwrapped;\n}\n\n/**\n * Make harness-user-created files under the given path readable, and directories writable, for the\n * sandbox user, so sandboxed programs can read their sources and create outputs next to them.\n */\nexport function makeAccessibleToSandboxUser(targetPath: string): void {\n if (!sandboxUserName) return;\n // Files owned by the sandbox user fail to chmod but are already world-writable (umask 0).\n child_process.spawnSync(CHMOD_PATH, ['-R', 'a+rwX', targetPath], { env: MINIMAL_ENV });\n}\n\n/**\n * Kill the sandbox user's processes with the given signals. The harness user cannot signal another\n * user's processes (nor the root-owned `sudo` wrapper), so this goes through sudo. Killing every\n * sandbox process is safe because the judge server handles one request at a time. The default\n * sends SIGTERM immediately followed by SIGKILL (final cleanup); callers that want a grace period\n * send `['TERM']`, wait, and then send `['KILL']`.\n *\n * Fails closed: a `['TERM']`-only sweep throws when its `sudo` could not be spawned (e.g. a\n * submission exhausted the PID cgroup); a sweep including SIGKILL throws when a sandbox process is\n * still alive, or cannot be listed, afterwards. Reporting success would leave that process running\n * next to the next request on this instance.\n */\nexport function killSandboxUserProcesses(signals: readonly ('TERM' | 'KILL')[] = ['TERM', 'KILL']): void {\n if (!sandboxUserName) return;\n // One direct call per signal instead of one `sh -c 'pkill ...; pkill ...'`: the wrapping shell\n // would run as the sandbox user too, so the first pkill would kill it before the second ran.\n // Exit statuses are not inspected: `sudo` exits with 1 both when `pkill` matched nothing and on\n // its own failures, and a sweep can itself be killed by a concurrent watchdog sweep\n // ({@link startSandboxTimeoutWatchdog}'s `pkill -KILL` matches it). What matters is the outcome.\n const spawnErrors: string[] = [];\n for (const signal of signals) {\n const result = runAsSandboxUser(['pkill', `-${signal}`, '-u', sandboxUserName]);\n if (result.error) spawnErrors.push(`pkill -${signal}: ${result.error.message}`);\n }\n if (!signals.includes('KILL')) {\n // Survivors are expected during the caller's grace period, so only a sweep that never started\n // is a failure here.\n if (spawnErrors.length > 0) {\n throw new Error(`failed to signal ${sandboxUserName} processes: ${spawnErrors.join('; ')}`);\n }\n return;\n }\n const survivors = findSurvivingSandboxUserProcesses();\n if (survivors) throw new Error(`failed to terminate ${sandboxUserName} processes: ${survivors}`);\n}\n\n/**\n * Lists the sandbox user's live processes as the harness user (listing needs no privilege), or\n * `undefined` when none remain. SIGKILL is re-sent between attempts: a child forked after `pkill`\n * scanned `/proc` was never signalled, and a process in uninterruptible sleep dies only once its\n * I/O completes. Just-killed processes linger as zombies until reaped; a zombie row is ignored only\n * when it has a single thread, because a thread-group leader that exited via `pthread_exit` shows\n * as a zombie while its other threads keep running.\n */\nfunction findSurvivingSandboxUserProcesses(): string | undefined {\n for (let attempt = 0; ; attempt++) {\n if (attempt > 0) {\n runAsSandboxUser(['pkill', '-KILL', '-u', sandboxUserName as string]);\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);\n }\n const result = child_process.spawnSync('ps', ['-o', 'pid=,stat=,nlwp=,comm=', '-u', sandboxUserName as string], {\n env: MINIMAL_ENV,\n encoding: 'utf8',\n });\n // `ps` exits with 1 both when no process matched and on its own errors, which it reports on\n // stderr; anything else is a failure to list, which must count as a failure to verify.\n const stderr = result.stderr?.trim();\n if (result.error) return `cannot list processes: ${result.error.message}`;\n if (result.signal || (result.status !== 0 && (result.status !== 1 || stderr))) {\n return `cannot list processes: ${stderr || `ps exited with ${result.status ?? result.signal}`}`;\n }\n const survivors = result.stdout\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line && !/^\\d+\\s+Z\\S*\\s+1\\s/.test(line));\n if (survivors.length === 0) return undefined;\n if (attempt >= 10) return survivors.join(', ');\n }\n}\n\n/**\n * Let the sandbox user reopen the permissions of its own files under the given path, for\n * harness-side traversal/cleanup of trees where a sandboxed process restricted permissions\n * (some tools chmod their outputs regardless of umask).\n */\nexport function relaxPermissionsAsSandboxUser(targetPath: string): void {\n if (!sandboxUserName) return;\n runAsSandboxUser(['chmod', '-R', 'a+rwX', targetPath]);\n}\n\n/**\n * How long after a command's own deadline {@link startSandboxTimeoutWatchdog} force-kills the\n * sandbox user. Callers waiting for the watchdog to end a submission must wait at least this long.\n */\nexport const SANDBOX_WATCHDOG_GRACE_SECONDS = 5;\n\n/** Cancels a {@link startSandboxTimeoutWatchdog}; `fired` reports whether its deadline elapsed. */\nexport interface SandboxTimeoutWatchdog {\n cancel(): void;\n fired(): boolean;\n}\n\n/**\n * Start a harness-owned watchdog that force-kills every sandbox process after the given deadline.\n * A sandboxed submission can signal its own `timeout` supervisor (same UID), and a synchronous\n * spawn blocks the harness's event loop, so without this external deadline a submission could run\n * until the outer judge-server limit. The watchdog's `sh`/`sleep` run as the harness user, out of\n * the submission's reach; killing the watchdog's process group cancels it before it spawns sudo.\n *\n * ALWAYS cancel in a `finally`: the watchdog is detached and `unref`'d, so a leaked one keeps\n * running after the harness exits and would SIGKILL a later request's submission.\n */\nexport function startSandboxTimeoutWatchdog(timeoutSeconds: number): SandboxTimeoutWatchdog {\n if (!sandboxUserName) return { cancel: () => {}, fired: () => false };\n\n const deadlineSeconds = Math.ceil(timeoutSeconds) + SANDBOX_WATCHDOG_GRACE_SECONDS;\n const watchdog = child_process.spawn(\n '/bin/sh',\n ['-c', `${SLEEP_PATH} ${deadlineSeconds}; ${SUDO_PATH} -u \"$1\" pkill -KILL -u \"$1\"`, 'sh', sandboxUserName],\n { detached: true, stdio: 'ignore', env: MINIMAL_ENV }\n );\n // Without a listener, a spawn failure (e.g. the submission exhausted the PID cgroup) would be an\n // unhandled 'error' event that terminates the harness. Fail closed instead: callers check\n // `fired()`, and a watchdog that never started reports its deadline as elapsed.\n let spawnFailed = false;\n watchdog.on('error', () => {\n spawnFailed = true;\n });\n let exited = false;\n watchdog.on('exit', () => {\n exited = true;\n });\n watchdog.unref();\n\n const startTimeMilliseconds = Date.now();\n let cancelled = false;\n return {\n cancel: () => {\n cancelled = true;\n if (watchdog.pid === undefined || exited) return;\n try {\n process.kill(-watchdog.pid, 'SIGKILL');\n } catch {\n // The watchdog already fired and exited.\n }\n },\n // The `exit`/`error` events cannot be observed from a synchronous caller (they need the event\n // loop), so decide by elapsed time, which is what the watchdog itself waits on.\n fired: () =>\n spawnFailed ||\n watchdog.pid === undefined ||\n (!cancelled && Date.now() - startTimeMilliseconds >= deadlineSeconds * 1000),\n };\n}\n\n/**\n * Run a helper as the sandbox user with a minimal environment. The harness environment must not be\n * passed: sudoers forwards it verbatim, and after sudo drops privileges the helper's\n * `/proc/<pid>/environ` becomes readable by every other sandbox process.\n */\nfunction runAsSandboxUser(command: [string, ...string[]]): child_process.SpawnSyncReturns<Buffer> {\n return child_process.spawnSync(SUDO_PATH, ['-u', sandboxUserName as string, ...command], { env: MINIMAL_ENV });\n}\n\n/**\n * `fs.rm`-like removal with a fallback for trees holding sandbox-user-owned entries whose\n * permissions block deletion: let the sandbox user reopen its own files first, then retry. The\n * containing directory is relaxed as well — unlinking an entry needs write permission on its\n * parent, so a submission that chmods a directory it owns would otherwise make everything inside it\n * undeletable.\n */\nexport async function forceRemove(targetPath: string): Promise<void> {\n try {\n await fs.promises.rm(targetPath, { force: true, recursive: true });\n } catch (error) {\n if (!sandboxUserName) throw error;\n relaxPermissionsAsSandboxUser(targetPath);\n // Not recursive: only the containing directory's own write bit governs the unlink.\n runAsSandboxUser(['chmod', 'a+rwX', path.dirname(targetPath)]);\n await fs.promises.rm(targetPath, { force: true, recursive: true });\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sandboxUser.cjs","names":["os","path","child_process","fs"],"sources":["../../src/helpers/sandboxUser.ts"],"sourcesContent":["import child_process from 'node:child_process';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\n\n/**\n * Name of the environment variable through which a judge server tells problem-utils to run\n * untrusted submitted programs as the given unprivileged OS user via sudo.\n *\n * The judge server sets this variable only when it runs the judge harness (e.g. `judge.ts`) as its\n * own trusted user, so that problem files (test cases and the harness itself) stay unreadable to\n * submissions while the submissions themselves run under the sandbox user. When the variable is\n * absent or empty (local development, course authoring, or an all-sandbox judge run), commands run\n * as the current user like before.\n *\n * Contract for the delegating judge server (delegation is Linux-only — sudo user separation and\n * `/home/<user>` homes are provisioned in the judge Docker image; never set this on macOS):\n * - The harness process environment is forwarded to sandboxed submissions (sudo runs with\n * `--preserve-env`), so it must not contain secrets beyond what submissions may see. The one\n * exception is the set glibc strips when executing the setuid `sudo` (secure-execution mode:\n * `LD_*`, `TMPDIR`, `LOCPATH`, `NLSPATH`, `TZDIR`, … — see ld.so(8)); no sudoers setting can\n * recover those, and only `LD_LIBRARY_PATH` is restored, by the wrapper below.\n * - sudoers must let the harness user run arbitrary commands as the sandbox user without a\n * password, pass the environment through, and keep sudo off a pseudo-terminal (a pty would merge\n * stderr into stdout and CRLF-mangle output when the harness happens to run from a terminal):\n * e.g. `Defaults:<harness> !env_reset, !env_delete, !env_check, !secure_path, !use_pty` plus\n * `<harness> ALL=(<sandbox>) NOPASSWD:SETENV: ALL`.\n * - Variables meant for the submitted program rather than for the harness must be passed under\n * {@link SANDBOX_ENV_PREFIX}; problem-utils strips that prefix when it builds a submission's\n * environment.\n * - The sandbox user's home directory must exist at `/home/<sandbox user>` and be writable. It is\n * shared across sequential requests, so the server is responsible for resetting whatever\n * cross-request persistence there matters to it.\n */\nexport const SANDBOX_USER_ENV_NAME = 'EXERCODE_SANDBOX_USER';\n\n// Absolute paths so a submission-influenced `PATH` cannot redirect binaries that the trusted\n// harness user executes (`sudo` is also setuid and must be the real one).\nconst SUDO_PATH = '/usr/bin/sudo';\nconst CHMOD_PATH = '/bin/chmod';\nconst SLEEP_PATH = '/bin/sleep';\n// Minimal environment for trusted helper processes; never forward the harness environment to them.\nconst MINIMAL_ENV = { PATH: '/usr/local/bin:/usr/bin:/bin' } as const;\n\n/**\n * Environment for helper processes the *trusted* harness user runs (`ps`, `xwininfo`, `Xvfb`, …).\n * The judge server overlays caller-supplied variables onto the harness environment, and a sandboxed\n * submission can create executables in its persistent home, so resolving these helpers through the\n * inherited `PATH` would hand the submission code execution as the harness user. Node resolves the\n * command through the child environment's `PATH`, so a fixed `PATH` here pins them to system paths.\n */\nexport function getTrustedHelperEnv(extraEnv?: NodeJS.ProcessEnv): NodeJS.ProcessEnv {\n // Without delegation the submission runs as this same user, so there is no boundary to defend and\n // pinning `PATH` would only break setups whose X11/`ps` binaries live elsewhere (e.g. /usr/games,\n // /snap/bin, Nix). Keep the inherited environment there, exactly as before delegation existed.\n if (!sandboxUserName) return { ...process.env, ...extraEnv };\n // `PATH` last: a caller passing a whole environment through would otherwise reinstate the\n // submission-influenced one this function exists to replace.\n return { ...extraEnv, PATH: MINIMAL_ENV.PATH };\n}\n\nexport const sandboxUserName = process.env[SANDBOX_USER_ENV_NAME] || undefined;\n\n// Fail fast on the catastrophic misconfiguration where the sandbox user is the harness's own user:\n// cleanup (`killSandboxUserProcesses`) would then SIGKILL the harness itself on the first run.\nif (sandboxUserName && sandboxUserName === os.userInfo().username) {\n throw new Error(\n `${SANDBOX_USER_ENV_NAME} must name a different OS user than the one running the harness (got \"${sandboxUserName}\"). Leave it unset to run everything as the current user.`\n );\n}\n\n/**\n * The deadline supervisor. Absolute under delegation: the wrapper's `exec \"$0\"` resolves it through\n * the submission's own `PATH` (which a judge server may set via {@link SANDBOX_ENV_PREFIX}), so a\n * bare name would let a submission replace the very process that enforces its time limit. The bare\n * name is kept without delegation, where `timeout` may live elsewhere (Homebrew coreutils).\n */\nexport const TIMEOUT_COMMAND = sandboxUserName ? '/usr/bin/timeout' : 'timeout';\n\n/**\n * Wrap a command so it runs as the sandbox user. `umask 0` makes every file the sandboxed process\n * creates world-writable, so the harness user can clean it up without privileges. The wrapper also\n * restores `LD_LIBRARY_PATH`, which ld.so strips across the setuid `sudo` exec. Put `timeout`\n * inside the wrapped command: the harness user cannot signal the root-owned `sudo` process, so an\n * outer timer alone could not stop a runaway submission.\n */\nexport function wrapCommandWithSandboxUser(command: readonly [string, ...string[]]): [string, ...string[]] {\n if (!sandboxUserName) return [...command];\n // Idempotent; see {@link isSandboxWrappedCommand}.\n if (isSandboxWrappedCommand(command)) return [...command];\n return [...buildSandboxWrapperPrefix(sandboxUserName), ...command];\n}\n\nconst SANDBOX_WRAPPER_SCRIPT =\n 'umask 0; if [ -n \"$SANDBOX_LD_LIBRARY_PATH\" ]; then export LD_LIBRARY_PATH=\"$SANDBOX_LD_LIBRARY_PATH\"; fi; exec \"$0\" \"$@\"';\n\nfunction buildSandboxWrapperPrefix(user: string): [string, ...string[]] {\n return [SUDO_PATH, '--preserve-env', '-u', user, '--', 'sh', '-c', SANDBOX_WRAPPER_SCRIPT];\n}\n\n/**\n * Whether the command was already wrapped by {@link wrapCommandWithSandboxUser}. The presets hand\n * custom runners a wrapped command, and a runner may forward it to another helper that wraps too;\n * a nested wrapper's inner `sudo` would run AS the sandbox user, which sudoers does not authorize,\n * so every test case of such a problem would fail only under delegation.\n *\n * Matches the wrapper prefix exactly rather than looking for `sudo` anywhere: a submission-derived\n * command could otherwise carry a literal `/usr/bin/sudo` argument and skip wrapping entirely,\n * which would run the submission as the trusted harness user.\n */\nexport function isSandboxWrappedCommand(command: readonly string[]): boolean {\n if (!sandboxUserName) return false;\n const prefix = buildSandboxWrapperPrefix(sandboxUserName);\n return prefix.every((argument, index) => command[index] === argument);\n}\n\n/**\n * Insert `innerPrefix` (e.g. a `time` measurement prefix) so that it runs INSIDE the sandbox\n * wrapper when `command` is wrapped, and simply in front otherwise. Prefixing a wrapped command\n * from the outside would run `innerPrefix` as the trusted harness user while its arguments (such\n * as an output path) point into a sandbox-writable directory, where a submission can plant a\n * symlink and have the harness truncate an arbitrary file it owns.\n */\nexport function prependInsideSandboxWrapper(\n command: readonly [string, ...string[]],\n innerPrefix: readonly string[]\n): [string, ...string[]] {\n if (!isSandboxWrappedCommand(command)) return [...innerPrefix, ...command] as [string, ...string[]];\n const prefixLength = buildSandboxWrapperPrefix(sandboxUserName as string).length;\n return [...command.slice(0, prefixLength), ...innerPrefix, ...command.slice(prefixLength)] as [string, ...string[]];\n}\n\n/**\n * Environment overrides for sandboxed processes: a writable home and the `LD_LIBRARY_PATH`\n * smuggled past the setuid `sudo` exec (see {@link wrapCommandWithSandboxUser}). Pass the\n * environment the command will actually run with so a caller-supplied `LD_LIBRARY_PATH` survives\n * the exec; the harness's own value is only the fallback.\n */\nexport function getSandboxUserEnvOverrides(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv {\n if (!sandboxUserName) return {};\n const sourceEnv = env ?? process.env;\n const submissionOnlyEnv = unwrapSubmissionOnlyEnv(sourceEnv);\n // A submission-only `LD_LIBRARY_PATH` must be the one smuggled past the exec, not the harness's.\n const ldLibraryPath = submissionOnlyEnv.LD_LIBRARY_PATH ?? sourceEnv.LD_LIBRARY_PATH ?? process.env.LD_LIBRARY_PATH;\n return {\n HOME: `/home/${sandboxUserName}`,\n ...submissionOnlyEnv,\n // Last: the wrapper reads this one, so a caller must not be able to overwrite it.\n ...(ldLibraryPath && { SANDBOX_LD_LIBRARY_PATH: ldLibraryPath }),\n };\n}\n\n/**\n * Prefix under which a delegating judge server passes variables that belong to the submitted\n * program alone. Applying a request's variables to the harness itself would let a submission point\n * e.g. `PATH` or `NODE_OPTIONS` at its own files and get code executed as the trusted harness user,\n * so the server prefixes them and problem-utils strips the prefix back off here, where the\n * environment of a sandboxed submission is built.\n */\nexport const SANDBOX_ENV_PREFIX = 'SANDBOX_ENV_';\n\nfunction unwrapSubmissionOnlyEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {\n const unwrapped: NodeJS.ProcessEnv = {};\n for (const [name, value] of Object.entries(env)) {\n if (!name.startsWith(SANDBOX_ENV_PREFIX)) continue;\n const unwrappedName = name.slice(SANDBOX_ENV_PREFIX.length);\n if (unwrappedName) unwrapped[unwrappedName] = value;\n }\n return unwrapped;\n}\n\n/**\n * Make harness-user-created files under the given path readable, and directories writable, for the\n * sandbox user, so sandboxed programs can read their sources and create outputs next to them.\n */\nexport function makeAccessibleToSandboxUser(targetPath: string): void {\n if (!sandboxUserName) return;\n // Files owned by the sandbox user fail to chmod but are already world-writable (umask 0).\n child_process.spawnSync(CHMOD_PATH, ['-R', 'a+rwX', targetPath], { env: MINIMAL_ENV });\n}\n\n/**\n * Let the sandbox user traverse (but not modify) every directory from `rootPath` down to\n * `directoryPath`, e.g. the synthesized ancestors of a temporary copy.\n */\nexport function makeTraversableBySandboxUser(rootPath: string, directoryPath: string): void {\n if (!sandboxUserName) return;\n const directories: string[] = [];\n for (let current = directoryPath; ; current = path.dirname(current)) {\n directories.push(current);\n if (current === rootPath || path.dirname(current) === current) break;\n }\n child_process.spawnSync(CHMOD_PATH, ['a+X', ...directories], { env: MINIMAL_ENV });\n}\n\n/**\n * Kill the sandbox user's processes with the given signals. The harness user cannot signal another\n * user's processes (nor the root-owned `sudo` wrapper), so this goes through sudo. Killing every\n * sandbox process is safe because the judge server handles one request at a time. The default\n * sends SIGTERM immediately followed by SIGKILL (final cleanup); callers that want a grace period\n * send `['TERM']`, wait, and then send `['KILL']`.\n *\n * Fails closed: a `['TERM']`-only sweep throws when its `sudo` could not be spawned (e.g. a\n * submission exhausted the PID cgroup); a sweep including SIGKILL throws when a sandbox process is\n * still alive, or cannot be listed, afterwards. Reporting success would leave that process running\n * next to the next request on this instance.\n */\nexport function killSandboxUserProcesses(signals: readonly ('TERM' | 'KILL')[] = ['TERM', 'KILL']): void {\n if (!sandboxUserName) return;\n // One direct call per signal instead of one `sh -c 'pkill ...; pkill ...'`: the wrapping shell\n // would run as the sandbox user too, so the first pkill would kill it before the second ran.\n // Exit statuses are not inspected: `sudo` exits with 1 both when `pkill` matched nothing and on\n // its own failures, and a sweep can itself be killed by a concurrent watchdog sweep\n // ({@link startSandboxTimeoutWatchdog}'s `pkill -KILL` matches it). What matters is the outcome.\n const spawnErrors: string[] = [];\n for (const signal of signals) {\n const result = runAsSandboxUser(['pkill', `-${signal}`, '-u', sandboxUserName]);\n if (result.error) spawnErrors.push(`pkill -${signal}: ${result.error.message}`);\n }\n if (!signals.includes('KILL')) {\n // Survivors are expected during the caller's grace period, so only a sweep that never started\n // is a failure here.\n if (spawnErrors.length > 0) {\n throw new Error(`failed to signal ${sandboxUserName} processes: ${spawnErrors.join('; ')}`);\n }\n return;\n }\n const survivors = findSurvivingSandboxUserProcesses();\n if (survivors) throw new Error(`failed to terminate ${sandboxUserName} processes: ${survivors}`);\n}\n\n/**\n * Lists the sandbox user's live processes as the harness user (listing needs no privilege), or\n * `undefined` when none remain. SIGKILL is re-sent between attempts: a child forked after `pkill`\n * scanned `/proc` was never signalled, and a process in uninterruptible sleep dies only once its\n * I/O completes. Just-killed processes linger as zombies until reaped; a zombie row is ignored only\n * when it has a single thread, because a thread-group leader that exited via `pthread_exit` shows\n * as a zombie while its other threads keep running.\n */\nfunction findSurvivingSandboxUserProcesses(): string | undefined {\n for (let attempt = 0; ; attempt++) {\n if (attempt > 0) {\n runAsSandboxUser(['pkill', '-KILL', '-u', sandboxUserName as string]);\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);\n }\n const result = child_process.spawnSync('ps', ['-o', 'pid=,stat=,nlwp=,comm=', '-u', sandboxUserName as string], {\n env: MINIMAL_ENV,\n encoding: 'utf8',\n });\n // `ps` exits with 1 both when no process matched and on its own errors, which it reports on\n // stderr; anything else is a failure to list, which must count as a failure to verify.\n const stderr = result.stderr?.trim();\n if (result.error) return `cannot list processes: ${result.error.message}`;\n if (result.signal || (result.status !== 0 && (result.status !== 1 || stderr))) {\n return `cannot list processes: ${stderr || `ps exited with ${result.status ?? result.signal}`}`;\n }\n const survivors = result.stdout\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line && !/^\\d+\\s+Z\\S*\\s+1\\s/.test(line));\n if (survivors.length === 0) return undefined;\n if (attempt >= 10) return survivors.join(', ');\n }\n}\n\n/**\n * Let the sandbox user reopen the permissions of its own files under the given path, for\n * harness-side traversal/cleanup of trees where a sandboxed process restricted permissions\n * (some tools chmod their outputs regardless of umask).\n */\nexport function relaxPermissionsAsSandboxUser(targetPath: string): void {\n if (!sandboxUserName) return;\n runAsSandboxUser(['chmod', '-R', 'a+rwX', targetPath]);\n}\n\n/**\n * How long after a command's own deadline {@link startSandboxTimeoutWatchdog} force-kills the\n * sandbox user. Callers waiting for the watchdog to end a submission must wait at least this long.\n */\nexport const SANDBOX_WATCHDOG_GRACE_SECONDS = 5;\n\n/** Cancels a {@link startSandboxTimeoutWatchdog}; `fired` reports whether its deadline elapsed. */\nexport interface SandboxTimeoutWatchdog {\n cancel(): void;\n fired(): boolean;\n}\n\n/**\n * Start a harness-owned watchdog that force-kills every sandbox process after the given deadline.\n * A sandboxed submission can signal its own `timeout` supervisor (same UID), and a synchronous\n * spawn blocks the harness's event loop, so without this external deadline a submission could run\n * until the outer judge-server limit. The watchdog's `sh`/`sleep` run as the harness user, out of\n * the submission's reach; killing the watchdog's process group cancels it before it spawns sudo.\n *\n * ALWAYS cancel in a `finally`: the watchdog is detached and `unref`'d, so a leaked one keeps\n * running after the harness exits and would SIGKILL a later request's submission.\n */\nexport function startSandboxTimeoutWatchdog(timeoutSeconds: number): SandboxTimeoutWatchdog {\n if (!sandboxUserName) return { cancel: () => {}, fired: () => false };\n\n const deadlineSeconds = Math.ceil(timeoutSeconds) + SANDBOX_WATCHDOG_GRACE_SECONDS;\n const watchdog = child_process.spawn(\n '/bin/sh',\n ['-c', `${SLEEP_PATH} ${deadlineSeconds}; ${SUDO_PATH} -u \"$1\" pkill -KILL -u \"$1\"`, 'sh', sandboxUserName],\n { detached: true, stdio: 'ignore', env: MINIMAL_ENV }\n );\n // Without a listener, a spawn failure (e.g. the submission exhausted the PID cgroup) would be an\n // unhandled 'error' event that terminates the harness. Fail closed instead: callers check\n // `fired()`, and a watchdog that never started reports its deadline as elapsed.\n let spawnFailed = false;\n watchdog.on('error', () => {\n spawnFailed = true;\n });\n let exited = false;\n watchdog.on('exit', () => {\n exited = true;\n });\n watchdog.unref();\n\n const startTimeMilliseconds = Date.now();\n let cancelled = false;\n return {\n cancel: () => {\n cancelled = true;\n if (watchdog.pid === undefined || exited) return;\n try {\n process.kill(-watchdog.pid, 'SIGKILL');\n } catch {\n // The watchdog already fired and exited.\n }\n },\n // The `exit`/`error` events cannot be observed from a synchronous caller (they need the event\n // loop), so decide by elapsed time, which is what the watchdog itself waits on.\n fired: () =>\n spawnFailed ||\n watchdog.pid === undefined ||\n (!cancelled && Date.now() - startTimeMilliseconds >= deadlineSeconds * 1000),\n };\n}\n\n/**\n * Run a helper as the sandbox user with a minimal environment. The harness environment must not be\n * passed: sudoers forwards it verbatim, and after sudo drops privileges the helper's\n * `/proc/<pid>/environ` becomes readable by every other sandbox process.\n */\nfunction runAsSandboxUser(command: [string, ...string[]]): child_process.SpawnSyncReturns<Buffer> {\n return child_process.spawnSync(SUDO_PATH, ['-u', sandboxUserName as string, ...command], { env: MINIMAL_ENV });\n}\n\n/**\n * `fs.rm`-like removal with a fallback for trees holding sandbox-user-owned entries whose\n * permissions block deletion: let the sandbox user reopen its own files first, then retry. The\n * containing directory is relaxed as well — unlinking an entry needs write permission on its\n * parent, so a submission that chmods a directory it owns would otherwise make everything inside it\n * undeletable.\n */\nexport async function forceRemove(targetPath: string): Promise<void> {\n try {\n await fs.promises.rm(targetPath, { force: true, recursive: true });\n } catch (error) {\n if (!sandboxUserName) throw error;\n relaxPermissionsAsSandboxUser(targetPath);\n // Not recursive: only the containing directory's own write bit governs the unlink.\n runAsSandboxUser(['chmod', 'a+rwX', path.dirname(targetPath)]);\n await fs.promises.rm(targetPath, { force: true, recursive: true });\n }\n}\n"],"mappings":"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"}
|
|
@@ -91,6 +91,11 @@ export declare const SANDBOX_ENV_PREFIX = "SANDBOX_ENV_";
|
|
|
91
91
|
* sandbox user, so sandboxed programs can read their sources and create outputs next to them.
|
|
92
92
|
*/
|
|
93
93
|
export declare function makeAccessibleToSandboxUser(targetPath: string): void;
|
|
94
|
+
/**
|
|
95
|
+
* Let the sandbox user traverse (but not modify) every directory from `rootPath` down to
|
|
96
|
+
* `directoryPath`, e.g. the synthesized ancestors of a temporary copy.
|
|
97
|
+
*/
|
|
98
|
+
export declare function makeTraversableBySandboxUser(rootPath: string, directoryPath: string): void;
|
|
94
99
|
/**
|
|
95
100
|
* Kill the sandbox user's processes with the given signals. The harness user cannot signal another
|
|
96
101
|
* user's processes (nor the root-owned `sudo` wrapper), so this goes through sudo. Killing every
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import e from"node:
|
|
2
|
-
`).map(e=>e.trim()).filter(e=>e&&!/^\d+\s+Z\S*\s+1\s/.test(e));if(i.length===0)return;if(e>=10)return i.join(`, `)}}function
|
|
1
|
+
import e from"node:fs";import t from"node:path";import n from"node:child_process";import r from"node:os";const i=`EXERCODE_SANDBOX_USER`,a=`/usr/bin/sudo`,o=`/bin/chmod`,s={PATH:`/usr/local/bin:/usr/bin:/bin`};function c(e){return l?{...e,PATH:s.PATH}:{...process.env,...e}}const l=process.env.EXERCODE_SANDBOX_USER||void 0;if(l&&l===r.userInfo().username)throw Error(`${i} must name a different OS user than the one running the harness (got "${l}"). Leave it unset to run everything as the current user.`);const u=l?`/usr/bin/timeout`:`timeout`;function d(e){return!l||p(e)?[...e]:[...f(l),...e]}function f(e){return[a,`--preserve-env`,`-u`,e,`--`,`sh`,`-c`,`umask 0; if [ -n "$SANDBOX_LD_LIBRARY_PATH" ]; then export LD_LIBRARY_PATH="$SANDBOX_LD_LIBRARY_PATH"; fi; exec "$0" "$@"`]}function p(e){return l?f(l).every((t,n)=>e[n]===t):!1}function m(e,t){if(!p(e))return[...t,...e];let n=f(l).length;return[...e.slice(0,n),...t,...e.slice(n)]}function h(e){if(!l)return{};let t=e??process.env,n=_(t),r=n.LD_LIBRARY_PATH??t.LD_LIBRARY_PATH??process.env.LD_LIBRARY_PATH;return{HOME:`/home/${l}`,...n,...r&&{SANDBOX_LD_LIBRARY_PATH:r}}}const g=`SANDBOX_ENV_`;function _(e){let t={};for(let[n,r]of Object.entries(e)){if(!n.startsWith(`SANDBOX_ENV_`))continue;let e=n.slice(12);e&&(t[e]=r)}return t}function v(e){l&&n.spawnSync(o,[`-R`,`a+rwX`,e],{env:s})}function y(e,r){if(!l)return;let i=[];for(let n=r;i.push(n),n!==e&&t.dirname(n)!==n;n=t.dirname(n));n.spawnSync(o,[`a+X`,...i],{env:s})}function b(e=[`TERM`,`KILL`]){if(!l)return;let t=[];for(let n of e){let e=T([`pkill`,`-${n}`,`-u`,l]);e.error&&t.push(`pkill -${n}: ${e.error.message}`)}if(!e.includes(`KILL`)){if(t.length>0)throw Error(`failed to signal ${l} processes: ${t.join(`; `)}`);return}let n=x();if(n)throw Error(`failed to terminate ${l} processes: ${n}`)}function x(){for(let e=0;;e++){e>0&&(T([`pkill`,`-KILL`,`-u`,l]),Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,100));let t=n.spawnSync(`ps`,[`-o`,`pid=,stat=,nlwp=,comm=`,`-u`,l],{env:s,encoding:`utf8`}),r=t.stderr?.trim();if(t.error)return`cannot list processes: ${t.error.message}`;if(t.signal||t.status!==0&&(t.status!==1||r))return`cannot list processes: ${r||`ps exited with ${t.status??t.signal}`}`;let i=t.stdout.split(`
|
|
2
|
+
`).map(e=>e.trim()).filter(e=>e&&!/^\d+\s+Z\S*\s+1\s/.test(e));if(i.length===0)return;if(e>=10)return i.join(`, `)}}function S(e){l&&T([`chmod`,`-R`,`a+rwX`,e])}const C=5;function w(e){if(!l)return{cancel:()=>{},fired:()=>!1};let t=Math.ceil(e)+5,r=n.spawn(`/bin/sh`,[`-c`,`/bin/sleep ${t}; ${a} -u "$1" pkill -KILL -u "$1"`,`sh`,l],{detached:!0,stdio:`ignore`,env:s}),i=!1;r.on(`error`,()=>{i=!0});let o=!1;r.on(`exit`,()=>{o=!0}),r.unref();let c=Date.now(),u=!1;return{cancel:()=>{if(u=!0,!(r.pid===void 0||o))try{process.kill(-r.pid,`SIGKILL`)}catch{}},fired:()=>i||r.pid===void 0||!u&&Date.now()-c>=t*1e3}}function T(e){return n.spawnSync(a,[`-u`,l,...e],{env:s})}async function E(n){try{await e.promises.rm(n,{force:!0,recursive:!0})}catch(r){if(!l)throw r;S(n),T([`chmod`,`a+rwX`,t.dirname(n)]),await e.promises.rm(n,{force:!0,recursive:!0})}}export{g as SANDBOX_ENV_PREFIX,i as SANDBOX_USER_ENV_NAME,C as SANDBOX_WATCHDOG_GRACE_SECONDS,u as TIMEOUT_COMMAND,E as forceRemove,h as getSandboxUserEnvOverrides,c as getTrustedHelperEnv,p as isSandboxWrappedCommand,b as killSandboxUserProcesses,v as makeAccessibleToSandboxUser,y as makeTraversableBySandboxUser,m as prependInsideSandboxWrapper,S as relaxPermissionsAsSandboxUser,l as sandboxUserName,w as startSandboxTimeoutWatchdog,d as wrapCommandWithSandboxUser};
|
|
3
3
|
//# sourceMappingURL=sandboxUser.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandboxUser.js","names":[],"sources":["../../src/helpers/sandboxUser.ts"],"sourcesContent":["import child_process from 'node:child_process';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\n\n/**\n * Name of the environment variable through which a judge server tells problem-utils to run\n * untrusted submitted programs as the given unprivileged OS user via sudo.\n *\n * The judge server sets this variable only when it runs the judge harness (e.g. `judge.ts`) as its\n * own trusted user, so that problem files (test cases and the harness itself) stay unreadable to\n * submissions while the submissions themselves run under the sandbox user. When the variable is\n * absent or empty (local development, course authoring, or an all-sandbox judge run), commands run\n * as the current user like before.\n *\n * Contract for the delegating judge server (delegation is Linux-only — sudo user separation and\n * `/home/<user>` homes are provisioned in the judge Docker image; never set this on macOS):\n * - The harness process environment is forwarded to sandboxed submissions (sudo runs with\n * `--preserve-env`), so it must not contain secrets beyond what submissions may see. The one\n * exception is the set glibc strips when executing the setuid `sudo` (secure-execution mode:\n * `LD_*`, `TMPDIR`, `LOCPATH`, `NLSPATH`, `TZDIR`, … — see ld.so(8)); no sudoers setting can\n * recover those, and only `LD_LIBRARY_PATH` is restored, by the wrapper below.\n * - sudoers must let the harness user run arbitrary commands as the sandbox user without a\n * password, pass the environment through, and keep sudo off a pseudo-terminal (a pty would merge\n * stderr into stdout and CRLF-mangle output when the harness happens to run from a terminal):\n * e.g. `Defaults:<harness> !env_reset, !env_delete, !env_check, !secure_path, !use_pty` plus\n * `<harness> ALL=(<sandbox>) NOPASSWD:SETENV: ALL`.\n * - Variables meant for the submitted program rather than for the harness must be passed under\n * {@link SANDBOX_ENV_PREFIX}; problem-utils strips that prefix when it builds a submission's\n * environment.\n * - The sandbox user's home directory must exist at `/home/<sandbox user>` and be writable. It is\n * shared across sequential requests, so the server is responsible for resetting whatever\n * cross-request persistence there matters to it.\n */\nexport const SANDBOX_USER_ENV_NAME = 'EXERCODE_SANDBOX_USER';\n\n// Absolute paths so a submission-influenced `PATH` cannot redirect binaries that the trusted\n// harness user executes (`sudo` is also setuid and must be the real one).\nconst SUDO_PATH = '/usr/bin/sudo';\nconst CHMOD_PATH = '/bin/chmod';\nconst SLEEP_PATH = '/bin/sleep';\n// Minimal environment for trusted helper processes; never forward the harness environment to them.\nconst MINIMAL_ENV = { PATH: '/usr/local/bin:/usr/bin:/bin' } as const;\n\n/**\n * Environment for helper processes the *trusted* harness user runs (`ps`, `xwininfo`, `Xvfb`, …).\n * The judge server overlays caller-supplied variables onto the harness environment, and a sandboxed\n * submission can create executables in its persistent home, so resolving these helpers through the\n * inherited `PATH` would hand the submission code execution as the harness user. Node resolves the\n * command through the child environment's `PATH`, so a fixed `PATH` here pins them to system paths.\n */\nexport function getTrustedHelperEnv(extraEnv?: NodeJS.ProcessEnv): NodeJS.ProcessEnv {\n // Without delegation the submission runs as this same user, so there is no boundary to defend and\n // pinning `PATH` would only break setups whose X11/`ps` binaries live elsewhere (e.g. /usr/games,\n // /snap/bin, Nix). Keep the inherited environment there, exactly as before delegation existed.\n if (!sandboxUserName) return { ...process.env, ...extraEnv };\n // `PATH` last: a caller passing a whole environment through would otherwise reinstate the\n // submission-influenced one this function exists to replace.\n return { ...extraEnv, PATH: MINIMAL_ENV.PATH };\n}\n\nexport const sandboxUserName = process.env[SANDBOX_USER_ENV_NAME] || undefined;\n\n// Fail fast on the catastrophic misconfiguration where the sandbox user is the harness's own user:\n// cleanup (`killSandboxUserProcesses`) would then SIGKILL the harness itself on the first run.\nif (sandboxUserName && sandboxUserName === os.userInfo().username) {\n throw new Error(\n `${SANDBOX_USER_ENV_NAME} must name a different OS user than the one running the harness (got \"${sandboxUserName}\"). Leave it unset to run everything as the current user.`\n );\n}\n\n/**\n * The deadline supervisor. Absolute under delegation: the wrapper's `exec \"$0\"` resolves it through\n * the submission's own `PATH` (which a judge server may set via {@link SANDBOX_ENV_PREFIX}), so a\n * bare name would let a submission replace the very process that enforces its time limit. The bare\n * name is kept without delegation, where `timeout` may live elsewhere (Homebrew coreutils).\n */\nexport const TIMEOUT_COMMAND = sandboxUserName ? '/usr/bin/timeout' : 'timeout';\n\n/**\n * Wrap a command so it runs as the sandbox user. `umask 0` makes every file the sandboxed process\n * creates world-writable, so the harness user can clean it up without privileges. The wrapper also\n * restores `LD_LIBRARY_PATH`, which ld.so strips across the setuid `sudo` exec. Put `timeout`\n * inside the wrapped command: the harness user cannot signal the root-owned `sudo` process, so an\n * outer timer alone could not stop a runaway submission.\n */\nexport function wrapCommandWithSandboxUser(command: readonly [string, ...string[]]): [string, ...string[]] {\n if (!sandboxUserName) return [...command];\n // Idempotent; see {@link isSandboxWrappedCommand}.\n if (isSandboxWrappedCommand(command)) return [...command];\n return [...buildSandboxWrapperPrefix(sandboxUserName), ...command];\n}\n\nconst SANDBOX_WRAPPER_SCRIPT =\n 'umask 0; if [ -n \"$SANDBOX_LD_LIBRARY_PATH\" ]; then export LD_LIBRARY_PATH=\"$SANDBOX_LD_LIBRARY_PATH\"; fi; exec \"$0\" \"$@\"';\n\nfunction buildSandboxWrapperPrefix(user: string): [string, ...string[]] {\n return [SUDO_PATH, '--preserve-env', '-u', user, '--', 'sh', '-c', SANDBOX_WRAPPER_SCRIPT];\n}\n\n/**\n * Whether the command was already wrapped by {@link wrapCommandWithSandboxUser}. The presets hand\n * custom runners a wrapped command, and a runner may forward it to another helper that wraps too;\n * a nested wrapper's inner `sudo` would run AS the sandbox user, which sudoers does not authorize,\n * so every test case of such a problem would fail only under delegation.\n *\n * Matches the wrapper prefix exactly rather than looking for `sudo` anywhere: a submission-derived\n * command could otherwise carry a literal `/usr/bin/sudo` argument and skip wrapping entirely,\n * which would run the submission as the trusted harness user.\n */\nexport function isSandboxWrappedCommand(command: readonly string[]): boolean {\n if (!sandboxUserName) return false;\n const prefix = buildSandboxWrapperPrefix(sandboxUserName);\n return prefix.every((argument, index) => command[index] === argument);\n}\n\n/**\n * Insert `innerPrefix` (e.g. a `time` measurement prefix) so that it runs INSIDE the sandbox\n * wrapper when `command` is wrapped, and simply in front otherwise. Prefixing a wrapped command\n * from the outside would run `innerPrefix` as the trusted harness user while its arguments (such\n * as an output path) point into a sandbox-writable directory, where a submission can plant a\n * symlink and have the harness truncate an arbitrary file it owns.\n */\nexport function prependInsideSandboxWrapper(\n command: readonly [string, ...string[]],\n innerPrefix: readonly string[]\n): [string, ...string[]] {\n if (!isSandboxWrappedCommand(command)) return [...innerPrefix, ...command] as [string, ...string[]];\n const prefixLength = buildSandboxWrapperPrefix(sandboxUserName as string).length;\n return [...command.slice(0, prefixLength), ...innerPrefix, ...command.slice(prefixLength)] as [string, ...string[]];\n}\n\n/**\n * Environment overrides for sandboxed processes: a writable home and the `LD_LIBRARY_PATH`\n * smuggled past the setuid `sudo` exec (see {@link wrapCommandWithSandboxUser}). Pass the\n * environment the command will actually run with so a caller-supplied `LD_LIBRARY_PATH` survives\n * the exec; the harness's own value is only the fallback.\n */\nexport function getSandboxUserEnvOverrides(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv {\n if (!sandboxUserName) return {};\n const sourceEnv = env ?? process.env;\n const submissionOnlyEnv = unwrapSubmissionOnlyEnv(sourceEnv);\n // A submission-only `LD_LIBRARY_PATH` must be the one smuggled past the exec, not the harness's.\n const ldLibraryPath = submissionOnlyEnv.LD_LIBRARY_PATH ?? sourceEnv.LD_LIBRARY_PATH ?? process.env.LD_LIBRARY_PATH;\n return {\n HOME: `/home/${sandboxUserName}`,\n ...submissionOnlyEnv,\n // Last: the wrapper reads this one, so a caller must not be able to overwrite it.\n ...(ldLibraryPath && { SANDBOX_LD_LIBRARY_PATH: ldLibraryPath }),\n };\n}\n\n/**\n * Prefix under which a delegating judge server passes variables that belong to the submitted\n * program alone. Applying a request's variables to the harness itself would let a submission point\n * e.g. `PATH` or `NODE_OPTIONS` at its own files and get code executed as the trusted harness user,\n * so the server prefixes them and problem-utils strips the prefix back off here, where the\n * environment of a sandboxed submission is built.\n */\nexport const SANDBOX_ENV_PREFIX = 'SANDBOX_ENV_';\n\nfunction unwrapSubmissionOnlyEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {\n const unwrapped: NodeJS.ProcessEnv = {};\n for (const [name, value] of Object.entries(env)) {\n if (!name.startsWith(SANDBOX_ENV_PREFIX)) continue;\n const unwrappedName = name.slice(SANDBOX_ENV_PREFIX.length);\n if (unwrappedName) unwrapped[unwrappedName] = value;\n }\n return unwrapped;\n}\n\n/**\n * Make harness-user-created files under the given path readable, and directories writable, for the\n * sandbox user, so sandboxed programs can read their sources and create outputs next to them.\n */\nexport function makeAccessibleToSandboxUser(targetPath: string): void {\n if (!sandboxUserName) return;\n // Files owned by the sandbox user fail to chmod but are already world-writable (umask 0).\n child_process.spawnSync(CHMOD_PATH, ['-R', 'a+rwX', targetPath], { env: MINIMAL_ENV });\n}\n\n/**\n * Kill the sandbox user's processes with the given signals. The harness user cannot signal another\n * user's processes (nor the root-owned `sudo` wrapper), so this goes through sudo. Killing every\n * sandbox process is safe because the judge server handles one request at a time. The default\n * sends SIGTERM immediately followed by SIGKILL (final cleanup); callers that want a grace period\n * send `['TERM']`, wait, and then send `['KILL']`.\n *\n * Fails closed: a `['TERM']`-only sweep throws when its `sudo` could not be spawned (e.g. a\n * submission exhausted the PID cgroup); a sweep including SIGKILL throws when a sandbox process is\n * still alive, or cannot be listed, afterwards. Reporting success would leave that process running\n * next to the next request on this instance.\n */\nexport function killSandboxUserProcesses(signals: readonly ('TERM' | 'KILL')[] = ['TERM', 'KILL']): void {\n if (!sandboxUserName) return;\n // One direct call per signal instead of one `sh -c 'pkill ...; pkill ...'`: the wrapping shell\n // would run as the sandbox user too, so the first pkill would kill it before the second ran.\n // Exit statuses are not inspected: `sudo` exits with 1 both when `pkill` matched nothing and on\n // its own failures, and a sweep can itself be killed by a concurrent watchdog sweep\n // ({@link startSandboxTimeoutWatchdog}'s `pkill -KILL` matches it). What matters is the outcome.\n const spawnErrors: string[] = [];\n for (const signal of signals) {\n const result = runAsSandboxUser(['pkill', `-${signal}`, '-u', sandboxUserName]);\n if (result.error) spawnErrors.push(`pkill -${signal}: ${result.error.message}`);\n }\n if (!signals.includes('KILL')) {\n // Survivors are expected during the caller's grace period, so only a sweep that never started\n // is a failure here.\n if (spawnErrors.length > 0) {\n throw new Error(`failed to signal ${sandboxUserName} processes: ${spawnErrors.join('; ')}`);\n }\n return;\n }\n const survivors = findSurvivingSandboxUserProcesses();\n if (survivors) throw new Error(`failed to terminate ${sandboxUserName} processes: ${survivors}`);\n}\n\n/**\n * Lists the sandbox user's live processes as the harness user (listing needs no privilege), or\n * `undefined` when none remain. SIGKILL is re-sent between attempts: a child forked after `pkill`\n * scanned `/proc` was never signalled, and a process in uninterruptible sleep dies only once its\n * I/O completes. Just-killed processes linger as zombies until reaped; a zombie row is ignored only\n * when it has a single thread, because a thread-group leader that exited via `pthread_exit` shows\n * as a zombie while its other threads keep running.\n */\nfunction findSurvivingSandboxUserProcesses(): string | undefined {\n for (let attempt = 0; ; attempt++) {\n if (attempt > 0) {\n runAsSandboxUser(['pkill', '-KILL', '-u', sandboxUserName as string]);\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);\n }\n const result = child_process.spawnSync('ps', ['-o', 'pid=,stat=,nlwp=,comm=', '-u', sandboxUserName as string], {\n env: MINIMAL_ENV,\n encoding: 'utf8',\n });\n // `ps` exits with 1 both when no process matched and on its own errors, which it reports on\n // stderr; anything else is a failure to list, which must count as a failure to verify.\n const stderr = result.stderr?.trim();\n if (result.error) return `cannot list processes: ${result.error.message}`;\n if (result.signal || (result.status !== 0 && (result.status !== 1 || stderr))) {\n return `cannot list processes: ${stderr || `ps exited with ${result.status ?? result.signal}`}`;\n }\n const survivors = result.stdout\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line && !/^\\d+\\s+Z\\S*\\s+1\\s/.test(line));\n if (survivors.length === 0) return undefined;\n if (attempt >= 10) return survivors.join(', ');\n }\n}\n\n/**\n * Let the sandbox user reopen the permissions of its own files under the given path, for\n * harness-side traversal/cleanup of trees where a sandboxed process restricted permissions\n * (some tools chmod their outputs regardless of umask).\n */\nexport function relaxPermissionsAsSandboxUser(targetPath: string): void {\n if (!sandboxUserName) return;\n runAsSandboxUser(['chmod', '-R', 'a+rwX', targetPath]);\n}\n\n/**\n * How long after a command's own deadline {@link startSandboxTimeoutWatchdog} force-kills the\n * sandbox user. Callers waiting for the watchdog to end a submission must wait at least this long.\n */\nexport const SANDBOX_WATCHDOG_GRACE_SECONDS = 5;\n\n/** Cancels a {@link startSandboxTimeoutWatchdog}; `fired` reports whether its deadline elapsed. */\nexport interface SandboxTimeoutWatchdog {\n cancel(): void;\n fired(): boolean;\n}\n\n/**\n * Start a harness-owned watchdog that force-kills every sandbox process after the given deadline.\n * A sandboxed submission can signal its own `timeout` supervisor (same UID), and a synchronous\n * spawn blocks the harness's event loop, so without this external deadline a submission could run\n * until the outer judge-server limit. The watchdog's `sh`/`sleep` run as the harness user, out of\n * the submission's reach; killing the watchdog's process group cancels it before it spawns sudo.\n *\n * ALWAYS cancel in a `finally`: the watchdog is detached and `unref`'d, so a leaked one keeps\n * running after the harness exits and would SIGKILL a later request's submission.\n */\nexport function startSandboxTimeoutWatchdog(timeoutSeconds: number): SandboxTimeoutWatchdog {\n if (!sandboxUserName) return { cancel: () => {}, fired: () => false };\n\n const deadlineSeconds = Math.ceil(timeoutSeconds) + SANDBOX_WATCHDOG_GRACE_SECONDS;\n const watchdog = child_process.spawn(\n '/bin/sh',\n ['-c', `${SLEEP_PATH} ${deadlineSeconds}; ${SUDO_PATH} -u \"$1\" pkill -KILL -u \"$1\"`, 'sh', sandboxUserName],\n { detached: true, stdio: 'ignore', env: MINIMAL_ENV }\n );\n // Without a listener, a spawn failure (e.g. the submission exhausted the PID cgroup) would be an\n // unhandled 'error' event that terminates the harness. Fail closed instead: callers check\n // `fired()`, and a watchdog that never started reports its deadline as elapsed.\n let spawnFailed = false;\n watchdog.on('error', () => {\n spawnFailed = true;\n });\n let exited = false;\n watchdog.on('exit', () => {\n exited = true;\n });\n watchdog.unref();\n\n const startTimeMilliseconds = Date.now();\n let cancelled = false;\n return {\n cancel: () => {\n cancelled = true;\n if (watchdog.pid === undefined || exited) return;\n try {\n process.kill(-watchdog.pid, 'SIGKILL');\n } catch {\n // The watchdog already fired and exited.\n }\n },\n // The `exit`/`error` events cannot be observed from a synchronous caller (they need the event\n // loop), so decide by elapsed time, which is what the watchdog itself waits on.\n fired: () =>\n spawnFailed ||\n watchdog.pid === undefined ||\n (!cancelled && Date.now() - startTimeMilliseconds >= deadlineSeconds * 1000),\n };\n}\n\n/**\n * Run a helper as the sandbox user with a minimal environment. The harness environment must not be\n * passed: sudoers forwards it verbatim, and after sudo drops privileges the helper's\n * `/proc/<pid>/environ` becomes readable by every other sandbox process.\n */\nfunction runAsSandboxUser(command: [string, ...string[]]): child_process.SpawnSyncReturns<Buffer> {\n return child_process.spawnSync(SUDO_PATH, ['-u', sandboxUserName as string, ...command], { env: MINIMAL_ENV });\n}\n\n/**\n * `fs.rm`-like removal with a fallback for trees holding sandbox-user-owned entries whose\n * permissions block deletion: let the sandbox user reopen its own files first, then retry. The\n * containing directory is relaxed as well — unlinking an entry needs write permission on its\n * parent, so a submission that chmods a directory it owns would otherwise make everything inside it\n * undeletable.\n */\nexport async function forceRemove(targetPath: string): Promise<void> {\n try {\n await fs.promises.rm(targetPath, { force: true, recursive: true });\n } catch (error) {\n if (!sandboxUserName) throw error;\n relaxPermissionsAsSandboxUser(targetPath);\n // Not recursive: only the containing directory's own write bit governs the unlink.\n runAsSandboxUser(['chmod', 'a+rwX', path.dirname(targetPath)]);\n await fs.promises.rm(targetPath, { force: true, recursive: true });\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sandboxUser.js","names":[],"sources":["../../src/helpers/sandboxUser.ts"],"sourcesContent":["import child_process from 'node:child_process';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\n\n/**\n * Name of the environment variable through which a judge server tells problem-utils to run\n * untrusted submitted programs as the given unprivileged OS user via sudo.\n *\n * The judge server sets this variable only when it runs the judge harness (e.g. `judge.ts`) as its\n * own trusted user, so that problem files (test cases and the harness itself) stay unreadable to\n * submissions while the submissions themselves run under the sandbox user. When the variable is\n * absent or empty (local development, course authoring, or an all-sandbox judge run), commands run\n * as the current user like before.\n *\n * Contract for the delegating judge server (delegation is Linux-only — sudo user separation and\n * `/home/<user>` homes are provisioned in the judge Docker image; never set this on macOS):\n * - The harness process environment is forwarded to sandboxed submissions (sudo runs with\n * `--preserve-env`), so it must not contain secrets beyond what submissions may see. The one\n * exception is the set glibc strips when executing the setuid `sudo` (secure-execution mode:\n * `LD_*`, `TMPDIR`, `LOCPATH`, `NLSPATH`, `TZDIR`, … — see ld.so(8)); no sudoers setting can\n * recover those, and only `LD_LIBRARY_PATH` is restored, by the wrapper below.\n * - sudoers must let the harness user run arbitrary commands as the sandbox user without a\n * password, pass the environment through, and keep sudo off a pseudo-terminal (a pty would merge\n * stderr into stdout and CRLF-mangle output when the harness happens to run from a terminal):\n * e.g. `Defaults:<harness> !env_reset, !env_delete, !env_check, !secure_path, !use_pty` plus\n * `<harness> ALL=(<sandbox>) NOPASSWD:SETENV: ALL`.\n * - Variables meant for the submitted program rather than for the harness must be passed under\n * {@link SANDBOX_ENV_PREFIX}; problem-utils strips that prefix when it builds a submission's\n * environment.\n * - The sandbox user's home directory must exist at `/home/<sandbox user>` and be writable. It is\n * shared across sequential requests, so the server is responsible for resetting whatever\n * cross-request persistence there matters to it.\n */\nexport const SANDBOX_USER_ENV_NAME = 'EXERCODE_SANDBOX_USER';\n\n// Absolute paths so a submission-influenced `PATH` cannot redirect binaries that the trusted\n// harness user executes (`sudo` is also setuid and must be the real one).\nconst SUDO_PATH = '/usr/bin/sudo';\nconst CHMOD_PATH = '/bin/chmod';\nconst SLEEP_PATH = '/bin/sleep';\n// Minimal environment for trusted helper processes; never forward the harness environment to them.\nconst MINIMAL_ENV = { PATH: '/usr/local/bin:/usr/bin:/bin' } as const;\n\n/**\n * Environment for helper processes the *trusted* harness user runs (`ps`, `xwininfo`, `Xvfb`, …).\n * The judge server overlays caller-supplied variables onto the harness environment, and a sandboxed\n * submission can create executables in its persistent home, so resolving these helpers through the\n * inherited `PATH` would hand the submission code execution as the harness user. Node resolves the\n * command through the child environment's `PATH`, so a fixed `PATH` here pins them to system paths.\n */\nexport function getTrustedHelperEnv(extraEnv?: NodeJS.ProcessEnv): NodeJS.ProcessEnv {\n // Without delegation the submission runs as this same user, so there is no boundary to defend and\n // pinning `PATH` would only break setups whose X11/`ps` binaries live elsewhere (e.g. /usr/games,\n // /snap/bin, Nix). Keep the inherited environment there, exactly as before delegation existed.\n if (!sandboxUserName) return { ...process.env, ...extraEnv };\n // `PATH` last: a caller passing a whole environment through would otherwise reinstate the\n // submission-influenced one this function exists to replace.\n return { ...extraEnv, PATH: MINIMAL_ENV.PATH };\n}\n\nexport const sandboxUserName = process.env[SANDBOX_USER_ENV_NAME] || undefined;\n\n// Fail fast on the catastrophic misconfiguration where the sandbox user is the harness's own user:\n// cleanup (`killSandboxUserProcesses`) would then SIGKILL the harness itself on the first run.\nif (sandboxUserName && sandboxUserName === os.userInfo().username) {\n throw new Error(\n `${SANDBOX_USER_ENV_NAME} must name a different OS user than the one running the harness (got \"${sandboxUserName}\"). Leave it unset to run everything as the current user.`\n );\n}\n\n/**\n * The deadline supervisor. Absolute under delegation: the wrapper's `exec \"$0\"` resolves it through\n * the submission's own `PATH` (which a judge server may set via {@link SANDBOX_ENV_PREFIX}), so a\n * bare name would let a submission replace the very process that enforces its time limit. The bare\n * name is kept without delegation, where `timeout` may live elsewhere (Homebrew coreutils).\n */\nexport const TIMEOUT_COMMAND = sandboxUserName ? '/usr/bin/timeout' : 'timeout';\n\n/**\n * Wrap a command so it runs as the sandbox user. `umask 0` makes every file the sandboxed process\n * creates world-writable, so the harness user can clean it up without privileges. The wrapper also\n * restores `LD_LIBRARY_PATH`, which ld.so strips across the setuid `sudo` exec. Put `timeout`\n * inside the wrapped command: the harness user cannot signal the root-owned `sudo` process, so an\n * outer timer alone could not stop a runaway submission.\n */\nexport function wrapCommandWithSandboxUser(command: readonly [string, ...string[]]): [string, ...string[]] {\n if (!sandboxUserName) return [...command];\n // Idempotent; see {@link isSandboxWrappedCommand}.\n if (isSandboxWrappedCommand(command)) return [...command];\n return [...buildSandboxWrapperPrefix(sandboxUserName), ...command];\n}\n\nconst SANDBOX_WRAPPER_SCRIPT =\n 'umask 0; if [ -n \"$SANDBOX_LD_LIBRARY_PATH\" ]; then export LD_LIBRARY_PATH=\"$SANDBOX_LD_LIBRARY_PATH\"; fi; exec \"$0\" \"$@\"';\n\nfunction buildSandboxWrapperPrefix(user: string): [string, ...string[]] {\n return [SUDO_PATH, '--preserve-env', '-u', user, '--', 'sh', '-c', SANDBOX_WRAPPER_SCRIPT];\n}\n\n/**\n * Whether the command was already wrapped by {@link wrapCommandWithSandboxUser}. The presets hand\n * custom runners a wrapped command, and a runner may forward it to another helper that wraps too;\n * a nested wrapper's inner `sudo` would run AS the sandbox user, which sudoers does not authorize,\n * so every test case of such a problem would fail only under delegation.\n *\n * Matches the wrapper prefix exactly rather than looking for `sudo` anywhere: a submission-derived\n * command could otherwise carry a literal `/usr/bin/sudo` argument and skip wrapping entirely,\n * which would run the submission as the trusted harness user.\n */\nexport function isSandboxWrappedCommand(command: readonly string[]): boolean {\n if (!sandboxUserName) return false;\n const prefix = buildSandboxWrapperPrefix(sandboxUserName);\n return prefix.every((argument, index) => command[index] === argument);\n}\n\n/**\n * Insert `innerPrefix` (e.g. a `time` measurement prefix) so that it runs INSIDE the sandbox\n * wrapper when `command` is wrapped, and simply in front otherwise. Prefixing a wrapped command\n * from the outside would run `innerPrefix` as the trusted harness user while its arguments (such\n * as an output path) point into a sandbox-writable directory, where a submission can plant a\n * symlink and have the harness truncate an arbitrary file it owns.\n */\nexport function prependInsideSandboxWrapper(\n command: readonly [string, ...string[]],\n innerPrefix: readonly string[]\n): [string, ...string[]] {\n if (!isSandboxWrappedCommand(command)) return [...innerPrefix, ...command] as [string, ...string[]];\n const prefixLength = buildSandboxWrapperPrefix(sandboxUserName as string).length;\n return [...command.slice(0, prefixLength), ...innerPrefix, ...command.slice(prefixLength)] as [string, ...string[]];\n}\n\n/**\n * Environment overrides for sandboxed processes: a writable home and the `LD_LIBRARY_PATH`\n * smuggled past the setuid `sudo` exec (see {@link wrapCommandWithSandboxUser}). Pass the\n * environment the command will actually run with so a caller-supplied `LD_LIBRARY_PATH` survives\n * the exec; the harness's own value is only the fallback.\n */\nexport function getSandboxUserEnvOverrides(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv {\n if (!sandboxUserName) return {};\n const sourceEnv = env ?? process.env;\n const submissionOnlyEnv = unwrapSubmissionOnlyEnv(sourceEnv);\n // A submission-only `LD_LIBRARY_PATH` must be the one smuggled past the exec, not the harness's.\n const ldLibraryPath = submissionOnlyEnv.LD_LIBRARY_PATH ?? sourceEnv.LD_LIBRARY_PATH ?? process.env.LD_LIBRARY_PATH;\n return {\n HOME: `/home/${sandboxUserName}`,\n ...submissionOnlyEnv,\n // Last: the wrapper reads this one, so a caller must not be able to overwrite it.\n ...(ldLibraryPath && { SANDBOX_LD_LIBRARY_PATH: ldLibraryPath }),\n };\n}\n\n/**\n * Prefix under which a delegating judge server passes variables that belong to the submitted\n * program alone. Applying a request's variables to the harness itself would let a submission point\n * e.g. `PATH` or `NODE_OPTIONS` at its own files and get code executed as the trusted harness user,\n * so the server prefixes them and problem-utils strips the prefix back off here, where the\n * environment of a sandboxed submission is built.\n */\nexport const SANDBOX_ENV_PREFIX = 'SANDBOX_ENV_';\n\nfunction unwrapSubmissionOnlyEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {\n const unwrapped: NodeJS.ProcessEnv = {};\n for (const [name, value] of Object.entries(env)) {\n if (!name.startsWith(SANDBOX_ENV_PREFIX)) continue;\n const unwrappedName = name.slice(SANDBOX_ENV_PREFIX.length);\n if (unwrappedName) unwrapped[unwrappedName] = value;\n }\n return unwrapped;\n}\n\n/**\n * Make harness-user-created files under the given path readable, and directories writable, for the\n * sandbox user, so sandboxed programs can read their sources and create outputs next to them.\n */\nexport function makeAccessibleToSandboxUser(targetPath: string): void {\n if (!sandboxUserName) return;\n // Files owned by the sandbox user fail to chmod but are already world-writable (umask 0).\n child_process.spawnSync(CHMOD_PATH, ['-R', 'a+rwX', targetPath], { env: MINIMAL_ENV });\n}\n\n/**\n * Let the sandbox user traverse (but not modify) every directory from `rootPath` down to\n * `directoryPath`, e.g. the synthesized ancestors of a temporary copy.\n */\nexport function makeTraversableBySandboxUser(rootPath: string, directoryPath: string): void {\n if (!sandboxUserName) return;\n const directories: string[] = [];\n for (let current = directoryPath; ; current = path.dirname(current)) {\n directories.push(current);\n if (current === rootPath || path.dirname(current) === current) break;\n }\n child_process.spawnSync(CHMOD_PATH, ['a+X', ...directories], { env: MINIMAL_ENV });\n}\n\n/**\n * Kill the sandbox user's processes with the given signals. The harness user cannot signal another\n * user's processes (nor the root-owned `sudo` wrapper), so this goes through sudo. Killing every\n * sandbox process is safe because the judge server handles one request at a time. The default\n * sends SIGTERM immediately followed by SIGKILL (final cleanup); callers that want a grace period\n * send `['TERM']`, wait, and then send `['KILL']`.\n *\n * Fails closed: a `['TERM']`-only sweep throws when its `sudo` could not be spawned (e.g. a\n * submission exhausted the PID cgroup); a sweep including SIGKILL throws when a sandbox process is\n * still alive, or cannot be listed, afterwards. Reporting success would leave that process running\n * next to the next request on this instance.\n */\nexport function killSandboxUserProcesses(signals: readonly ('TERM' | 'KILL')[] = ['TERM', 'KILL']): void {\n if (!sandboxUserName) return;\n // One direct call per signal instead of one `sh -c 'pkill ...; pkill ...'`: the wrapping shell\n // would run as the sandbox user too, so the first pkill would kill it before the second ran.\n // Exit statuses are not inspected: `sudo` exits with 1 both when `pkill` matched nothing and on\n // its own failures, and a sweep can itself be killed by a concurrent watchdog sweep\n // ({@link startSandboxTimeoutWatchdog}'s `pkill -KILL` matches it). What matters is the outcome.\n const spawnErrors: string[] = [];\n for (const signal of signals) {\n const result = runAsSandboxUser(['pkill', `-${signal}`, '-u', sandboxUserName]);\n if (result.error) spawnErrors.push(`pkill -${signal}: ${result.error.message}`);\n }\n if (!signals.includes('KILL')) {\n // Survivors are expected during the caller's grace period, so only a sweep that never started\n // is a failure here.\n if (spawnErrors.length > 0) {\n throw new Error(`failed to signal ${sandboxUserName} processes: ${spawnErrors.join('; ')}`);\n }\n return;\n }\n const survivors = findSurvivingSandboxUserProcesses();\n if (survivors) throw new Error(`failed to terminate ${sandboxUserName} processes: ${survivors}`);\n}\n\n/**\n * Lists the sandbox user's live processes as the harness user (listing needs no privilege), or\n * `undefined` when none remain. SIGKILL is re-sent between attempts: a child forked after `pkill`\n * scanned `/proc` was never signalled, and a process in uninterruptible sleep dies only once its\n * I/O completes. Just-killed processes linger as zombies until reaped; a zombie row is ignored only\n * when it has a single thread, because a thread-group leader that exited via `pthread_exit` shows\n * as a zombie while its other threads keep running.\n */\nfunction findSurvivingSandboxUserProcesses(): string | undefined {\n for (let attempt = 0; ; attempt++) {\n if (attempt > 0) {\n runAsSandboxUser(['pkill', '-KILL', '-u', sandboxUserName as string]);\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);\n }\n const result = child_process.spawnSync('ps', ['-o', 'pid=,stat=,nlwp=,comm=', '-u', sandboxUserName as string], {\n env: MINIMAL_ENV,\n encoding: 'utf8',\n });\n // `ps` exits with 1 both when no process matched and on its own errors, which it reports on\n // stderr; anything else is a failure to list, which must count as a failure to verify.\n const stderr = result.stderr?.trim();\n if (result.error) return `cannot list processes: ${result.error.message}`;\n if (result.signal || (result.status !== 0 && (result.status !== 1 || stderr))) {\n return `cannot list processes: ${stderr || `ps exited with ${result.status ?? result.signal}`}`;\n }\n const survivors = result.stdout\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line && !/^\\d+\\s+Z\\S*\\s+1\\s/.test(line));\n if (survivors.length === 0) return undefined;\n if (attempt >= 10) return survivors.join(', ');\n }\n}\n\n/**\n * Let the sandbox user reopen the permissions of its own files under the given path, for\n * harness-side traversal/cleanup of trees where a sandboxed process restricted permissions\n * (some tools chmod their outputs regardless of umask).\n */\nexport function relaxPermissionsAsSandboxUser(targetPath: string): void {\n if (!sandboxUserName) return;\n runAsSandboxUser(['chmod', '-R', 'a+rwX', targetPath]);\n}\n\n/**\n * How long after a command's own deadline {@link startSandboxTimeoutWatchdog} force-kills the\n * sandbox user. Callers waiting for the watchdog to end a submission must wait at least this long.\n */\nexport const SANDBOX_WATCHDOG_GRACE_SECONDS = 5;\n\n/** Cancels a {@link startSandboxTimeoutWatchdog}; `fired` reports whether its deadline elapsed. */\nexport interface SandboxTimeoutWatchdog {\n cancel(): void;\n fired(): boolean;\n}\n\n/**\n * Start a harness-owned watchdog that force-kills every sandbox process after the given deadline.\n * A sandboxed submission can signal its own `timeout` supervisor (same UID), and a synchronous\n * spawn blocks the harness's event loop, so without this external deadline a submission could run\n * until the outer judge-server limit. The watchdog's `sh`/`sleep` run as the harness user, out of\n * the submission's reach; killing the watchdog's process group cancels it before it spawns sudo.\n *\n * ALWAYS cancel in a `finally`: the watchdog is detached and `unref`'d, so a leaked one keeps\n * running after the harness exits and would SIGKILL a later request's submission.\n */\nexport function startSandboxTimeoutWatchdog(timeoutSeconds: number): SandboxTimeoutWatchdog {\n if (!sandboxUserName) return { cancel: () => {}, fired: () => false };\n\n const deadlineSeconds = Math.ceil(timeoutSeconds) + SANDBOX_WATCHDOG_GRACE_SECONDS;\n const watchdog = child_process.spawn(\n '/bin/sh',\n ['-c', `${SLEEP_PATH} ${deadlineSeconds}; ${SUDO_PATH} -u \"$1\" pkill -KILL -u \"$1\"`, 'sh', sandboxUserName],\n { detached: true, stdio: 'ignore', env: MINIMAL_ENV }\n );\n // Without a listener, a spawn failure (e.g. the submission exhausted the PID cgroup) would be an\n // unhandled 'error' event that terminates the harness. Fail closed instead: callers check\n // `fired()`, and a watchdog that never started reports its deadline as elapsed.\n let spawnFailed = false;\n watchdog.on('error', () => {\n spawnFailed = true;\n });\n let exited = false;\n watchdog.on('exit', () => {\n exited = true;\n });\n watchdog.unref();\n\n const startTimeMilliseconds = Date.now();\n let cancelled = false;\n return {\n cancel: () => {\n cancelled = true;\n if (watchdog.pid === undefined || exited) return;\n try {\n process.kill(-watchdog.pid, 'SIGKILL');\n } catch {\n // The watchdog already fired and exited.\n }\n },\n // The `exit`/`error` events cannot be observed from a synchronous caller (they need the event\n // loop), so decide by elapsed time, which is what the watchdog itself waits on.\n fired: () =>\n spawnFailed ||\n watchdog.pid === undefined ||\n (!cancelled && Date.now() - startTimeMilliseconds >= deadlineSeconds * 1000),\n };\n}\n\n/**\n * Run a helper as the sandbox user with a minimal environment. The harness environment must not be\n * passed: sudoers forwards it verbatim, and after sudo drops privileges the helper's\n * `/proc/<pid>/environ` becomes readable by every other sandbox process.\n */\nfunction runAsSandboxUser(command: [string, ...string[]]): child_process.SpawnSyncReturns<Buffer> {\n return child_process.spawnSync(SUDO_PATH, ['-u', sandboxUserName as string, ...command], { env: MINIMAL_ENV });\n}\n\n/**\n * `fs.rm`-like removal with a fallback for trees holding sandbox-user-owned entries whose\n * permissions block deletion: let the sandbox user reopen its own files first, then retry. The\n * containing directory is relaxed as well — unlinking an entry needs write permission on its\n * parent, so a submission that chmods a directory it owns would otherwise make everything inside it\n * undeletable.\n */\nexport async function forceRemove(targetPath: string): Promise<void> {\n try {\n await fs.promises.rm(targetPath, { force: true, recursive: true });\n } catch (error) {\n if (!sandboxUserName) throw error;\n relaxPermissionsAsSandboxUser(targetPath);\n // Not recursive: only the containing directory's own write bit governs the unlink.\n runAsSandboxUser(['chmod', 'a+rwX', path.dirname(targetPath)]);\n await fs.promises.rm(targetPath, { force: true, recursive: true });\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("./sandboxUser.cjs"),n=require("./safeFs.cjs");let r=require("node:
|
|
1
|
+
"use strict";const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("./sandboxUser.cjs"),n=require("./safeFs.cjs");let r=require("node:fs");r=e.__toESM(r,1);let i=require("node:path");i=e.__toESM(i,1);let a=require("node:http");a=e.__toESM(a,1);function o(e){let t=a.default.createServer((t,n)=>{let a=new URL(t.url??`/`,`http://127.0.0.1`).pathname;a===`/`&&(a=`/index.html`);let o=a.endsWith(`/`)?i.default.join(a,`index.html`):a,s=i.default.join(e,o);if(!c(e,s)){n.statusCode=404,n.end();return}if(r.default.existsSync(s))try{let e=r.default.readFileSync(s);n.writeHead(200,{"Access-Control-Allow-Origin":`*`,Pragma:`no-cache`,"Cache-Control":`no-cache`,"Content-Type":l(o)}),n.end(e)}catch{n.statusCode=500,n.end()}else n.statusCode=404,n.end()});t.listen();let n=t.address();if(!n)throw Error(`server has been unexpectedly closed`);return{[Symbol.asyncDispose]:async()=>{t.closeAllConnections(),await new Promise(e=>{t.close(()=>{e()})})},url:typeof n==`string`?n:`http://127.0.0.1:${n.port}`,port:typeof n==`object`?n.port:void 0}}const s={".css":`text/css`,".html":`text/html`,".js":`text/javascript`,".gif":`image/gif`,".jpg":`image/jpeg`,".png":`image/png`,".svg":`image/svg+xml`};function c(e,i){if(!t.sandboxUserName)return!0;try{return n.isContainedPath(r.default.realpathSync(e),r.default.realpathSync(i))}catch{return!r.default.existsSync(i)}}function l(e){for(let[t,n]of Object.entries(s))if(e.endsWith(t))return n;return`text/plain`}exports.startHttpServer=o;
|
|
2
2
|
//# sourceMappingURL=startHttpServer.cjs.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{sandboxUserName as e}from"./sandboxUser.js";import{isContainedPath as t}from"./safeFs.js";import n from"node:
|
|
1
|
+
import{sandboxUserName as e}from"./sandboxUser.js";import{isContainedPath as t}from"./safeFs.js";import n from"node:fs";import r from"node:path";import i from"node:http";function a(e){let t=i.createServer((t,i)=>{let a=new URL(t.url??`/`,`http://127.0.0.1`).pathname;a===`/`&&(a=`/index.html`);let o=a.endsWith(`/`)?r.join(a,`index.html`):a,l=r.join(e,o);if(!s(e,l)){i.statusCode=404,i.end();return}if(n.existsSync(l))try{let e=n.readFileSync(l);i.writeHead(200,{"Access-Control-Allow-Origin":`*`,Pragma:`no-cache`,"Cache-Control":`no-cache`,"Content-Type":c(o)}),i.end(e)}catch{i.statusCode=500,i.end()}else i.statusCode=404,i.end()});t.listen();let a=t.address();if(!a)throw Error(`server has been unexpectedly closed`);return{[Symbol.asyncDispose]:async()=>{t.closeAllConnections(),await new Promise(e=>{t.close(()=>{e()})})},url:typeof a==`string`?a:`http://127.0.0.1:${a.port}`,port:typeof a==`object`?a.port:void 0}}const o={".css":`text/css`,".html":`text/html`,".js":`text/javascript`,".gif":`image/gif`,".jpg":`image/jpeg`,".png":`image/png`,".svg":`image/svg+xml`};function s(r,i){if(!e)return!0;try{return t(n.realpathSync(r),n.realpathSync(i))}catch{return!n.existsSync(i)}}function c(e){for(let[t,n]of Object.entries(o))if(e.endsWith(t))return n;return`text/plain`}export{a as startHttpServer};
|
|
2
2
|
//# sourceMappingURL=startHttpServer.js.map
|