kern-sandbox 0.2.8 → 0.2.10

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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/index.js +60 -1
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -129,7 +129,7 @@ A non-zero exit from *your code* is **not** a fault (`fault` stays `null`): it i
129
129
  | `oom` | the kernel's OOM killer took the box against its own memory cap. Read from a descriptor the code in the box cannot write, so it is an observation and not a guess from the exit code |
130
130
  | `killed` | SIGKILL with **no** OOM reported: an external kill (`kern stop`, a signal, the host out of memory), or a cap that did not bind here, which the message names |
131
131
  | `exec_failed` | the box started, the command did not exist inside it. `{language:"node"}` on an image with no `node` is the ordinary way there; the message names the binary AND the image |
132
- | `startup_failed` | your `timeoutS` fired while kern was still BUILDING the box, so the code never ran. A longer timeout does not help: a bind source on a dead NFS export does this |
132
+ | `startup_failed` | the box never ran, and kern said why in `stderr`. Two shapes: your `timeoutS` fired while kern was still BUILDING the box (run it again: a fast second call was a cold image read), or kern refused to build it at all (an image that cannot be pulled, a mount it will not make) |
133
133
 
134
134
  ```js
135
135
  const r = await kern.runCode("while True: pass", { timeoutS: 5 });
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.2.8";
39
+ const VERSION = "0.2.10";
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
@@ -446,6 +446,48 @@ const REFUSED_MOUNT_SOURCES = new Set([
446
446
  "/run/docker.sock",
447
447
  ]);
448
448
 
449
+ /** kern's OWN state on this host: [path, what it is]. Refused as a mount source, like the docker socket
450
+ * and for the same reason.
451
+ *
452
+ * FOUND BY A CHECKLIST ROW, measured on the Python binding first: mounting `$XDG_RUNTIME_DIR/kern` was
453
+ * ACCEPTED, which hands the code in the box kern's control plane (the registry, instance dirs, netns
454
+ * handles and exit files of every box this user runs). The image cache is the same class one step
455
+ * removed: a box that writes it poisons the rootfs a LATER box runs. Resolved per call, because these
456
+ * follow the environment and a service manager moves them. */
457
+ function kernStateDirs() {
458
+ const uid = process.getuid();
459
+ const runtime = process.env.XDG_RUNTIME_DIR || `/run/user/${uid}`;
460
+ const home = os.homedir();
461
+ const cache = process.env.XDG_CACHE_HOME || path.join(home, ".cache");
462
+ const config = process.env.XDG_CONFIG_HOME || path.join(home, ".config");
463
+ return [
464
+ [path.join(runtime, "kern"), "kern's runtime state (the registry, instance dirs, netns handles and exit files of every box you are running)"],
465
+ [`/run/user/${uid}/kern`, "kern's runtime state"],
466
+ [path.join(cache, "kern"), "kern's image cache (a box that writes it poisons the rootfs a later box runs)"],
467
+ [path.join(config, "kern"), "kern's configuration (the profiles a later box may be given)"],
468
+ ];
469
+ }
470
+
471
+ /** Credential directories, refused as a COMPONENT anywhere in the source. The set above is absolute
472
+ * paths, so it refused `$HOME` and accepted `$HOME/.ssh`: MEASURED, a box mounted with `~/.ssh` listed
473
+ * `id_ed25519` and `authorized_keys`. Refusing the parent and allowing its most sensitive child is the
474
+ * wrong way round, and it is the scenario a prompt-injected agent is steered into ("read ~/.aws"). These
475
+ * match by NAME because they live under a per-user home. No escape hatch, same as `/etc`: a job that
476
+ * needs one credential should be given that one file in the workspace. */
477
+ const REFUSED_MOUNT_COMPONENTS = new Set([
478
+ ".ssh",
479
+ ".aws",
480
+ ".gnupg",
481
+ ".kube",
482
+ ".docker",
483
+ ".azure",
484
+ ".password-store",
485
+ ".netrc",
486
+ ".git-credentials",
487
+ ".pypirc",
488
+ ".npmrc",
489
+ ]);
490
+
449
491
  /** A PROGRAMMER/config error, THROWN: bad argument, illegal mount, `kern` not installed, or the box
450
492
  * FAILED TO START (kern exits 125 - a mount refused at runtime, an unmappable `--user`, a seccomp or
451
493
  * AppArmor setup error). A box that never started ran no user code, so it rejects rather than resolve a
@@ -837,6 +879,23 @@ function validateMount(source, target) {
837
879
  `refusing to mount the sensitive host path ${JSON.stringify(real)} into a sandbox ` +
838
880
  "(this would defeat the isolation)",
839
881
  );
882
+ for (const [state, what] of kernStateDirs()) {
883
+ let sreal;
884
+ try { sreal = fs.realpathSync(state); } catch { sreal = state; }
885
+ if (real === sreal || real.startsWith(sreal + path.sep))
886
+ throw new MountRefused(
887
+ `refusing to mount ${JSON.stringify(real)}: it is ${what}. Mounting kern's own state into a box ` +
888
+ "it started gives the code inside the sandbox's control plane, which is the same reason the " +
889
+ "docker socket is refused",
890
+ );
891
+ }
892
+ for (const part of real.split(path.sep))
893
+ if (REFUSED_MOUNT_COMPONENTS.has(part))
894
+ throw new MountRefused(
895
+ `refusing to mount ${JSON.stringify(real)}: ${JSON.stringify(part)} holds credentials, and code ` +
896
+ "in the box would read them. If the job needs one secret, write THAT FILE into the workspace " +
897
+ "(sbx.writeFile) or mount a directory that holds only it",
898
+ );
840
899
  return [real, target];
841
900
  }
842
901
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kern-sandbox",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
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",