@zenera/sandbox 1.2.0 → 1.2.2

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/dist/bwrap.d.ts CHANGED
@@ -12,6 +12,24 @@ export declare const IMAGE_ENV_FILE = "/opt/zenera/etc/image-env.json";
12
12
  /** Set by `zen image build`: what the image was built from. */
13
13
  export declare const IMAGE_BUILD_ENV = "ZEN_IMAGE_BUILD";
14
14
  export declare const IMAGE_BASE_ENV = "ZEN_IMAGE_BASE";
15
+ /**
16
+ * Commands go first when memory runs out, the server last. Set by a shell of
17
+ * ours just before it becomes bwrap, so everything in the sandbox inherits it
18
+ * (raising one's own score needs no privilege), whether or not the sandbox's
19
+ * `/proc` is writable. The same shell lowers the command's priority and caps
20
+ * the size of any one file it writes, when the host asks: soft limits that need
21
+ * no cgroup of their own, which a pod rarely lets a process make.
22
+ */
23
+ export declare function launchArgs(limits?: CommandLimits): string[];
24
+ /** What a host may hold each command to; unset or 0 is no limit. */
25
+ export interface CommandLimits {
26
+ /** how much lower the command's CPU priority is than the server's (1-19) */
27
+ nice?: number;
28
+ /** the largest file a command may write (RLIMIT_FSIZE); a bigger write fails with EFBIG */
29
+ fileSizeBytes?: number;
30
+ /** the size of the command's own /tmp, which is memory (tmpfs) */
31
+ tmpBytes?: number;
32
+ }
15
33
  /** `--proc`: a `/proc` of the command's own, or the container's, bound read-only. */
16
34
  export type ProcMode = 'fresh' | 'bind';
17
35
  export interface BwrapProbe {
@@ -32,6 +50,8 @@ export interface BwrapHost {
32
50
  env?: NodeJS.ProcessEnv;
33
51
  /** more paths no command sees, besides `/state` and `/run/secrets` (always hidden) */
34
52
  hidden?: readonly string[];
53
+ /** soft limits on every command: priority, file size, /tmp size */
54
+ limits?: CommandLimits;
35
55
  /**
36
56
  * What this image was built from, which `prepare` checks the spec against;
37
57
  * from `ZEN_IMAGE_BUILD` / `ZEN_IMAGE_BASE` when unset. Neither known, or
@@ -119,6 +139,8 @@ export interface BwrapArgs {
119
139
  proc: ProcMode;
120
140
  network: SandboxNetwork;
121
141
  strict: boolean;
142
+ /** the size of the command's /tmp; strict mode's own cap applies when smaller */
143
+ tmpBytes?: number;
122
144
  user?: {
123
145
  uid: string;
124
146
  gid?: string;
package/dist/bwrap.js CHANGED
@@ -53,9 +53,22 @@ const JOB_SCRIPT = '/tmp/.zenera-job.sh';
53
53
  * Commands go first when memory runs out, the server last. Set by a shell of
54
54
  * ours just before it becomes bwrap, so everything in the sandbox inherits it
55
55
  * (raising one's own score needs no privilege), whether or not the sandbox's
56
- * `/proc` is writable.
56
+ * `/proc` is writable. The same shell lowers the command's priority and caps
57
+ * the size of any one file it writes, when the host asks: soft limits that need
58
+ * no cgroup of their own, which a pod rarely lets a process make.
57
59
  */
58
- const LAUNCH = ['-c', '{ echo 1000 > /proc/self/oom_score_adj; } 2>/dev/null; exec "$0" "$@"'];
60
+ export function launchArgs(limits = {}) {
61
+ const steps = ['{ echo 1000 > /proc/self/oom_score_adj; } 2>/dev/null'];
62
+ // `sh`'s `ulimit -f` counts 512-byte blocks (POSIX; bash in sh mode, dash and ash alike).
63
+ if (limits.fileSizeBytes && limits.fileSizeBytes > 0) {
64
+ steps.push(`ulimit -f ${Math.ceil(limits.fileSizeBytes / 512)}`);
65
+ }
66
+ const nice = Math.trunc(limits.nice ?? 0);
67
+ steps.push(nice > 0
68
+ ? `if command -v nice >/dev/null 2>&1; then exec nice -n ${Math.min(nice, 19)} "$0" "$@"; fi; exec "$0" "$@"`
69
+ : 'exec "$0" "$@"');
70
+ return ['-c', steps.join('; ')];
71
+ }
59
72
  const CGROUP = '/sys/fs/cgroup';
60
73
  export const hostTree = {
61
74
  kind(path) {
@@ -170,7 +183,7 @@ export class BwrapSandbox {
170
183
  ? AbortSignal.any([opts.signal, this.#closing.signal])
171
184
  : this.#closing.signal;
172
185
  // The script goes down stdin, so no text of the agent's is ever part of a parse here.
173
- const res = await this.#run('/bin/sh', [...LAUNCH, this.#bwrap, ...this.#args(cwd, ['/bin/sh', '-s'])], {
186
+ const res = await this.#run('/bin/sh', [...launchArgs(this.#host.limits), this.#bwrap, ...this.#args(cwd, ['/bin/sh', '-s'])], {
174
187
  input: command,
175
188
  timeoutMs: this.#timeout(opts.timeout),
176
189
  signal,
@@ -202,7 +215,7 @@ export class BwrapSandbox {
202
215
  let child;
203
216
  try {
204
217
  child = spawn('/bin/sh', [
205
- ...LAUNCH,
218
+ ...launchArgs(this.#host.limits),
206
219
  this.#bwrap,
207
220
  ...this.#args(cwd, ['/bin/sh', JOB_SCRIPT], ['--ro-bind', script, JOB_SCRIPT]),
208
221
  ],
@@ -336,6 +349,7 @@ export class BwrapSandbox {
336
349
  proc: this.#proc,
337
350
  network: s.network,
338
351
  strict: s.hardening === 'strict',
352
+ tmpBytes: this.#host.limits?.tmpBytes,
339
353
  user: s.user,
340
354
  cwd,
341
355
  extra,
@@ -449,8 +463,9 @@ export function bwrapArgs(a) {
449
463
  }
450
464
  }
451
465
  args.push(...(a.proc === 'fresh' ? ['--proc', '/proc'] : ['--ro-bind', '/proc', '/proc']));
452
- if (a.strict) {
453
- args.push('--size', String(STRICT_TMP_BYTES));
466
+ if (a.strict || a.tmpBytes) {
467
+ const sizes = [a.strict ? STRICT_TMP_BYTES : 0, a.tmpBytes ?? 0].filter((n) => n > 0);
468
+ args.push('--size', String(Math.min(...sizes)));
454
469
  }
455
470
  args.push('--tmpfs', '/tmp');
456
471
  for (const b of a.binds) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenera/sandbox",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "Where an agent's commands run and the files they see: the Sandbox contract, podman and bubblewrap implementations, filesystem drivers and a contract test suite for other implementations.",
5
5
  "keywords": [
6
6
  "sandbox",