@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.
@@ -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 first task on this thread and the rest on the pool, in the order they
612
- * were spawned, which is the order a task no worker is free for runs in here,
613
- * and the order every task runs in when the scope is joined inside another
614
- * job (`nish_jobs_run`). */
615
- nish_jobs_run(&mine->job, mine->job.next);
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 names a failure prints; a number for any other. */
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 ", 6);
191
- for (int i = 0; i < n; i++) buf[6 + i] = digits[n - 1 - i];
192
- buf[6 + n] = 0;
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
- char tail[40] = " (";
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`. A rule granting
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 | O_DIRECTORY);
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 directory of a scope list, each granting `rights`. */
301
- static void nish_sb_scope(int ruleset, const char *dirs, uint64_t rights, int32_t mode) {
302
- for (; dirs && *dirs; dirs += strlen(dirs) + 1) nish_sb_rule(ruleset, dirs, rights, mode);
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