UNPKG

@judgeq/simple-sandbox

Version:

A simple sandbox for Node.js using Linux namespaces and cgroup.

186 lines (174 loc) 6.5 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); const bindings = require('bindings'); const fs = require('fs'); const randomString = require('randomstring'); const path = require('path'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e["default"] : e; } function _interopNamespace(e) { if (e && e.__esModule) return e; const n = Object.create(null); if (e) { for (const k in e) { n[k] = e[k]; } } n["default"] = e; return n; } const bindings__default = /*#__PURE__*/_interopDefaultLegacy(bindings); const randomString__namespace = /*#__PURE__*/_interopNamespace(randomString); const path__namespace = /*#__PURE__*/_interopNamespace(path); const sandboxAddon = bindings__default("simple-sandbox"); const nativeAddon = sandboxAddon; var SandboxStatus = /* @__PURE__ */ ((SandboxStatus2) => { SandboxStatus2[SandboxStatus2["Unknown"] = 0] = "Unknown"; SandboxStatus2[SandboxStatus2["OK"] = 1] = "OK"; SandboxStatus2[SandboxStatus2["TimeLimitExceeded"] = 2] = "TimeLimitExceeded"; SandboxStatus2[SandboxStatus2["MemoryLimitExceeded"] = 3] = "MemoryLimitExceeded"; SandboxStatus2[SandboxStatus2["RuntimeError"] = 4] = "RuntimeError"; SandboxStatus2[SandboxStatus2["Cancelled"] = 5] = "Cancelled"; SandboxStatus2[SandboxStatus2["OutputLimitExceeded"] = 6] = "OutputLimitExceeded"; return SandboxStatus2; })(SandboxStatus || {}); function milliToNano(milliseconds) { return milliseconds * 1e3 * 1e3; } class SandboxProcess { constructor(parameter, pid, execParam) { this.parameter = parameter; this.pid = pid; this.cancellationToken = null; this.countedCpuTime = 0; this.actualCpuTime = 0; this.timeout = false; this.cancelled = false; this.waitPromise = null; this.running = true; const myFather = this; this.stopCallback = () => { myFather.stop(); }; let checkIfTimedOut = () => { }; if (this.parameter.time !== -1) { const checkInterval = Math.min(this.parameter.time / 10, 50); let lastCheck = new Date().getTime(); checkIfTimedOut = () => { const current = new Date().getTime(); const spent = current - lastCheck; lastCheck = current; const val = Number(nativeAddon.getCgroupProperty("cpuacct", myFather.parameter.cgroup, "cpuacct.usage")); myFather.countedCpuTime += Math.max(val - myFather.actualCpuTime, milliToNano(spent) * 0.4); myFather.actualCpuTime = val; if (myFather.countedCpuTime > milliToNano(parameter.time)) { myFather.timeout = true; myFather.stop(); } }; this.cancellationToken = setInterval(checkIfTimedOut, checkInterval); } this.waitPromise = new Promise((res, rej) => { nativeAddon.waitForProcess(pid, execParam, (err, runResult) => { if (err) { try { myFather.stop(); myFather.cleanup(); } catch (e) { console.log("Error cleaning up error sandbox:", e); } rej(err); } else { try { const memUsageWithCache = Number(nativeAddon.getCgroupProperty("memory", myFather.parameter.cgroup, "memory.memsw.max_usage_in_bytes")); const cache = Number(nativeAddon.getCgroupProperty2("memory", myFather.parameter.cgroup, "memory.stat", "cache")); const memUsage = memUsageWithCache - cache; myFather.actualCpuTime = Number(nativeAddon.getCgroupProperty("cpuacct", myFather.parameter.cgroup, "cpuacct.usage")); myFather.cleanup(); const result = { status: SandboxStatus.Unknown, time: myFather.actualCpuTime, memory: memUsage, code: runResult.code }; if (myFather.timeout || myFather.actualCpuTime > milliToNano(myFather.parameter.time)) { result.status = SandboxStatus.TimeLimitExceeded; } else if (myFather.cancelled) { result.status = SandboxStatus.Cancelled; } else if (myFather.parameter.memory != -1 && memUsage > myFather.parameter.memory) { result.status = SandboxStatus.MemoryLimitExceeded; } else if (runResult.status === "signaled") { result.status = SandboxStatus.RuntimeError; } else if (runResult.status === "exited") { result.status = SandboxStatus.OK; } res(result); } catch (e) { rej(e); } } }); }); } removeCgroup() { nativeAddon.removeCgroup("memory", this.parameter.cgroup); nativeAddon.removeCgroup("cpuacct", this.parameter.cgroup); nativeAddon.removeCgroup("pids", this.parameter.cgroup); } cleanup() { if (this.running) { if (this.cancellationToken) { clearInterval(this.cancellationToken); } process.removeListener("exit", this.stopCallback); this.removeCgroup(); this.running = false; } } stop() { this.cancelled = true; try { process.kill(this.pid, "SIGKILL"); } catch (err) { } } async waitForStop() { return await this.waitPromise; } } if (!fs.existsSync("/sys/fs/cgroup/memory/memory.memsw.usage_in_bytes")) { throw new Error("Your linux kernel doesn't support memory-swap account. Please turn it on following the readme."); } const MAX_RETRY_TIMES = 20; function startSandbox(parameter) { const doStart = () => { const actualParameter = Object.assign({}, parameter); actualParameter.cgroup = path__namespace.join(actualParameter.cgroup, randomString__namespace.generate(9)); const startResult = nativeAddon.startSandbox(actualParameter); return new SandboxProcess(actualParameter, startResult.pid, startResult.execParam); }; let retryTimes = MAX_RETRY_TIMES; while (1) { try { return doStart(); } catch (e) { if ("message" in e && typeof e.message === "string" && e.message.startsWith("The child process ")) { if (retryTimes-- > 0) { continue; } } throw e; } } return doStart(); } function getUidAndGidInSandbox(rootfs, username) { try { return nativeAddon.getUidAndGidInSandbox(rootfs, username); } catch (e) { throw e; } } exports.SandboxStatus = SandboxStatus; exports.getUidAndGidInSandbox = getUidAndGidInSandbox; exports.startSandbox = startSandbox;