@exercode/problem-utils 2.4.3 → 3.0.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/dist/cli/checkAllProblems.cjs +1 -1
- package/dist/cli/checkAllProblems.cjs.map +1 -1
- package/dist/cli/checkAllProblems.js +1 -1
- package/dist/cli/checkAllProblems.js.map +1 -1
- package/dist/helpers/checkProblemDirIsolation.cjs +1 -1
- package/dist/helpers/checkProblemDirIsolation.js +1 -1
- package/dist/helpers/cleanWorkingDirectory.cjs +1 -1
- package/dist/helpers/cleanWorkingDirectory.cjs.map +1 -1
- package/dist/helpers/cleanWorkingDirectory.d.ts +2 -6
- package/dist/helpers/cleanWorkingDirectory.js +1 -1
- package/dist/helpers/cleanWorkingDirectory.js.map +1 -1
- package/dist/helpers/compareExpectedOutputFiles.cjs +1 -1
- package/dist/helpers/compareExpectedOutputFiles.cjs.map +1 -1
- package/dist/helpers/compareExpectedOutputFiles.js +1 -1
- package/dist/helpers/compareExpectedOutputFiles.js.map +1 -1
- package/dist/helpers/copyTestCaseFileInput.cjs +1 -1
- package/dist/helpers/copyTestCaseFileInput.cjs.map +1 -1
- package/dist/helpers/copyTestCaseFileInput.js +1 -1
- package/dist/helpers/copyTestCaseFileInput.js.map +1 -1
- package/dist/helpers/readOutputFiles.cjs +1 -1
- package/dist/helpers/readOutputFiles.cjs.map +1 -1
- package/dist/helpers/readOutputFiles.d.ts +0 -9
- package/dist/helpers/readOutputFiles.js +1 -1
- package/dist/helpers/readOutputFiles.js.map +1 -1
- package/dist/helpers/runCommandInTemporaryPackageManagerProject.cjs +1 -1
- package/dist/helpers/runCommandInTemporaryPackageManagerProject.cjs.map +1 -1
- package/dist/helpers/runCommandInTemporaryPackageManagerProject.d.ts +0 -3
- package/dist/helpers/runCommandInTemporaryPackageManagerProject.js +1 -1
- package/dist/helpers/runCommandInTemporaryPackageManagerProject.js.map +1 -1
- package/dist/helpers/spawnSyncWithTimeout.cjs +1 -1
- package/dist/helpers/spawnSyncWithTimeout.cjs.map +1 -1
- package/dist/helpers/spawnSyncWithTimeout.d.ts +0 -7
- package/dist/helpers/spawnSyncWithTimeout.js +1 -1
- package/dist/helpers/spawnSyncWithTimeout.js.map +1 -1
- package/dist/helpers/startHttpServer.cjs +1 -1
- package/dist/helpers/startHttpServer.cjs.map +1 -1
- package/dist/helpers/startHttpServer.js +1 -1
- package/dist/helpers/startHttpServer.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +0 -2
- 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 +0 -16
- package/dist/presets/command.js +1 -1
- package/dist/presets/command.js.map +1 -1
- package/dist/presets/evaluation.cjs +4 -4
- package/dist/presets/evaluation.cjs.map +1 -1
- package/dist/presets/evaluation.js +4 -4
- package/dist/presets/evaluation.js.map +1 -1
- package/dist/presets/guiCommand.cjs +3 -3
- package/dist/presets/guiCommand.cjs.map +1 -1
- package/dist/presets/guiCommand.d.ts +0 -24
- package/dist/presets/guiCommand.js +3 -3
- package/dist/presets/guiCommand.js.map +1 -1
- package/dist/presets/stdio.cjs +1 -1
- package/dist/presets/stdio.cjs.map +1 -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.cjs.map +1 -1
- package/dist/types/language.js +2 -2
- package/dist/types/language.js.map +1 -1
- package/package.json +2 -2
- package/dist/helpers/runCustomRunner.cjs +0 -2
- package/dist/helpers/runCustomRunner.cjs.map +0 -1
- package/dist/helpers/runCustomRunner.d.ts +0 -8
- package/dist/helpers/runCustomRunner.js +0 -2
- package/dist/helpers/runCustomRunner.js.map +0 -1
- package/dist/helpers/safeFs.cjs +0 -2
- package/dist/helpers/safeFs.cjs.map +0 -1
- package/dist/helpers/safeFs.d.ts +0 -29
- package/dist/helpers/safeFs.js +0 -2
- package/dist/helpers/safeFs.js.map +0 -1
- package/dist/helpers/sandboxUser.cjs +0 -3
- package/dist/helpers/sandboxUser.cjs.map +0 -1
- package/dist/helpers/sandboxUser.d.ts +0 -146
- package/dist/helpers/sandboxUser.js +0 -3
- package/dist/helpers/sandboxUser.js.map +0 -1
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Name of the environment variable through which a judge server tells problem-utils to run
|
|
3
|
-
* untrusted submitted programs as the given unprivileged OS user via sudo.
|
|
4
|
-
*
|
|
5
|
-
* The judge server sets this variable only when it runs the judge harness (e.g. `judge.ts`) as its
|
|
6
|
-
* own trusted user, so that problem files (test cases and the harness itself) stay unreadable to
|
|
7
|
-
* submissions while the submissions themselves run under the sandbox user. When the variable is
|
|
8
|
-
* absent or empty (local development, course authoring, or an all-sandbox judge run), commands run
|
|
9
|
-
* as the current user like before.
|
|
10
|
-
*
|
|
11
|
-
* Contract for the delegating judge server (delegation is Linux-only — sudo user separation and
|
|
12
|
-
* `/home/<user>` homes are provisioned in the judge Docker image; never set this on macOS):
|
|
13
|
-
* - The harness process environment is forwarded to sandboxed submissions (sudo runs with
|
|
14
|
-
* `--preserve-env`), so it must not contain secrets beyond what submissions may see. The one
|
|
15
|
-
* exception is the set glibc strips when executing the setuid `sudo` (secure-execution mode:
|
|
16
|
-
* `LD_*`, `TMPDIR`, `LOCPATH`, `NLSPATH`, `TZDIR`, … — see ld.so(8)); no sudoers setting can
|
|
17
|
-
* recover those, and only `LD_LIBRARY_PATH` is restored, by the wrapper below.
|
|
18
|
-
* - sudoers must let the harness user run arbitrary commands as the sandbox user without a
|
|
19
|
-
* password, pass the environment through, and keep sudo off a pseudo-terminal (a pty would merge
|
|
20
|
-
* stderr into stdout and CRLF-mangle output when the harness happens to run from a terminal):
|
|
21
|
-
* e.g. `Defaults:<harness> !env_reset, !env_delete, !env_check, !secure_path, !use_pty` plus
|
|
22
|
-
* `<harness> ALL=(<sandbox>) NOPASSWD:SETENV: ALL`.
|
|
23
|
-
* - Variables meant for the submitted program rather than for the harness must be passed under
|
|
24
|
-
* {@link SANDBOX_ENV_PREFIX}; problem-utils strips that prefix when it builds a submission's
|
|
25
|
-
* environment.
|
|
26
|
-
* - The sandbox user's home directory must exist at `/home/<sandbox user>` and be writable. It is
|
|
27
|
-
* shared across sequential requests, so the server is responsible for resetting whatever
|
|
28
|
-
* cross-request persistence there matters to it.
|
|
29
|
-
*/
|
|
30
|
-
export declare const SANDBOX_USER_ENV_NAME = "EXERCODE_SANDBOX_USER";
|
|
31
|
-
/**
|
|
32
|
-
* Environment for helper processes the *trusted* harness user runs (`ps`, `xwininfo`, `Xvfb`, …).
|
|
33
|
-
* The judge server overlays caller-supplied variables onto the harness environment, and a sandboxed
|
|
34
|
-
* submission can create executables in its persistent home, so resolving these helpers through the
|
|
35
|
-
* inherited `PATH` would hand the submission code execution as the harness user. Node resolves the
|
|
36
|
-
* command through the child environment's `PATH`, so a fixed `PATH` here pins them to system paths.
|
|
37
|
-
*/
|
|
38
|
-
export declare function getTrustedHelperEnv(extraEnv?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
39
|
-
export declare const sandboxUserName: string | undefined;
|
|
40
|
-
/**
|
|
41
|
-
* The deadline supervisor. Absolute under delegation: the wrapper's `exec "$0"` resolves it through
|
|
42
|
-
* the submission's own `PATH` (which a judge server may set via {@link SANDBOX_ENV_PREFIX}), so a
|
|
43
|
-
* bare name would let a submission replace the very process that enforces its time limit. The bare
|
|
44
|
-
* name is kept without delegation, where `timeout` may live elsewhere (Homebrew coreutils).
|
|
45
|
-
*/
|
|
46
|
-
export declare const TIMEOUT_COMMAND: string;
|
|
47
|
-
/**
|
|
48
|
-
* Wrap a command so it runs as the sandbox user. `umask 0` makes every file the sandboxed process
|
|
49
|
-
* creates world-writable, so the harness user can clean it up without privileges. The wrapper also
|
|
50
|
-
* restores `LD_LIBRARY_PATH`, which ld.so strips across the setuid `sudo` exec. Put `timeout`
|
|
51
|
-
* inside the wrapped command: the harness user cannot signal the root-owned `sudo` process, so an
|
|
52
|
-
* outer timer alone could not stop a runaway submission.
|
|
53
|
-
*/
|
|
54
|
-
export declare function wrapCommandWithSandboxUser(command: readonly [string, ...string[]]): [string, ...string[]];
|
|
55
|
-
/**
|
|
56
|
-
* Whether the command was already wrapped by {@link wrapCommandWithSandboxUser}. The presets hand
|
|
57
|
-
* custom runners a wrapped command, and a runner may forward it to another helper that wraps too;
|
|
58
|
-
* a nested wrapper's inner `sudo` would run AS the sandbox user, which sudoers does not authorize,
|
|
59
|
-
* so every test case of such a problem would fail only under delegation.
|
|
60
|
-
*
|
|
61
|
-
* Matches the wrapper prefix exactly rather than looking for `sudo` anywhere: a submission-derived
|
|
62
|
-
* command could otherwise carry a literal `/usr/bin/sudo` argument and skip wrapping entirely,
|
|
63
|
-
* which would run the submission as the trusted harness user.
|
|
64
|
-
*/
|
|
65
|
-
export declare function isSandboxWrappedCommand(command: readonly string[]): boolean;
|
|
66
|
-
/**
|
|
67
|
-
* Insert `innerPrefix` (e.g. a `time` measurement prefix) so that it runs INSIDE the sandbox
|
|
68
|
-
* wrapper when `command` is wrapped, and simply in front otherwise. Prefixing a wrapped command
|
|
69
|
-
* from the outside would run `innerPrefix` as the trusted harness user while its arguments (such
|
|
70
|
-
* as an output path) point into a sandbox-writable directory, where a submission can plant a
|
|
71
|
-
* symlink and have the harness truncate an arbitrary file it owns.
|
|
72
|
-
*/
|
|
73
|
-
export declare function prependInsideSandboxWrapper(command: readonly [string, ...string[]], innerPrefix: readonly string[]): [string, ...string[]];
|
|
74
|
-
/**
|
|
75
|
-
* Environment overrides for sandboxed processes: a writable home and the `LD_LIBRARY_PATH`
|
|
76
|
-
* smuggled past the setuid `sudo` exec (see {@link wrapCommandWithSandboxUser}). Pass the
|
|
77
|
-
* environment the command will actually run with so a caller-supplied `LD_LIBRARY_PATH` survives
|
|
78
|
-
* the exec; the harness's own value is only the fallback.
|
|
79
|
-
*/
|
|
80
|
-
export declare function getSandboxUserEnvOverrides(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
81
|
-
/**
|
|
82
|
-
* Prefix under which a delegating judge server passes variables that belong to the submitted
|
|
83
|
-
* program alone. Applying a request's variables to the harness itself would let a submission point
|
|
84
|
-
* e.g. `PATH` or `NODE_OPTIONS` at its own files and get code executed as the trusted harness user,
|
|
85
|
-
* so the server prefixes them and problem-utils strips the prefix back off here, where the
|
|
86
|
-
* environment of a sandboxed submission is built.
|
|
87
|
-
*/
|
|
88
|
-
export declare const SANDBOX_ENV_PREFIX = "SANDBOX_ENV_";
|
|
89
|
-
/**
|
|
90
|
-
* Make harness-user-created files under the given path readable, and directories writable, for the
|
|
91
|
-
* sandbox user, so sandboxed programs can read their sources and create outputs next to them.
|
|
92
|
-
*/
|
|
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;
|
|
99
|
-
/**
|
|
100
|
-
* Kill the sandbox user's processes with the given signals. The harness user cannot signal another
|
|
101
|
-
* user's processes (nor the root-owned `sudo` wrapper), so this goes through sudo. Killing every
|
|
102
|
-
* sandbox process is safe because the judge server handles one request at a time. The default
|
|
103
|
-
* sends SIGTERM immediately followed by SIGKILL (final cleanup); callers that want a grace period
|
|
104
|
-
* send `['TERM']`, wait, and then send `['KILL']`.
|
|
105
|
-
*
|
|
106
|
-
* Fails closed: a `['TERM']`-only sweep throws when its `sudo` could not be spawned (e.g. a
|
|
107
|
-
* submission exhausted the PID cgroup); a sweep including SIGKILL throws when a sandbox process is
|
|
108
|
-
* still alive, or cannot be listed, afterwards. Reporting success would leave that process running
|
|
109
|
-
* next to the next request on this instance.
|
|
110
|
-
*/
|
|
111
|
-
export declare function killSandboxUserProcesses(signals?: readonly ('TERM' | 'KILL')[]): void;
|
|
112
|
-
/**
|
|
113
|
-
* Let the sandbox user reopen the permissions of its own files under the given path, for
|
|
114
|
-
* harness-side traversal/cleanup of trees where a sandboxed process restricted permissions
|
|
115
|
-
* (some tools chmod their outputs regardless of umask).
|
|
116
|
-
*/
|
|
117
|
-
export declare function relaxPermissionsAsSandboxUser(targetPath: string): void;
|
|
118
|
-
/**
|
|
119
|
-
* How long after a command's own deadline {@link startSandboxTimeoutWatchdog} force-kills the
|
|
120
|
-
* sandbox user. Callers waiting for the watchdog to end a submission must wait at least this long.
|
|
121
|
-
*/
|
|
122
|
-
export declare const SANDBOX_WATCHDOG_GRACE_SECONDS = 5;
|
|
123
|
-
/** Cancels a {@link startSandboxTimeoutWatchdog}; `fired` reports whether its deadline elapsed. */
|
|
124
|
-
export interface SandboxTimeoutWatchdog {
|
|
125
|
-
cancel(): void;
|
|
126
|
-
fired(): boolean;
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Start a harness-owned watchdog that force-kills every sandbox process after the given deadline.
|
|
130
|
-
* A sandboxed submission can signal its own `timeout` supervisor (same UID), and a synchronous
|
|
131
|
-
* spawn blocks the harness's event loop, so without this external deadline a submission could run
|
|
132
|
-
* until the outer judge-server limit. The watchdog's `sh`/`sleep` run as the harness user, out of
|
|
133
|
-
* the submission's reach; killing the watchdog's process group cancels it before it spawns sudo.
|
|
134
|
-
*
|
|
135
|
-
* ALWAYS cancel in a `finally`: the watchdog is detached and `unref`'d, so a leaked one keeps
|
|
136
|
-
* running after the harness exits and would SIGKILL a later request's submission.
|
|
137
|
-
*/
|
|
138
|
-
export declare function startSandboxTimeoutWatchdog(timeoutSeconds: number): SandboxTimeoutWatchdog;
|
|
139
|
-
/**
|
|
140
|
-
* `fs.rm`-like removal with a fallback for trees holding sandbox-user-owned entries whose
|
|
141
|
-
* permissions block deletion: let the sandbox user reopen its own files first, then retry. The
|
|
142
|
-
* containing directory is relaxed as well — unlinking an entry needs write permission on its
|
|
143
|
-
* parent, so a submission that chmods a directory it owns would otherwise make everything inside it
|
|
144
|
-
* undeletable.
|
|
145
|
-
*/
|
|
146
|
-
export declare function forceRemove(targetPath: string): Promise<void>;
|
|
@@ -1,3 +0,0 @@
|
|
|
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
|
-
//# sourceMappingURL=sandboxUser.js.map
|
|
@@ -1 +0,0 @@
|
|
|
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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"}
|