@amritk/nish-x86_64-linux 0.28.0 → 0.30.0
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/INSTALL.md +8 -5
- package/bin/nish +0 -0
- package/docs/AI.md +114 -24
- package/package.json +1 -1
- package/runtime/core/host.d.ts +2 -1
- package/runtime/core/host.mjs +4 -0
- package/runtime/nish.d.ts +127 -0
- package/runtime/nish.h +94 -5
- package/runtime/nish.mjs +18 -4
- package/runtime/runtime-net.c +386 -5
- package/runtime/runtime-os.c +59 -8
- package/runtime/runtime-parallel.c +60 -5
- package/runtime/runtime-region.c +231 -0
- package/runtime/runtime-sandbox.c +43 -34
- package/runtime/runtime-shm.c +541 -0
- package/runtime/runtime.c +838 -792
- package/scripts/build.sh +55 -12
- package/std/README.md +12 -3
- package/std/collections.ts +5 -5
- package/std/crypto/hmac.ts +305 -2
- package/std/crypto/jwt.ts +1661 -0
- package/std/json-typed.ts +34 -0
- package/std/json.ts +35 -6
- package/std/net/handler.ts +1348 -0
- package/std/net/http-client.ts +97 -16
- package/std/net/http-compress.ts +54 -12
- package/std/net/http1-client.ts +270 -41
- package/std/net/http1-server.ts +145 -32
- package/std/net/http1.ts +274 -51
- package/std/net/outbound.ts +409 -0
- package/std/net/postgres-carrier.ts +985 -0
- package/std/net/postgres.ts +297 -4
- package/std/net/prefork.ts +344 -20
- package/std/net/upstream.ts +759 -0
- package/std/secret.ts +1 -1
- package/std/sync/ring.ts +587 -0
- package/std/sync/shm.ts +284 -0
- package/std/term-rate.ts +13 -1
- package/std/threads.ts +35 -3
- /package/runtime/{LICENSE-ryu → LICENSE-zmij} +0 -0
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
|
|
42
42
|
#if defined(NISH_THREADS) && !defined(__wasi__) && !defined(__wasm__)
|
|
43
43
|
#define NISH_PAR_REAL 1
|
|
44
|
+
#include <errno.h>
|
|
44
45
|
#include <pthread.h>
|
|
45
46
|
#include <sched.h>
|
|
46
47
|
#ifdef __GLIBC__
|
|
@@ -360,6 +361,51 @@ static void *nish_job_thread(void *p) {
|
|
|
360
361
|
return 0;
|
|
361
362
|
}
|
|
362
363
|
|
|
364
|
+
/* The end of the program when a thread with the stack a scope asked for
|
|
365
|
+
* cannot be started: running the task on a smaller one would trade the panic
|
|
366
|
+
* for an overflow nobody can read. `limit` is pthread's EAGAIN, the system out
|
|
367
|
+
* of threads or of memory for the stack; anything else is the size refused. */
|
|
368
|
+
__attribute__((noinline, cold)) static void nish_stack_fail(int limit) {
|
|
369
|
+
static const char size[] = "nish: cannot start a thread with the stack scopeWithStack asked for\n";
|
|
370
|
+
static const char room[] =
|
|
371
|
+
"nish: cannot start a thread for scopeWithStack: out of threads or of memory for its stack\n";
|
|
372
|
+
nish_panic_claim();
|
|
373
|
+
if (limit)
|
|
374
|
+
(void)!write(2, room, sizeof room - 1);
|
|
375
|
+
else
|
|
376
|
+
(void)!write(2, size, sizeof size - 1);
|
|
377
|
+
nish_fatal_exit();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/* Runs every job of the list `jobs` on a thread of its own with a stack of
|
|
381
|
+
* `stack` bytes, as the unpooled path does, and returns once every one has
|
|
382
|
+
* finished; the calling thread runs none of them, since its own stack is the
|
|
383
|
+
* one that was too small. The size is rounded up to the page and to the
|
|
384
|
+
* platform's minimum, a page is added for what the C library keeps at the top
|
|
385
|
+
* of a thread's stack, and a guard page sits below it, so a task that
|
|
386
|
+
* overflows faults. A job runs at depth 1, as on a worker, so anything it
|
|
387
|
+
* divides runs inline. Out of line and cold, so `nish_scope_join` keeps the
|
|
388
|
+
* body it had before the option, plus one load and a branch. */
|
|
389
|
+
__attribute__((noinline, cold)) static void nish_jobs_run_sized(nish_job *jobs, int64_t stack) {
|
|
390
|
+
long sys = sysconf(_SC_PAGESIZE);
|
|
391
|
+
size_t page = sys > 0 ? (size_t)sys : 4096;
|
|
392
|
+
size_t size = ((size_t)stack + page - 1) / page * page;
|
|
393
|
+
/* sysconf, not PTHREAD_STACK_MIN: a strict -std=c11 build hides the macro. */
|
|
394
|
+
long min = sysconf(_SC_THREAD_STACK_MIN);
|
|
395
|
+
if (min > 0 && size < (size_t)min) size = (size_t)min;
|
|
396
|
+
pthread_attr_t attr;
|
|
397
|
+
int rc = pthread_attr_init(&attr);
|
|
398
|
+
if (rc != 0) nish_stack_fail(rc == EAGAIN || rc == ENOMEM);
|
|
399
|
+
if (pthread_attr_setguardsize(&attr, page) != 0 || pthread_attr_setstacksize(&attr, size + page) != 0)
|
|
400
|
+
nish_stack_fail(0);
|
|
401
|
+
for (nish_job *j = jobs; j; j = j->next) {
|
|
402
|
+
rc = pthread_create(&j->th, &attr, nish_job_thread, j);
|
|
403
|
+
if (rc != 0) nish_stack_fail(rc == EAGAIN);
|
|
404
|
+
}
|
|
405
|
+
pthread_attr_destroy(&attr);
|
|
406
|
+
for (nish_job *j = jobs; j; j = j->next) pthread_join(j->th, 0);
|
|
407
|
+
}
|
|
408
|
+
|
|
363
409
|
/* Runs `lead` on this thread and the list `rest` elsewhere, oldest first, and
|
|
364
410
|
* returns once every one of them has finished. The caller runs `lead` itself,
|
|
365
411
|
* so N jobs keep N-1 other threads busy and this one is not idle, and then
|
|
@@ -608,11 +654,20 @@ void nish_scope_join(void *scope) {
|
|
|
608
654
|
}
|
|
609
655
|
if (!mine) return;
|
|
610
656
|
#ifdef NISH_PAR_REAL
|
|
611
|
-
/* The
|
|
612
|
-
*
|
|
613
|
-
*
|
|
614
|
-
*
|
|
615
|
-
|
|
657
|
+
/* The stack `scopeWithStack` asked for, `ThreadScope`'s first and only field
|
|
658
|
+
* (std/threads.ts), or 0 for `scope()`. A scope with one runs every task on
|
|
659
|
+
* a thread of its own that has it; the checker refuses a scope inside a
|
|
660
|
+
* task, so this thread is never one of those. */
|
|
661
|
+
int32_t stack = *(const int32_t *)scope;
|
|
662
|
+
if (__builtin_expect(stack > 0, 0)) {
|
|
663
|
+
nish_jobs_run_sized(&mine->job, stack);
|
|
664
|
+
} else {
|
|
665
|
+
/* The first task on this thread and the rest on the pool, in the order
|
|
666
|
+
* they were spawned, which is the order a task no worker is free for runs
|
|
667
|
+
* in here, and the order every task runs in when the scope is joined
|
|
668
|
+
* inside another job (`nish_jobs_run`). */
|
|
669
|
+
nish_jobs_run(&mine->job, mine->job.next);
|
|
670
|
+
}
|
|
616
671
|
#else
|
|
617
672
|
for (nish_task *t = mine; t; t = t->next) t->run(t->payload);
|
|
618
673
|
#endif
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/* Nish runtime, the region half: arenas a program makes, enters, leaves and
|
|
2
|
+
* frees one at a time, so a long-running program can give back the memory of
|
|
3
|
+
* one session, one cache entry or one connection without a garbage collector
|
|
4
|
+
* (docs/wp46-long-running-memory.md, section 2.1, "Runtime and ABI").
|
|
5
|
+
*
|
|
6
|
+
* A translation unit of its own, paired with runtime.c in scripts/build.sh's
|
|
7
|
+
* `halves` and src/run-cache.ts's `runtimeUnits()` like the others, so it has
|
|
8
|
+
* its own measured `.text*` ceiling in tests/runtime-budgets.js and a program
|
|
9
|
+
* that never makes a region links none of it. The wasm profile has no malloc
|
|
10
|
+
* to give a region its chunks, so `scripts/build.sh --profile wasm` refuses IR
|
|
11
|
+
* that names `@nish_region_`; the wasi profile links this file.
|
|
12
|
+
*
|
|
13
|
+
* **A handle is a number.** `nish_region_new` answers an `i64`: the slot's
|
|
14
|
+
* index in the low 24 bits and the slot's generation in the 40 above them.
|
|
15
|
+
* Generations start at 1, so 0 -- what a fresh `Region<T>[]` holds -- is never
|
|
16
|
+
* a handle. `nish_region_free` moves the slot's generation on, so a copy of a
|
|
17
|
+
* freed handle is stale; a slot whose generation would pass 40 bits is
|
|
18
|
+
* retired rather than reused, so no handle ever names a slot twice. Every
|
|
19
|
+
* call that takes a handle checks it first and panics on a zero handle, a
|
|
20
|
+
* stale one, and one made on another thread. The 24-bit index caps a process
|
|
21
|
+
* at 16,777,216 live regions; `nish_region_new` past that panics.
|
|
22
|
+
*
|
|
23
|
+
* **The table** is one per process: a directory of pages, each page 1,024
|
|
24
|
+
* slots allocated (zeroed) the first time a slot in it is handed out, so a
|
|
25
|
+
* program that makes ten regions holds one page. Taking a slot and giving one
|
|
26
|
+
* back take the table's lock, once per `new` and once per `free`; `enter` and
|
|
27
|
+
* `leave` take none, because a slot is only ever entered by the thread that
|
|
28
|
+
* made it and the check reads the two fields another thread's `free` could
|
|
29
|
+
* write (generation and thread) with atomic loads. Only a threads build
|
|
30
|
+
* (`-DNISH_THREADS`) has a second thread to lock against, so only it pays for
|
|
31
|
+
* the lock.
|
|
32
|
+
*
|
|
33
|
+
* **Entering swaps arenas.** A slot holds a whole `struct nish_arena`.
|
|
34
|
+
* `nish_region_enter` swaps it with the thread's `nish_arena`, so every
|
|
35
|
+
* allocation the program makes until `nish_region_leave` -- the inlined bump
|
|
36
|
+
* as well as the runtime's own -- lands in the region; `leave` swaps them
|
|
37
|
+
* back. While a region is entered its slot holds the arena it displaced, so
|
|
38
|
+
* entering a second region inside the first nests: each leave restores what
|
|
39
|
+
* its enter took. A slot that is entered cannot be entered again (it would
|
|
40
|
+
* swap its own arena with itself) or freed, and `leave` must name the region
|
|
41
|
+
* entered last on this thread.
|
|
42
|
+
*
|
|
43
|
+
* **Chunk size.** A thread's own arena grows by 64 KiB chunks, which is right
|
|
44
|
+
* for one arena a thread and wrong for a hundred thousand of them. A region's
|
|
45
|
+
* first chunk is the size of its first allocation, and each later chunk
|
|
46
|
+
* doubles, or takes the allocation's own size when that is more. The size of
|
|
47
|
+
* the next chunk is runtime.c's thread-local `nish_region_chunk`, which
|
|
48
|
+
* `nish_arena_grow` reads on its slow path and which `enter` and `leave` swap
|
|
49
|
+
* with the slot's as they swap the arena; 0 there means the thread's own
|
|
50
|
+
* arena and the 64 KiB floor. A region starts it at 8, the smallest block.
|
|
51
|
+
*
|
|
52
|
+
* **The root.** `Region<T>` is only the number, so the slot keeps the root
|
|
53
|
+
* object `region`'s function answered: `nish_region_leave` records it and
|
|
54
|
+
* `nish_region_enter` answers it.
|
|
55
|
+
*
|
|
56
|
+
* Freeing a region frees its chunks through runtime.c's `nish_free_arena`,
|
|
57
|
+
* with the region's arena swapped in for the call, so an `-DNISH_ASAN` build
|
|
58
|
+
* forgets the freed blocks as it does for the thread's arena. */
|
|
59
|
+
|
|
60
|
+
#include <stdint.h>
|
|
61
|
+
#include <stdlib.h>
|
|
62
|
+
|
|
63
|
+
#include "nish.h"
|
|
64
|
+
|
|
65
|
+
#define NISH_COLD __attribute__((noreturn, cold, noinline))
|
|
66
|
+
|
|
67
|
+
/* runtime.c's: the size of the next chunk `nish_arena_grow` makes for the
|
|
68
|
+
arena in `nish_arena`, or 0 for a thread's own, which takes 64 KiB. */
|
|
69
|
+
extern NISH_TLS uint64_t nish_region_chunk;
|
|
70
|
+
|
|
71
|
+
#define NISH_REGION_SLOT_BITS 24
|
|
72
|
+
#define NISH_REGION_SLOTS ((uint64_t)1 << NISH_REGION_SLOT_BITS)
|
|
73
|
+
#define NISH_REGION_GEN_END ((uint64_t)1 << 40)
|
|
74
|
+
#define NISH_REGION_PAGE 1024
|
|
75
|
+
|
|
76
|
+
typedef struct nish_region_slot {
|
|
77
|
+
struct nish_arena arena; /* the region's arena, or, while entered, the one it displaced */
|
|
78
|
+
uint64_t gen; /* the live handle's generation; NISH_REGION_GEN_END once retired */
|
|
79
|
+
uint64_t thread; /* the thread that made it, 0 while the slot is free */
|
|
80
|
+
uint64_t chunk; /* the region's next chunk size, or, while entered, the displaced one */
|
|
81
|
+
uint64_t outer; /* while entered: the handle entered before it; while free: the next free slot + 1 */
|
|
82
|
+
void *root; /* what `leave` recorded, answered by `enter` */
|
|
83
|
+
uint64_t entered;
|
|
84
|
+
} nish_region_slot;
|
|
85
|
+
|
|
86
|
+
static nish_region_slot *nish_region_pages[NISH_REGION_SLOTS / NISH_REGION_PAGE];
|
|
87
|
+
static uint64_t nish_region_used; /* slots ever handed out: the next never-used index */
|
|
88
|
+
static uint64_t nish_region_free_list; /* a free slot's index + 1, or 0 */
|
|
89
|
+
/* The handle entered last on this thread, or 0. */
|
|
90
|
+
static NISH_TLS uint64_t nish_region_current;
|
|
91
|
+
|
|
92
|
+
#ifdef NISH_THREADS
|
|
93
|
+
static int nish_region_lock_word;
|
|
94
|
+
static uint64_t nish_region_threads;
|
|
95
|
+
static NISH_TLS uint64_t nish_region_thread_id;
|
|
96
|
+
static void nish_region_lock(void) {
|
|
97
|
+
while (__atomic_exchange_n(&nish_region_lock_word, 1, __ATOMIC_ACQUIRE))
|
|
98
|
+
while (__atomic_load_n(&nish_region_lock_word, __ATOMIC_RELAXED)) {}
|
|
99
|
+
}
|
|
100
|
+
static void nish_region_unlock(void) { __atomic_store_n(&nish_region_lock_word, 0, __ATOMIC_RELEASE); }
|
|
101
|
+
/* A number no other thread of the process has: 64 bits, so it never wraps
|
|
102
|
+
back onto a live thread's. */
|
|
103
|
+
static uint64_t nish_region_me(void) {
|
|
104
|
+
if (!nish_region_thread_id) nish_region_thread_id = __atomic_add_fetch(&nish_region_threads, 1, __ATOMIC_RELAXED);
|
|
105
|
+
return nish_region_thread_id;
|
|
106
|
+
}
|
|
107
|
+
#else
|
|
108
|
+
static void nish_region_lock(void) {}
|
|
109
|
+
static void nish_region_unlock(void) {}
|
|
110
|
+
static uint64_t nish_region_me(void) { return 1; }
|
|
111
|
+
#endif
|
|
112
|
+
|
|
113
|
+
/* The panics, by number: one cold function and one table, rather than a
|
|
114
|
+
message's address loaded at each call site. Each ends in `nish_panic`, so
|
|
115
|
+
only the first panic of a process prints and atexit handlers run. */
|
|
116
|
+
enum {
|
|
117
|
+
NISH_REGION_ZERO,
|
|
118
|
+
NISH_REGION_STALE,
|
|
119
|
+
NISH_REGION_THREAD,
|
|
120
|
+
NISH_REGION_ENTERED,
|
|
121
|
+
NISH_REGION_FREE_ENTERED,
|
|
122
|
+
NISH_REGION_LEAVE,
|
|
123
|
+
NISH_REGION_FULL,
|
|
124
|
+
NISH_REGION_OOM,
|
|
125
|
+
};
|
|
126
|
+
#define NISH_REGION_MSG(text) { sizeof text - 1, text }
|
|
127
|
+
static const struct { uint64_t len; char data[72]; } nish_region_messages[] = {
|
|
128
|
+
NISH_REGION_MSG("nish: a zero region handle: no region was made for it"),
|
|
129
|
+
NISH_REGION_MSG("nish: a stale region handle: the region was freed"),
|
|
130
|
+
NISH_REGION_MSG("nish: a region handle made on another thread"),
|
|
131
|
+
NISH_REGION_MSG("nish: enter of a region that is already entered"),
|
|
132
|
+
NISH_REGION_MSG("nish: free of a region that is entered"),
|
|
133
|
+
NISH_REGION_MSG("nish: leave of a region that is not the one entered last"),
|
|
134
|
+
NISH_REGION_MSG("nish: more than 16777216 live regions"),
|
|
135
|
+
NISH_REGION_MSG("nish: out of memory"),
|
|
136
|
+
};
|
|
137
|
+
static NISH_COLD void nish_region_die(int why) {
|
|
138
|
+
nish_panic((const nish_str *)&nish_region_messages[why]);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* The slot `r` names, or a panic: zero, stale, another thread's. */
|
|
142
|
+
static nish_region_slot *nish_region_slot_of(uint64_t r) {
|
|
143
|
+
if (!r) nish_region_die(NISH_REGION_ZERO);
|
|
144
|
+
uint64_t index = r & (NISH_REGION_SLOTS - 1);
|
|
145
|
+
nish_region_slot *page = __atomic_load_n(&nish_region_pages[index / NISH_REGION_PAGE], __ATOMIC_ACQUIRE);
|
|
146
|
+
if (!page) nish_region_die(NISH_REGION_STALE);
|
|
147
|
+
nish_region_slot *s = &page[index % NISH_REGION_PAGE];
|
|
148
|
+
uint64_t thread = __atomic_load_n(&s->thread, __ATOMIC_RELAXED);
|
|
149
|
+
if (!thread || __atomic_load_n(&s->gen, __ATOMIC_RELAXED) != r >> NISH_REGION_SLOT_BITS)
|
|
150
|
+
nish_region_die(NISH_REGION_STALE);
|
|
151
|
+
if (thread != nish_region_me()) nish_region_die(NISH_REGION_THREAD);
|
|
152
|
+
return s;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* Swap the slot's arena and chunk size with the thread's: `enter` and `leave`
|
|
156
|
+
both, and `free` around its `nish_free_arena`. */
|
|
157
|
+
static void nish_region_swap(nish_region_slot *s) {
|
|
158
|
+
struct nish_arena a = nish_arena;
|
|
159
|
+
nish_arena = s->arena;
|
|
160
|
+
s->arena = a;
|
|
161
|
+
uint64_t c = nish_region_chunk;
|
|
162
|
+
nish_region_chunk = s->chunk;
|
|
163
|
+
s->chunk = c;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
uint64_t nish_region_new(void) {
|
|
167
|
+
uint64_t me = nish_region_me();
|
|
168
|
+
nish_region_lock();
|
|
169
|
+
uint64_t index;
|
|
170
|
+
nish_region_slot *s;
|
|
171
|
+
if (nish_region_free_list) {
|
|
172
|
+
index = nish_region_free_list - 1;
|
|
173
|
+
s = &nish_region_pages[index / NISH_REGION_PAGE][index % NISH_REGION_PAGE];
|
|
174
|
+
nish_region_free_list = s->outer;
|
|
175
|
+
} else {
|
|
176
|
+
if (nish_region_used == NISH_REGION_SLOTS) nish_region_die(NISH_REGION_FULL);
|
|
177
|
+
index = nish_region_used++;
|
|
178
|
+
nish_region_slot **page = &nish_region_pages[index / NISH_REGION_PAGE];
|
|
179
|
+
if (!*page) {
|
|
180
|
+
nish_region_slot *p = calloc(NISH_REGION_PAGE, sizeof *p);
|
|
181
|
+
if (!p) nish_region_die(NISH_REGION_OOM);
|
|
182
|
+
__atomic_store_n(page, p, __ATOMIC_RELEASE);
|
|
183
|
+
}
|
|
184
|
+
s = &(*page)[index % NISH_REGION_PAGE];
|
|
185
|
+
__atomic_store_n(&s->gen, 1, __ATOMIC_RELAXED);
|
|
186
|
+
}
|
|
187
|
+
s->chunk = 8;
|
|
188
|
+
s->outer = 0;
|
|
189
|
+
__atomic_store_n(&s->thread, me, __ATOMIC_RELAXED);
|
|
190
|
+
uint64_t r = s->gen << NISH_REGION_SLOT_BITS | index;
|
|
191
|
+
nish_region_unlock();
|
|
192
|
+
return r;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
void *nish_region_enter(uint64_t r) {
|
|
196
|
+
nish_region_slot *s = nish_region_slot_of(r);
|
|
197
|
+
if (s->entered) nish_region_die(NISH_REGION_ENTERED);
|
|
198
|
+
s->entered = 1;
|
|
199
|
+
s->outer = nish_region_current;
|
|
200
|
+
nish_region_current = r;
|
|
201
|
+
nish_region_swap(s);
|
|
202
|
+
return s->root;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
void nish_region_leave(uint64_t r, void *root) {
|
|
206
|
+
nish_region_slot *s = nish_region_slot_of(r);
|
|
207
|
+
if (nish_region_current != r) nish_region_die(NISH_REGION_LEAVE);
|
|
208
|
+
nish_region_swap(s);
|
|
209
|
+
nish_region_current = s->outer;
|
|
210
|
+
s->entered = 0;
|
|
211
|
+
s->root = root;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
void nish_region_free(uint64_t r) {
|
|
215
|
+
nish_region_slot *s = nish_region_slot_of(r);
|
|
216
|
+
if (s->entered) nish_region_die(NISH_REGION_FREE_ENTERED);
|
|
217
|
+
nish_region_swap(s);
|
|
218
|
+
nish_free_arena();
|
|
219
|
+
nish_region_swap(s);
|
|
220
|
+
s->root = 0;
|
|
221
|
+
uint64_t index = r & (NISH_REGION_SLOTS - 1);
|
|
222
|
+
nish_region_lock();
|
|
223
|
+
__atomic_store_n(&s->thread, 0, __ATOMIC_RELAXED);
|
|
224
|
+
uint64_t gen = s->gen + 1;
|
|
225
|
+
__atomic_store_n(&s->gen, gen, __ATOMIC_RELAXED);
|
|
226
|
+
if (gen < NISH_REGION_GEN_END) {
|
|
227
|
+
s->outer = nish_region_free_list;
|
|
228
|
+
nish_region_free_list = index + 1;
|
|
229
|
+
}
|
|
230
|
+
nish_region_unlock();
|
|
231
|
+
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* before any constructor in the executable, an `ffi` object or a shared
|
|
13
13
|
* library:
|
|
14
14
|
*
|
|
15
|
-
* nish_sandbox(caps, mode, read_dirs, write_dirs)
|
|
15
|
+
* nish_sandbox(caps, mode, read_dirs, write_dirs, exec_paths)
|
|
16
16
|
*
|
|
17
17
|
* `caps` is `main`'s capability mask (src/capabilities.ts numbers the bits),
|
|
18
18
|
* `mode` is 1 for `--sandbox` and 2 for `--sandbox=best-effort`. The two
|
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
* scopes, each a run of NUL-terminated absolute paths ended by an empty one,
|
|
21
21
|
* or null when the capability is granted whole or not at all; a list moves
|
|
22
22
|
* its capability's rights from the rule on `/` to one rule per directory.
|
|
23
|
+
* `exec_paths` is the programs a dependency's `process.spawn=<program>`
|
|
24
|
+
* grants name (WP40 §2a), and their interpreters, in the same shape: with
|
|
25
|
+
* it, `EXECUTE` leaves the rule on `/` and each program gets a rule of its own.
|
|
23
26
|
*
|
|
24
27
|
* What it does, in the order of the note's section 3, each step before the
|
|
25
28
|
* next because each needs the one before it:
|
|
@@ -169,27 +172,28 @@ static const uint16_t nish_sb_uring[] = {425, 426, 427}; /* io_uring_setup, _ent
|
|
|
169
172
|
/* Set by step 6 and read by `nish_sandbox_check`. */
|
|
170
173
|
static int nish_sb_applied = 0;
|
|
171
174
|
|
|
172
|
-
/* The errno
|
|
175
|
+
/* The errno a failure prints, as the line's parenthesized tail: its name, or
|
|
176
|
+
* its number for any other. */
|
|
173
177
|
static const char *nish_sb_errno_name(int e, char *buf) {
|
|
174
178
|
switch (e) {
|
|
175
|
-
case EPERM: return "EPERM";
|
|
176
|
-
case ENOSYS: return "ENOSYS";
|
|
177
|
-
case EOPNOTSUPP: return "EOPNOTSUPP";
|
|
178
|
-
case EINVAL: return "EINVAL";
|
|
179
|
-
case EACCES: return "EACCES";
|
|
180
|
-
case ENOENT: return "ENOENT";
|
|
181
|
-
case EFAULT: return "EFAULT";
|
|
182
|
-
case E2BIG: return "E2BIG";
|
|
183
|
-
case ENOMEM: return "ENOMEM";
|
|
184
|
-
case EBUSY: return "EBUSY";
|
|
179
|
+
case EPERM: return " (EPERM)";
|
|
180
|
+
case ENOSYS: return " (ENOSYS)";
|
|
181
|
+
case EOPNOTSUPP: return " (EOPNOTSUPP)";
|
|
182
|
+
case EINVAL: return " (EINVAL)";
|
|
183
|
+
case EACCES: return " (EACCES)";
|
|
184
|
+
case ENOENT: return " (ENOENT)";
|
|
185
|
+
case EFAULT: return " (EFAULT)";
|
|
186
|
+
case E2BIG: return " (E2BIG)";
|
|
187
|
+
case ENOMEM: return " (ENOMEM)";
|
|
188
|
+
case EBUSY: return " (EBUSY)";
|
|
185
189
|
}
|
|
186
190
|
char digits[12];
|
|
187
191
|
int n = 0;
|
|
188
192
|
unsigned u = (unsigned)e;
|
|
189
193
|
do digits[n++] = (char)('0' + u % 10); while ((u /= 10) != 0 && n < 11);
|
|
190
|
-
memcpy(buf, "errno ",
|
|
191
|
-
for (int i = 0; i < n; i++) buf[
|
|
192
|
-
buf
|
|
194
|
+
memcpy(buf, " (errno ", 8);
|
|
195
|
+
for (int i = 0; i < n; i++) buf[8 + i] = digits[n - 1 - i];
|
|
196
|
+
memcpy(buf + 8 + n, ")", 2);
|
|
193
197
|
return buf;
|
|
194
198
|
}
|
|
195
199
|
|
|
@@ -205,12 +209,7 @@ static void nish_sb_say(const char *a, const char *b, const char *c) {
|
|
|
205
209
|
* named after `what`. */
|
|
206
210
|
static void nish_sb_fail_at(const char *what, const char *path) {
|
|
207
211
|
char buf[24];
|
|
208
|
-
|
|
209
|
-
const char *name = nish_sb_errno_name(errno, buf);
|
|
210
|
-
size_t len = strlen(name);
|
|
211
|
-
memcpy(tail + 2, name, len);
|
|
212
|
-
memcpy(tail + 2 + len, ")", 2);
|
|
213
|
-
nish_sb_say(what, path, tail);
|
|
212
|
+
nish_sb_say(what, path, nish_sb_errno_name(errno, buf));
|
|
214
213
|
_exit(126);
|
|
215
214
|
}
|
|
216
215
|
|
|
@@ -279,15 +278,19 @@ static size_t nish_sb_name(char *line, size_t at, const char *name) {
|
|
|
279
278
|
return at + len;
|
|
280
279
|
}
|
|
281
280
|
|
|
282
|
-
/* One path_beneath rule granting `rights` beneath `path
|
|
281
|
+
/* One path_beneath rule granting `rights` beneath `path`, opened with
|
|
282
|
+
* `oflag` added: O_DIRECTORY for a scope directory, which refuses anything
|
|
283
|
+
* else, and 0 for a program the exec allowlist names, which O_PATH opens as
|
|
284
|
+
* a file. The failure lines name a directory either way, a program's too,
|
|
285
|
+
* because a second wording costs this unit's budget more than it says. A rule granting
|
|
283
286
|
* nothing is refused (ENOMSG), and nothing is what it would add, so it is
|
|
284
287
|
* left out. A scope directory gone since the build refuses strict; under
|
|
285
288
|
* best-effort it is named and its rule left out, which withholds the right
|
|
286
289
|
* there too, so the program loses the directory rather than gaining more. */
|
|
287
|
-
static void nish_sb_rule(int ruleset, const char *path, uint64_t rights, int32_t mode) {
|
|
290
|
+
static void nish_sb_rule(int ruleset, const char *path, uint64_t rights, int32_t mode, int oflag) {
|
|
288
291
|
struct nish_ll_path_beneath rule = {rights, -1};
|
|
289
292
|
if (!rights) return;
|
|
290
|
-
rule.parent_fd = open(path, O_PATH | O_CLOEXEC |
|
|
293
|
+
rule.parent_fd = open(path, O_PATH | O_CLOEXEC | oflag);
|
|
291
294
|
if (rule.parent_fd < 0) {
|
|
292
295
|
if (mode != 2) nish_sb_fail_at("cannot open the directory ", path);
|
|
293
296
|
nish_sb_say("the scope directory ", path, " cannot be opened, so the program gets no access beneath it");
|
|
@@ -297,9 +300,10 @@ static void nish_sb_rule(int ruleset, const char *path, uint64_t rights, int32_t
|
|
|
297
300
|
close(rule.parent_fd);
|
|
298
301
|
}
|
|
299
302
|
|
|
300
|
-
/* One rule per
|
|
301
|
-
|
|
302
|
-
|
|
303
|
+
/* One rule per path of a scope list, each granting `rights` and opened with
|
|
304
|
+
* `oflag` as `nish_sb_rule` says. */
|
|
305
|
+
static void nish_sb_scope(int ruleset, const char *dirs, uint64_t rights, int32_t mode, int oflag) {
|
|
306
|
+
for (; dirs && *dirs; dirs += strlen(dirs) + 1) nish_sb_rule(ruleset, dirs, rights, mode, oflag);
|
|
303
307
|
}
|
|
304
308
|
|
|
305
309
|
/* Step 4. Every right the ABI knows is handled, and one path_beneath rule on
|
|
@@ -307,7 +311,7 @@ static void nish_sb_scope(int ruleset, const char *dirs, uint64_t rights, int32_
|
|
|
307
311
|
* everywhere or nowhere; a scoped capability's rights are granted beneath its
|
|
308
312
|
* directories alone. Returns with nothing done when there is no Landlock and
|
|
309
313
|
* the mode is best-effort. */
|
|
310
|
-
static void nish_sb_landlock(int32_t caps, int32_t mode, const char *read_dirs, const char *write_dirs) {
|
|
314
|
+
static void nish_sb_landlock(int32_t caps, int32_t mode, const char *read_dirs, const char *write_dirs, const char *exec_paths) {
|
|
311
315
|
int abi = nish_sb_landlock_abi();
|
|
312
316
|
if (abi < 0) {
|
|
313
317
|
/* EPERM is a container runtime's own seccomp profile refusing the call;
|
|
@@ -331,6 +335,9 @@ static void nish_sb_landlock(int32_t caps, int32_t mode, const char *read_dirs,
|
|
|
331
335
|
if (read_dirs) whole &= ~CAP_BIT_FS_READ;
|
|
332
336
|
if (write_dirs) whole &= ~CAP_BIT_FS_WRITE;
|
|
333
337
|
uint64_t granted = nish_sb_granted(whole);
|
|
338
|
+
/* An exec allowlist (a dependency's process.spawn grant, WP40 §2a) keeps
|
|
339
|
+
* EXECUTE off `/` and grants it on each listed file alone. */
|
|
340
|
+
if (exec_paths) granted &= ~LL_EXECUTE;
|
|
334
341
|
uint64_t lost = ~granted & ~known & (LL_TRUNCATE | LL_IOCTL_DEV);
|
|
335
342
|
int unscoped = abi < 6;
|
|
336
343
|
/* Strict runs without IOCTL_DEV and without the ABI 6 scopes: a terminal
|
|
@@ -352,9 +359,10 @@ static void nish_sb_landlock(int32_t caps, int32_t mode, const char *read_dirs,
|
|
|
352
359
|
struct nish_ll_ruleset attr = {known, 0, abi >= 6 ? LL_SCOPE_ALL : 0};
|
|
353
360
|
int ruleset = (int)syscall(LL_CREATE, &attr, sizeof attr, 0);
|
|
354
361
|
if (ruleset < 0) nish_sb_fail("cannot create the Landlock ruleset");
|
|
355
|
-
nish_sb_rule(ruleset, "/", granted & known, 1);
|
|
356
|
-
if (read_dirs && (caps & CAP_BIT_FS_READ)) nish_sb_scope(ruleset, read_dirs, nish_sb_granted(CAP_BIT_FS_READ) & known, mode);
|
|
357
|
-
if (write_dirs && (caps & CAP_BIT_FS_WRITE)) nish_sb_scope(ruleset, write_dirs, nish_sb_granted(CAP_BIT_FS_WRITE) & known, mode);
|
|
362
|
+
nish_sb_rule(ruleset, "/", granted & known, 1, O_DIRECTORY);
|
|
363
|
+
if (read_dirs && (caps & CAP_BIT_FS_READ)) nish_sb_scope(ruleset, read_dirs, nish_sb_granted(CAP_BIT_FS_READ) & known, mode, O_DIRECTORY);
|
|
364
|
+
if (write_dirs && (caps & CAP_BIT_FS_WRITE)) nish_sb_scope(ruleset, write_dirs, nish_sb_granted(CAP_BIT_FS_WRITE) & known, mode, O_DIRECTORY);
|
|
365
|
+
if (caps & CAP_BIT_SPAWN) nish_sb_scope(ruleset, exec_paths, LL_EXECUTE, mode, 0);
|
|
358
366
|
if (syscall(LL_RESTRICT, ruleset, 0) != 0) nish_sb_fail("cannot apply the Landlock ruleset");
|
|
359
367
|
close(ruleset);
|
|
360
368
|
}
|
|
@@ -428,11 +436,11 @@ static void nish_sb_seccomp(int32_t caps) {
|
|
|
428
436
|
if (loaded != 0) nish_sb_fail("cannot load the seccomp filter");
|
|
429
437
|
}
|
|
430
438
|
|
|
431
|
-
void nish_sandbox(int32_t caps, int32_t mode, const char *read_dirs, const char *write_dirs) {
|
|
439
|
+
void nish_sandbox(int32_t caps, int32_t mode, const char *read_dirs, const char *write_dirs, const char *exec_paths) {
|
|
432
440
|
nish_cpu_prime();
|
|
433
441
|
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0) nish_sb_fail("cannot set no_new_privs");
|
|
434
442
|
nish_sb_drop_capabilities();
|
|
435
|
-
nish_sb_landlock(caps, mode, read_dirs, write_dirs);
|
|
443
|
+
nish_sb_landlock(caps, mode, read_dirs, write_dirs, exec_paths);
|
|
436
444
|
/* Metadata writes stay EPERM inside a scoped write directory (open
|
|
437
445
|
* question 1): Landlock has no right for chmod, chown or xattrs. */
|
|
438
446
|
nish_sb_seccomp(write_dirs ? caps & ~CAP_BIT_FS_WRITE : caps);
|
|
@@ -457,11 +465,12 @@ static void nish_sb_none(void) {
|
|
|
457
465
|
_exit(126);
|
|
458
466
|
}
|
|
459
467
|
|
|
460
|
-
void nish_sandbox(int32_t caps, int32_t mode, const char *read_dirs, const char *write_dirs) {
|
|
468
|
+
void nish_sandbox(int32_t caps, int32_t mode, const char *read_dirs, const char *write_dirs, const char *exec_paths) {
|
|
461
469
|
(void)caps;
|
|
462
470
|
(void)mode;
|
|
463
471
|
(void)read_dirs;
|
|
464
472
|
(void)write_dirs;
|
|
473
|
+
(void)exec_paths;
|
|
465
474
|
nish_sb_none();
|
|
466
475
|
}
|
|
467
476
|
|