@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 +22 -0
- package/dist/bwrap.js +21 -6
- package/package.json +1 -1
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
|
-
|
|
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', [...
|
|
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
|
-
...
|
|
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
|
-
|
|
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.
|
|
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",
|