@judgeq/simple-sandbox
Version:
A simple sandbox for Node.js using Linux namespaces and cgroup.
186 lines (174 loc) • 6.5 kB
JavaScript
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;
;