kern-sandbox 0.1.19 → 0.1.20
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 +10 -8
- package/index.d.ts +1 -1
- package/index.js +24 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**[kern](https://github.com/getkern/kern)** is a fast, rootless sandbox and virtual resource
|
|
4
4
|
runtime for any workload, including untrusted and AI-generated code: a real, kernel-enforced box
|
|
5
|
-
that starts in **3.4 ms** from an OCI image, out of one
|
|
5
|
+
that starts in **3.4 ms** from an OCI image, out of one **1.58 MB** binary, with no daemon.
|
|
6
6
|
**kern-sandbox**
|
|
7
7
|
is its Node / TypeScript binding: run untrusted or agent-generated code in a fresh, isolated box, from Node.
|
|
8
8
|
|
|
@@ -11,7 +11,7 @@ package is on PyPI: [`kern-sandbox`](https://pypi.org/project/kern-sandbox/).
|
|
|
11
11
|
|
|
12
12
|
It is a thin, dependency-free wrapper around the [`kern`](https://github.com/getkern/kern) binary:
|
|
13
13
|
a fresh, isolated box per call, network off by default, hard resource caps, and a timeout the binding
|
|
14
|
-
itself enforces. Kernel-enforced isolation (namespaces, cgroups v2, seccomp), local, about 1.
|
|
14
|
+
itself enforces. Kernel-enforced isolation (namespaces, cgroups v2, seccomp), local, about 1.58 MB, with no cloud, no account, no VM.
|
|
15
15
|
|
|
16
16
|
```js
|
|
17
17
|
const kern = require("kern-sandbox");
|
|
@@ -98,7 +98,8 @@ A non-zero exit from *your code* is **not** a fault (`fault` stays `null`): it i
|
|
|
98
98
|
|---|---|
|
|
99
99
|
| `timeout` | the call exceeded `timeoutS`; the binding killed the box |
|
|
100
100
|
| `escape_blocked` | a syscall was blocked by the seccomp filter (SIGSYS) |
|
|
101
|
-
| `
|
|
101
|
+
| `oom` | the box was SIGKILLed with a `memoryMb` cap in effect: a breached `memory.max` is the cgroup OOM-killer (`memory.oom.group=1` kills the whole box) |
|
|
102
|
+
| `killed` | the box was SIGKILLed with **no** memory cap set, so the cause is ambiguous (host pressure, an external kill) and is not attributed to OOM |
|
|
102
103
|
|
|
103
104
|
A box that fails to **start** (kern exits 125: a mount refused at runtime, an unmappable `--user`, a
|
|
104
105
|
seccomp/AppArmor/cgroup setup error, or a pull/image error) is **thrown** as a `SandboxError`, not
|
|
@@ -225,11 +226,12 @@ clear error otherwise). The Python binding uses the stdlib `tarfile` and has no
|
|
|
225
226
|
## Honest threat model
|
|
226
227
|
|
|
227
228
|
kern is a **kernel-boundary** sandbox for **your own or semi-trusted** code (CI, dev, edge, your
|
|
228
|
-
agents' code). Its seccomp filter is a **
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
229
|
+
agents' code). Its default seccomp filter is a **deny-by-default allowlist** (moby's own default
|
|
230
|
+
filter minus kern's 35 escape syscalls): right for semi-trusted agent code, **not** a hard boundary
|
|
231
|
+
against deliberately hostile multi-tenant code. For that, reach for a microVM (Firecracker / Kata) or
|
|
232
|
+
gVisor. The wider denylist is the opt-out (`KERN_SECCOMP=denylist`), and `securityProfile: "untrusted"`
|
|
233
|
+
bundles the allowlist with `--cap-drop ALL` + `--read-only`. See the project's
|
|
234
|
+
[SECURITY.md](https://github.com/getkern/kern/blob/main/SECURITY.md).
|
|
233
235
|
|
|
234
236
|
## License
|
|
235
237
|
|
package/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Run LLM/agent-generated code in a fast, local, daemonless kernel sandbox.
|
|
3
3
|
|
|
4
4
|
/** What stopped the code at the SANDBOX level. Reported as data on a result, never thrown. */
|
|
5
|
-
export type SandboxFaultType = "timeout" | "escape_blocked" | "killed" | "startup_failed";
|
|
5
|
+
export type SandboxFaultType = "timeout" | "oom" | "escape_blocked" | "killed" | "startup_failed";
|
|
6
6
|
|
|
7
7
|
export interface SandboxFault {
|
|
8
8
|
type: SandboxFaultType;
|
package/index.js
CHANGED
|
@@ -1171,8 +1171,15 @@ class Sandbox {
|
|
|
1171
1171
|
);
|
|
1172
1172
|
if (rc === EXIT_SIGSYS || signal === "SIGSYS")
|
|
1173
1173
|
return sandboxFault("escape_blocked", "a syscall was blocked by the seccomp filter (SIGSYS)");
|
|
1174
|
-
if (rc === EXIT_SIGKILL || signal === "SIGKILL")
|
|
1175
|
-
|
|
1174
|
+
if (rc === EXIT_SIGKILL || signal === "SIGKILL") {
|
|
1175
|
+
// A memory-capped box SIGKILLed is the cgroup OOM-killer - what a breached memory.max does (kern
|
|
1176
|
+
// sets memory.oom.group=1, so the whole box dies at once). Claim `oom` when a --memory cap was in
|
|
1177
|
+
// effect: that is a fact WE set on the argv, not the workload's stderr, so it costs no security
|
|
1178
|
+
// discipline. Uncapped, the cause is ambiguous (host pressure, an external kill) - keep `killed`.
|
|
1179
|
+
if (this.memoryMb !== null)
|
|
1180
|
+
return sandboxFault("oom", "the box exceeded its memory cap and was OOM-killed (SIGKILL, exit 137)");
|
|
1181
|
+
return sandboxFault("killed", "the box was killed (SIGKILL); no memory cap was set to attribute it to OOM");
|
|
1182
|
+
}
|
|
1176
1183
|
if (rc === EXIT_SIGTERM || signal === "SIGTERM")
|
|
1177
1184
|
return sandboxFault("timeout", "the box exceeded its time limit (reaped by kern's timeout backstop)");
|
|
1178
1185
|
// Box-not-started: a non-zero exit whose stderr carries kern's OWN setup markers (printed by the
|
|
@@ -1754,8 +1761,8 @@ class Kernel {
|
|
|
1754
1761
|
return this._teardownResult("killed", `the kernel reply exceeded the ${this._cap}-byte cap`, started);
|
|
1755
1762
|
if (reply === null) {
|
|
1756
1763
|
const err = this._stderr.toString("utf8");
|
|
1757
|
-
const kind =
|
|
1758
|
-
return this._teardownResult(kind, err.trim() ||
|
|
1764
|
+
const [kind, dflt] = this._kernelDeathFault(err);
|
|
1765
|
+
return this._teardownResult(kind, err.trim() || dflt, started);
|
|
1759
1766
|
}
|
|
1760
1767
|
return this._resultFromReply(reply, started);
|
|
1761
1768
|
}
|
|
@@ -1801,6 +1808,19 @@ class Kernel {
|
|
|
1801
1808
|
});
|
|
1802
1809
|
}
|
|
1803
1810
|
|
|
1811
|
+
/** Why the resident kernel box died mid-cell, as `[type, defaultMessage]`. A kern setup marker on
|
|
1812
|
+
* stderr means it never came up (startup_failed). Otherwise, with a memoryMb cap in force, the cgroup
|
|
1813
|
+
* OOM-killer is the dominant cause of a kernel that disappears while running a cell - the SAME
|
|
1814
|
+
* attribution (from the same non-workload signal, the --memory flag WE set) as the one-shot _classify
|
|
1815
|
+
* SIGKILL path. A kernel death has no per-cell exit code to route through _classify, so the OOM
|
|
1816
|
+
* attribution lives here too. Uncapped, the cause is ambiguous -> `killed`. */
|
|
1817
|
+
_kernelDeathFault(err) {
|
|
1818
|
+
if (looksLikeStartupFailure(err)) return ["startup_failed", "the kernel box failed to start"];
|
|
1819
|
+
if (this._sbx.memoryMb !== null)
|
|
1820
|
+
return ["oom", "the kernel box was OOM-killed (it exceeded its memory cap)"];
|
|
1821
|
+
return ["killed", "the kernel box exited"];
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1804
1824
|
_teardownResult(type, message, started) {
|
|
1805
1825
|
this._kill();
|
|
1806
1826
|
// Same rule as the one-shot path: a box that never STARTED (the kernel failed to boot) throws, it
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kern-sandbox",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
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",
|