kern-sandbox 0.1.16 → 0.1.19
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/index.js +49 -13
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -36,7 +36,7 @@ const crypto = require("crypto");
|
|
|
36
36
|
const zlib = require("zlib");
|
|
37
37
|
const { spawn, spawnSync } = require("child_process");
|
|
38
38
|
|
|
39
|
-
const VERSION = "0.1.
|
|
39
|
+
const VERSION = "0.1.19";
|
|
40
40
|
|
|
41
41
|
const DEFAULT_IMAGE = "python:3.12-slim";
|
|
42
42
|
const WORKSPACE = "/workspace"; // where the persistent workspace is mounted inside every box
|
|
@@ -465,6 +465,11 @@ class ExecutionResult {
|
|
|
465
465
|
}
|
|
466
466
|
}
|
|
467
467
|
|
|
468
|
+
/** A sandbox event `{type, message}` for `result.fault`. NB: `startup_failed` is decided from an
|
|
469
|
+
* UNFORGEABLE kern signal (a byte on fd 3 / `KERN_STARTED_FD` a workload can neither write nor
|
|
470
|
+
* suppress). Against a kern too old to send it, the binding falls back to a stderr heuristic that can
|
|
471
|
+
* only OVER-report - a workload can make its own exit look like a start failure - never MISS a real
|
|
472
|
+
* one, so it fails in the safe direction. Pair this binding with the matching (or newer) kern release. */
|
|
468
473
|
function sandboxFault(type, message) {
|
|
469
474
|
return { type, message };
|
|
470
475
|
}
|
|
@@ -621,8 +626,14 @@ function looksLikeStartupFailure(stderr) {
|
|
|
621
626
|
"error: oci:",
|
|
622
627
|
"error: image:",
|
|
623
628
|
];
|
|
629
|
+
// kern also writes BENIGN `kern:` diagnostics that are NOT a box-start failure: the
|
|
630
|
+
// `--security-profile` posture banner, and `warning:`/`note:` lines. They start with `kern:` too, so
|
|
631
|
+
// without this skip a workload that merely exits non-zero WHILE one is on stderr (e.g. code run under
|
|
632
|
+
// securityProfile: "untrusted" that hits a network error) would be mislabeled `startup_failed`.
|
|
633
|
+
const benign = ["kern: security-profile=", "kern: warning:", "kern: note:"];
|
|
624
634
|
for (const line of stderr.split("\n")) {
|
|
625
635
|
const s = line.replace(/^\s+/, "");
|
|
636
|
+
if (benign.some((b) => s.startsWith(b))) continue;
|
|
626
637
|
if (s.includes("sandbox setup failed") || markers.some((m) => s.startsWith(m))) return true;
|
|
627
638
|
}
|
|
628
639
|
return false;
|
|
@@ -1021,22 +1032,39 @@ class Sandbox {
|
|
|
1021
1032
|
const argv = [...this._baseArgv(name, { network, timeoutS, isSetup }), "--", ...command];
|
|
1022
1033
|
const childEnv = { ...process.env };
|
|
1023
1034
|
if (!this.enforceLimits) childEnv.KERN_NO_SCOPE = "1";
|
|
1035
|
+
// Unforgeable "box started" channel: kern writes one byte to fd 3 iff its sandbox setup SUCCEEDED
|
|
1036
|
+
// and the command ran. The workload never holds fd 3, so it can neither forge nor suppress it -
|
|
1037
|
+
// unlike kern's stderr, which it can. A new kern makes this the authority for `startup_failed`; an
|
|
1038
|
+
// OLD kern never writes it, `boxStarted` stays false, and the stderr heuristic stands (backward
|
|
1039
|
+
// compatible).
|
|
1040
|
+
childEnv.KERN_STARTED_FD = "3";
|
|
1024
1041
|
|
|
1025
1042
|
const started = process.hrtime.bigint();
|
|
1026
1043
|
return new Promise((resolve, reject) => {
|
|
1027
1044
|
let child;
|
|
1045
|
+
let boxStarted = false;
|
|
1028
1046
|
try {
|
|
1029
1047
|
// detached: own process group, so we can signal the box + kern as a unit (killpg).
|
|
1048
|
+
// The 4th stdio slot is fd 3: the child (kern) writes the started byte, the parent reads it.
|
|
1030
1049
|
child = spawn(argv[0], argv.slice(1), {
|
|
1031
1050
|
env: childEnv,
|
|
1032
1051
|
detached: true,
|
|
1033
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1052
|
+
stdio: ["ignore", "pipe", "pipe", "pipe"],
|
|
1034
1053
|
});
|
|
1035
1054
|
} catch (e) {
|
|
1036
1055
|
this._removeEnvFile(name);
|
|
1037
1056
|
return reject(new SandboxError(`could not spawn the box: ${e.message}`));
|
|
1038
1057
|
}
|
|
1039
1058
|
|
|
1059
|
+
const startedCh = child.stdio[3];
|
|
1060
|
+
if (startedCh) {
|
|
1061
|
+
// One byte (0x01) = the box started; stream end with no byte = never started / old kern.
|
|
1062
|
+
startedCh.on("data", (b) => {
|
|
1063
|
+
if (b.length && b[0] === 1) boxStarted = true;
|
|
1064
|
+
});
|
|
1065
|
+
startedCh.on("error", () => {});
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1040
1068
|
const out = cappedCollector(child.stdout, this.maxOutputBytes, cbOut);
|
|
1041
1069
|
const err = cappedCollector(child.stderr, this.maxOutputBytes, cbErr);
|
|
1042
1070
|
let timedOut = false;
|
|
@@ -1059,12 +1087,20 @@ class Sandbox {
|
|
|
1059
1087
|
const stdout = out.buffer().toString("utf8");
|
|
1060
1088
|
const stderr = err.buffer().toString("utf8");
|
|
1061
1089
|
const rc = toRc(code, signal);
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1090
|
+
let fault = this._classify(rc, signal, stderr, timedOut, timeoutS);
|
|
1091
|
+
if (boxStarted && fault && fault.type === "startup_failed") {
|
|
1092
|
+
// kern signalled the box STARTED, so a `startup_failed` here is only the stderr heuristic
|
|
1093
|
+
// matching a marker the WORKLOAD wrote (code-based faults are decided first). The box
|
|
1094
|
+
// demonstrably ran: this is the workload's own non-zero exit - reclassify to a normal result.
|
|
1095
|
+
fault = null;
|
|
1096
|
+
}
|
|
1097
|
+
// A box that FAILED TO START ran no user code, so REJECT rather than resolve a hollow
|
|
1098
|
+
// ExecutionResult (empty stdout). Gated on `rc === 125` (kern's box-not-started code) AND the
|
|
1099
|
+
// startup_failed classification (which requires kern's own stderr marker): the confident pair
|
|
1100
|
+
// that tells a genuine box-not-started apart from a workload that itself exited 125 (no marker ->
|
|
1101
|
+
// fault null -> a normal result). An older kern (127) is returned as a data fault, not thrown.
|
|
1102
|
+
// Runtime events where the code DID run (timeout, OOM, escape) stay as data on `.fault`.
|
|
1103
|
+
if (rc === 125 && fault && fault.type === "startup_failed") {
|
|
1068
1104
|
return reject(new SandboxError(fault.message || "the box failed to start"));
|
|
1069
1105
|
}
|
|
1070
1106
|
const files = before ? this._diff(before) : [];
|
|
@@ -1139,11 +1175,11 @@ class Sandbox {
|
|
|
1139
1175
|
return sandboxFault("killed", "the box was killed (SIGKILL) - likely out of memory (exit 137)");
|
|
1140
1176
|
if (rc === EXIT_SIGTERM || signal === "SIGTERM")
|
|
1141
1177
|
return sandboxFault("timeout", "the box exceeded its time limit (reaped by kern's timeout backstop)");
|
|
1142
|
-
//
|
|
1143
|
-
// box
|
|
1144
|
-
//
|
|
1145
|
-
|
|
1146
|
-
//
|
|
1178
|
+
// Box-not-started: a non-zero exit whose stderr carries kern's OWN setup markers (printed by the
|
|
1179
|
+
// PARENT before the box runs). kern's box-not-started paths BOTH exit 125 AND print a `kern:` marker,
|
|
1180
|
+
// so `rc === 125 && marker` is the reliable signal - the marker is REQUIRED so a workload that merely
|
|
1181
|
+
// exits 125 ITSELF (the code ran and chose 125) is NOT mislabeled. `finish` REJECTS only on rc===125;
|
|
1182
|
+
// a non-125 startup_failed (an older kern's 127, or a forged marker) is returned as DATA, not thrown.
|
|
1147
1183
|
if (rc !== 0 && looksLikeStartupFailure(stderr))
|
|
1148
1184
|
return sandboxFault("startup_failed", stderr.trim().slice(0, 500));
|
|
1149
1185
|
// Any other non-zero exit (incl. 139 SIGSEGV) is the USER's code failing - a normal Result.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kern-sandbox",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"description": "kern is a fast, rootless sandbox and virtual resource runtime for any workload, including untrusted and AI-generated code; kern-sandbox is its Node/TypeScript binding. Run untrusted or agent-generated code (Python/JS/Bash) in a real, kernel-enforced box in single-digit milliseconds, with no cloud, no account and no VM.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sandbox",
|