@amritk/nish-x86_64-linux 0.27.2 → 0.28.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 +13 -8
- package/bin/nish +0 -0
- package/docs/AI.md +19 -2
- package/package.json +1 -1
- package/runtime/core.d.ts +47 -0
- package/runtime/core.mjs +67 -0
- package/runtime/nish.d.ts +49 -0
- package/runtime/nish.h +50 -5
- package/runtime/nish.mjs +6 -0
- package/runtime/runtime-checksum.c +270 -0
- package/runtime/runtime-parallel.c +21 -0
- package/runtime/runtime-sandbox.c +470 -0
- package/runtime/runtime-sha256.c +239 -0
- package/scripts/build.sh +44 -23
- package/std/README.md +15 -10
- package/std/compress/checksum.ts +88 -75
- package/std/compress/deflate.ts +224 -0
- package/std/compress/gzip.ts +52 -0
- package/std/crypto/bcrypt.ts +816 -280
- package/std/json-equal.ts +335 -0
- package/std/json-format.ts +1070 -0
- package/std/json-number.ts +428 -0
- package/std/json-pointer.ts +245 -0
- package/std/json-value.ts +510 -17
- package/std/net/http-compress.ts +259 -16
- package/std/net/http1-client.ts +104 -19
- package/std/net/http1-server.ts +61 -12
- package/std/net/postgres.ts +405 -0
- package/std/net/websocket.ts +295 -14
- package/std/regex.ts +613 -52
- package/std/term-io.ts +35 -8
- package/std/yaml-json.ts +841 -0
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
/* Nish runtime, the sandbox half: `--sandbox` (docs/wp37-os-sandbox.md)
|
|
2
|
+
* confines a native program to the capabilities its `main` reaches, and this
|
|
3
|
+
* file is the part that asks the kernel to.
|
|
4
|
+
*
|
|
5
|
+
* A translation unit of its own for the reason the other halves are one: each
|
|
6
|
+
* file carries its own measured `.text*` ceiling in tests/runtime-budgets.js,
|
|
7
|
+
* and runtime-os.c had 6 bytes of its ceiling left. scripts/build.sh pairs it
|
|
8
|
+
* with runtime.c like the others, and a program built without `--sandbox`
|
|
9
|
+
* calls nothing here, so `--gc-sections` drops all of it.
|
|
10
|
+
*
|
|
11
|
+
* The compiler emits one call, from an entry in `.preinit_array`, so it runs
|
|
12
|
+
* before any constructor in the executable, an `ffi` object or a shared
|
|
13
|
+
* library:
|
|
14
|
+
*
|
|
15
|
+
* nish_sandbox(caps, mode, read_dirs, write_dirs)
|
|
16
|
+
*
|
|
17
|
+
* `caps` is `main`'s capability mask (src/capabilities.ts numbers the bits),
|
|
18
|
+
* `mode` is 1 for `--sandbox` and 2 for `--sandbox=best-effort`. The two
|
|
19
|
+
* directory lists are the `--allow fs.read=<dir>` and `--allow fs.write=<dir>`
|
|
20
|
+
* scopes, each a run of NUL-terminated absolute paths ended by an empty one,
|
|
21
|
+
* or null when the capability is granted whole or not at all; a list moves
|
|
22
|
+
* its capability's rights from the rule on `/` to one rule per directory.
|
|
23
|
+
*
|
|
24
|
+
* What it does, in the order of the note's section 3, each step before the
|
|
25
|
+
* next because each needs the one before it:
|
|
26
|
+
*
|
|
27
|
+
* 1. prime the thread pool's ceiling, which reads /proc and /sys files that
|
|
28
|
+
* step 4 may close;
|
|
29
|
+
* 2. `PR_SET_NO_NEW_PRIVS`, which Landlock and seccomp need and which keeps
|
|
30
|
+
* an `exec` from gaining a capability back;
|
|
31
|
+
* 3. drop every capability, and read each set back;
|
|
32
|
+
* 4. Landlock: handle every filesystem right the running ABI knows, then
|
|
33
|
+
* grant back beneath `/` exactly what the mask grants whole, and beneath
|
|
34
|
+
* each scope directory what its capability grants;
|
|
35
|
+
* 5. the seccomp filter of the note's section 4, a denylist;
|
|
36
|
+
* 6. set the flag `nish_sandbox_check` reads at the top of `main`, so a C
|
|
37
|
+
* library that never ran `.preinit_array` (musl) refuses to run rather
|
|
38
|
+
* than running unconfined.
|
|
39
|
+
*
|
|
40
|
+
* A failure prints one line, `nish: --sandbox: <what> (<errno name>)`, and
|
|
41
|
+
* ends the process with `_exit(126)`, the shell's "found but cannot run", in
|
|
42
|
+
* both modes. Best-effort differs only where the kernel lacks a feature: no
|
|
43
|
+
* Landlock, or a Landlock too old to withhold a right; it then says what it
|
|
44
|
+
* could not withhold, once, and runs. No environment variable, file or
|
|
45
|
+
* argument is read that could widen any of it: step 1 reaches
|
|
46
|
+
* `getenv("NISH_NUM_THREADS")` through `nish_cpu_online`, which can only
|
|
47
|
+
* lower the pool's ceiling, and confines nothing. The one knob,
|
|
48
|
+
* `-DNISH_SANDBOX_SIMULATE_ABI=<n>`, makes the ABI probe answer `n` in a
|
|
49
|
+
* test-only build of this file; scripts/build.sh never passes it.
|
|
50
|
+
*
|
|
51
|
+
* Messages go through write(2) rather than stdio: nothing here may allocate or
|
|
52
|
+
* touch a `FILE` before libc has finished its own start-up. */
|
|
53
|
+
#define _GNU_SOURCE
|
|
54
|
+
#include "nish.h"
|
|
55
|
+
|
|
56
|
+
#include <unistd.h>
|
|
57
|
+
|
|
58
|
+
#ifdef __linux__
|
|
59
|
+
|
|
60
|
+
#include <errno.h>
|
|
61
|
+
#include <fcntl.h>
|
|
62
|
+
#include <stddef.h>
|
|
63
|
+
#include <string.h>
|
|
64
|
+
#include <sys/prctl.h>
|
|
65
|
+
#include <sys/syscall.h>
|
|
66
|
+
#include <linux/audit.h>
|
|
67
|
+
#include <linux/capability.h>
|
|
68
|
+
#include <linux/filter.h>
|
|
69
|
+
#include <linux/seccomp.h>
|
|
70
|
+
|
|
71
|
+
/* The capability bits of src/capabilities.ts this file reads. */
|
|
72
|
+
#define CAP_BIT_FS_READ (1 << 5)
|
|
73
|
+
#define CAP_BIT_FS_WRITE (1 << 6)
|
|
74
|
+
#define CAP_BIT_NET (1 << 7)
|
|
75
|
+
#define CAP_BIT_SPAWN (1 << 8)
|
|
76
|
+
|
|
77
|
+
/* Landlock's numbers, spelled here rather than taken from <linux/landlock.h>,
|
|
78
|
+
* which an older toolchain lacks and whose newest rights it lacks too. */
|
|
79
|
+
#define LL_CREATE 444
|
|
80
|
+
#define LL_ADD_RULE 445
|
|
81
|
+
#define LL_RESTRICT 446
|
|
82
|
+
#define LL_CREATE_RULESET_VERSION 1u
|
|
83
|
+
#define LL_RULE_PATH_BENEATH 1
|
|
84
|
+
#define LL_EXECUTE (1ull << 0)
|
|
85
|
+
#define LL_WRITE_FILE (1ull << 1)
|
|
86
|
+
#define LL_READ_FILE (1ull << 2)
|
|
87
|
+
#define LL_READ_DIR (1ull << 3)
|
|
88
|
+
#define LL_REFER (1ull << 13)
|
|
89
|
+
#define LL_TRUNCATE (1ull << 14)
|
|
90
|
+
#define LL_IOCTL_DEV (1ull << 15)
|
|
91
|
+
/* REMOVE_DIR through MAKE_SYM, bits 4 to 12: every right that makes or removes
|
|
92
|
+
* a name. */
|
|
93
|
+
#define LL_MAKE_REMOVE (((1ull << 13) - 1) & ~(LL_EXECUTE | LL_WRITE_FILE | LL_READ_FILE | LL_READ_DIR))
|
|
94
|
+
#define LL_ABI1 ((1ull << 13) - 1)
|
|
95
|
+
#define LL_SCOPE_ALL 3ull /* ABSTRACT_UNIX_SOCKET | SIGNAL, ABI 6 */
|
|
96
|
+
|
|
97
|
+
struct nish_ll_ruleset {
|
|
98
|
+
uint64_t handled_access_fs;
|
|
99
|
+
uint64_t handled_access_net;
|
|
100
|
+
uint64_t scoped;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
struct nish_ll_path_beneath {
|
|
104
|
+
uint64_t allowed_access;
|
|
105
|
+
int32_t parent_fd;
|
|
106
|
+
} __attribute__((packed));
|
|
107
|
+
|
|
108
|
+
#ifndef SECCOMP_RET_KILL_PROCESS
|
|
109
|
+
#define SECCOMP_RET_KILL_PROCESS 0x80000000u
|
|
110
|
+
#endif
|
|
111
|
+
#ifndef SECCOMP_GET_ACTION_AVAIL
|
|
112
|
+
#define SECCOMP_GET_ACTION_AVAIL 2
|
|
113
|
+
#endif
|
|
114
|
+
#ifndef SECCOMP_FILTER_FLAG_TSYNC
|
|
115
|
+
#define SECCOMP_FILTER_FLAG_TSYNC 1
|
|
116
|
+
#endif
|
|
117
|
+
#ifndef SYS_seccomp
|
|
118
|
+
#if defined(__x86_64__)
|
|
119
|
+
#define SYS_seccomp 317
|
|
120
|
+
#else
|
|
121
|
+
#define SYS_seccomp 277
|
|
122
|
+
#endif
|
|
123
|
+
#endif
|
|
124
|
+
#ifndef PR_CAP_AMBIENT
|
|
125
|
+
#define PR_CAP_AMBIENT 47
|
|
126
|
+
#define PR_CAP_AMBIENT_IS_SET 1
|
|
127
|
+
#define PR_CAP_AMBIENT_CLEAR_ALL 4
|
|
128
|
+
#endif
|
|
129
|
+
|
|
130
|
+
#define KILL SECCOMP_RET_KILL_PROCESS
|
|
131
|
+
#define CLONE_THREAD_FLAG 0x00010000u
|
|
132
|
+
#define EPERM_RET (SECCOMP_RET_ERRNO | EPERM)
|
|
133
|
+
#define ENOSYS_RET (SECCOMP_RET_ERRNO | ENOSYS)
|
|
134
|
+
|
|
135
|
+
/* The syscall numbers each row of the note's section 4 names, per
|
|
136
|
+
* architecture. A number the running kernel does not have matches nothing.
|
|
137
|
+
* AArch64 has no `fork`, `vfork`, `chmod`, `chown`, `lchown`, `utime`,
|
|
138
|
+
* `utimes` or `futimesat`: only the `*at` and `f` forms. */
|
|
139
|
+
#if defined(__x86_64__)
|
|
140
|
+
#define NISH_SB_ARCH AUDIT_ARCH_X86_64
|
|
141
|
+
static const uint16_t nish_sb_always[] = {101, 310, 311, 438}; /* ptrace, process_vm_readv/writev, pidfd_getfd */
|
|
142
|
+
static const uint16_t nish_sb_net[] = {41, 53, 42, 49, 50, 43, 288}; /* socket ... accept4 */
|
|
143
|
+
static const uint16_t nish_sb_spawn[] = {59, 322, 57, 58}; /* execve, execveat, fork, vfork */
|
|
144
|
+
#define NISH_SB_CLONE 56
|
|
145
|
+
static const uint16_t nish_sb_meta[] = {
|
|
146
|
+
90, 91, 268, 452, /* chmod, fchmod, fchmodat, fchmodat2 */
|
|
147
|
+
92, 93, 94, 260, /* chown, fchown, lchown, fchownat */
|
|
148
|
+
132, 235, 261, 280, /* utime, utimes, futimesat, utimensat */
|
|
149
|
+
188, 189, 190, 463, /* setxattr, lsetxattr, fsetxattr, setxattrat */
|
|
150
|
+
197, 198, 199, 466, /* removexattr, lremovexattr, fremovexattr, removexattrat */
|
|
151
|
+
};
|
|
152
|
+
#elif defined(__aarch64__)
|
|
153
|
+
#define NISH_SB_ARCH AUDIT_ARCH_AARCH64
|
|
154
|
+
static const uint16_t nish_sb_always[] = {117, 270, 271, 438};
|
|
155
|
+
static const uint16_t nish_sb_net[] = {198, 199, 203, 200, 201, 202, 242};
|
|
156
|
+
static const uint16_t nish_sb_spawn[] = {221, 281};
|
|
157
|
+
#define NISH_SB_CLONE 220
|
|
158
|
+
static const uint16_t nish_sb_meta[] = {
|
|
159
|
+
52, 53, 452, /* fchmod, fchmodat, fchmodat2 */
|
|
160
|
+
55, 54, /* fchown, fchownat */
|
|
161
|
+
88, /* utimensat */
|
|
162
|
+
5, 6, 7, 463, /* setxattr, lsetxattr, fsetxattr, setxattrat */
|
|
163
|
+
14, 15, 16, 466, /* removexattr, lremovexattr, fremovexattr, removexattrat */
|
|
164
|
+
};
|
|
165
|
+
#endif
|
|
166
|
+
static const uint16_t nish_sb_uring[] = {425, 426, 427}; /* io_uring_setup, _enter, _register */
|
|
167
|
+
#define NISH_SB_CLONE3 435
|
|
168
|
+
|
|
169
|
+
/* Set by step 6 and read by `nish_sandbox_check`. */
|
|
170
|
+
static int nish_sb_applied = 0;
|
|
171
|
+
|
|
172
|
+
/* The errno names a failure prints; a number for any other. */
|
|
173
|
+
static const char *nish_sb_errno_name(int e, char *buf) {
|
|
174
|
+
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";
|
|
185
|
+
}
|
|
186
|
+
char digits[12];
|
|
187
|
+
int n = 0;
|
|
188
|
+
unsigned u = (unsigned)e;
|
|
189
|
+
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;
|
|
193
|
+
return buf;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/* One line on stderr from up to three pieces; a null piece is left out. */
|
|
197
|
+
static void nish_sb_say(const char *a, const char *b, const char *c) {
|
|
198
|
+
const char *parts[4] = {"nish: --sandbox: ", a, b, c};
|
|
199
|
+
for (int i = 0; i < 4; i++)
|
|
200
|
+
if (parts[i]) (void)!write(2, parts[i], strlen(parts[i]));
|
|
201
|
+
(void)!write(2, "\n", 1);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/* The one way out on a failure, in both modes; `path`, when not null, is
|
|
205
|
+
* named after `what`. */
|
|
206
|
+
static void nish_sb_fail_at(const char *what, const char *path) {
|
|
207
|
+
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);
|
|
214
|
+
_exit(126);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
static void nish_sb_fail(const char *what) { nish_sb_fail_at(what, 0); }
|
|
218
|
+
|
|
219
|
+
/* Step 3. `capset` with every set empty needs no privilege; the bounding set
|
|
220
|
+
* needs CAP_SETPCAP to drop, and need not be dropped without it, because with
|
|
221
|
+
* NO_NEW_PRIVS and an empty permitted set nothing it allows can be gained.
|
|
222
|
+
* Each set is read back rather than trusted, since a call that answered 0
|
|
223
|
+
* and left a capability would otherwise run the program privileged. */
|
|
224
|
+
static void nish_sb_drop_capabilities(void) {
|
|
225
|
+
struct __user_cap_header_struct head = {_LINUX_CAPABILITY_VERSION_3, 0};
|
|
226
|
+
struct __user_cap_data_struct data[2];
|
|
227
|
+
if (syscall(SYS_capget, &head, data) != 0) nish_sb_fail("cannot read the capability sets");
|
|
228
|
+
int setpcap = (data[0].effective >> CAP_SETPCAP) & 1;
|
|
229
|
+
if (setpcap)
|
|
230
|
+
for (int cap = 0; cap < 64; cap++)
|
|
231
|
+
if (prctl(PR_CAPBSET_DROP, cap, 0, 0, 0) != 0 && errno != EINVAL) nish_sb_fail("cannot empty the bounding set");
|
|
232
|
+
/* EINVAL is a kernel older than 4.3, which has no ambient set to clear. */
|
|
233
|
+
if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0) != 0 && errno != EINVAL)
|
|
234
|
+
nish_sb_fail("cannot clear the ambient set");
|
|
235
|
+
memset(data, 0, sizeof data);
|
|
236
|
+
if (syscall(SYS_capset, &head, data) != 0) nish_sb_fail("cannot drop the capabilities");
|
|
237
|
+
if (syscall(SYS_capget, &head, data) != 0) nish_sb_fail("cannot read the capability sets");
|
|
238
|
+
errno = EPERM;
|
|
239
|
+
for (int i = 0; i < 2; i++)
|
|
240
|
+
if (data[i].effective | data[i].permitted | data[i].inheritable) nish_sb_fail("a capability survived the drop");
|
|
241
|
+
for (int cap = 0; cap < 64; cap++) {
|
|
242
|
+
if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_IS_SET, cap, 0, 0) == 1) nish_sb_fail("an ambient capability survived the drop");
|
|
243
|
+
if (setpcap && prctl(PR_CAPBSET_READ, cap, 0, 0, 0) == 1) nish_sb_fail("a bounding capability survived the drop");
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/* The Landlock ABI the kernel answers, or -1 with errno set. */
|
|
248
|
+
static int nish_sb_landlock_abi(void) {
|
|
249
|
+
#ifdef NISH_SANDBOX_SIMULATE_ABI
|
|
250
|
+
if (NISH_SANDBOX_SIMULATE_ABI == 0) {
|
|
251
|
+
errno = ENOSYS;
|
|
252
|
+
return -1;
|
|
253
|
+
}
|
|
254
|
+
return NISH_SANDBOX_SIMULATE_ABI;
|
|
255
|
+
#else
|
|
256
|
+
return (int)syscall(LL_CREATE, NULL, 0, LL_CREATE_RULESET_VERSION);
|
|
257
|
+
#endif
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/* The rights the mask grants, whatever the ABI knows. `process.spawn` grants
|
|
261
|
+
* EXECUTE and READ_FILE beneath `/` whatever `fs.read` says, since a child
|
|
262
|
+
* must read and execute its binary and libraries wherever PATH finds them,
|
|
263
|
+
* and IOCTL_DEV, since a device's ioctl can act like a spawn (TIOCSTI). */
|
|
264
|
+
static uint64_t nish_sb_granted(int32_t caps) {
|
|
265
|
+
uint64_t rights = 0;
|
|
266
|
+
if (caps & CAP_BIT_FS_READ) rights |= LL_READ_FILE | LL_READ_DIR;
|
|
267
|
+
if (caps & CAP_BIT_FS_WRITE) rights |= LL_WRITE_FILE | LL_MAKE_REMOVE | LL_REFER | LL_TRUNCATE;
|
|
268
|
+
if (caps & CAP_BIT_SPAWN) rights |= LL_EXECUTE | LL_READ_FILE | LL_IOCTL_DEV;
|
|
269
|
+
return rights;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/* Appends to the best-effort line the name of each right the ABI could not
|
|
273
|
+
* withhold. REFER is never named: below ABI 2 the kernel refuses every
|
|
274
|
+
* rename or link across directories, which is stricter, not weaker. */
|
|
275
|
+
static size_t nish_sb_name(char *line, size_t at, const char *name) {
|
|
276
|
+
size_t len = strlen(name);
|
|
277
|
+
if (at) memcpy(line + at, ", ", 2), at += 2;
|
|
278
|
+
memcpy(line + at, name, len);
|
|
279
|
+
return at + len;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/* One path_beneath rule granting `rights` beneath `path`. A rule granting
|
|
283
|
+
* nothing is refused (ENOMSG), and nothing is what it would add, so it is
|
|
284
|
+
* left out. A scope directory gone since the build refuses strict; under
|
|
285
|
+
* best-effort it is named and its rule left out, which withholds the right
|
|
286
|
+
* 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) {
|
|
288
|
+
struct nish_ll_path_beneath rule = {rights, -1};
|
|
289
|
+
if (!rights) return;
|
|
290
|
+
rule.parent_fd = open(path, O_PATH | O_CLOEXEC | O_DIRECTORY);
|
|
291
|
+
if (rule.parent_fd < 0) {
|
|
292
|
+
if (mode != 2) nish_sb_fail_at("cannot open the directory ", path);
|
|
293
|
+
nish_sb_say("the scope directory ", path, " cannot be opened, so the program gets no access beneath it");
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (syscall(LL_ADD_RULE, ruleset, LL_RULE_PATH_BENEATH, &rule, 0) != 0) nish_sb_fail("cannot add the Landlock rule");
|
|
297
|
+
close(rule.parent_fd);
|
|
298
|
+
}
|
|
299
|
+
|
|
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
|
+
}
|
|
304
|
+
|
|
305
|
+
/* Step 4. Every right the ABI knows is handled, and one path_beneath rule on
|
|
306
|
+
* `/` grants back what the mask grants whole, so an unscoped right is withheld
|
|
307
|
+
* everywhere or nowhere; a scoped capability's rights are granted beneath its
|
|
308
|
+
* directories alone. Returns with nothing done when there is no Landlock and
|
|
309
|
+
* 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) {
|
|
311
|
+
int abi = nish_sb_landlock_abi();
|
|
312
|
+
if (abi < 0) {
|
|
313
|
+
/* EPERM is a container runtime's own seccomp profile refusing the call;
|
|
314
|
+
* EOPNOTSUPP is Landlock built but not in the lsm= list. */
|
|
315
|
+
if (errno != ENOSYS && errno != EOPNOTSUPP && errno != EPERM) nish_sb_fail("cannot read the Landlock ABI");
|
|
316
|
+
if (mode != 2) {
|
|
317
|
+
nish_sb_say("this kernel has no Landlock; build with --sandbox=best-effort to run with seccomp alone", 0, 0);
|
|
318
|
+
_exit(126);
|
|
319
|
+
}
|
|
320
|
+
nish_sb_say("no Landlock, files are not confined", 0, 0);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
uint64_t known = LL_ABI1;
|
|
324
|
+
if (abi >= 2) known |= LL_REFER;
|
|
325
|
+
if (abi >= 3) known |= LL_TRUNCATE;
|
|
326
|
+
if (abi >= 5) known |= LL_IOCTL_DEV;
|
|
327
|
+
/* A scoped capability grants nothing beneath `/`, so a scoped fs.write
|
|
328
|
+
* below ABI 3 loses TRUNCATE everywhere and refuses strict like any other
|
|
329
|
+
* build that cannot withhold it. */
|
|
330
|
+
int32_t whole = caps;
|
|
331
|
+
if (read_dirs) whole &= ~CAP_BIT_FS_READ;
|
|
332
|
+
if (write_dirs) whole &= ~CAP_BIT_FS_WRITE;
|
|
333
|
+
uint64_t granted = nish_sb_granted(whole);
|
|
334
|
+
uint64_t lost = ~granted & ~known & (LL_TRUNCATE | LL_IOCTL_DEV);
|
|
335
|
+
int unscoped = abi < 6;
|
|
336
|
+
/* Strict runs without IOCTL_DEV and without the ABI 6 scopes: a terminal
|
|
337
|
+
* already on fd 0 gives the first on every ABI, and the second is no Nish
|
|
338
|
+
* builtin's (open questions 8 and 12). Only TRUNCATE refuses it. */
|
|
339
|
+
if (mode != 2 && (lost & LL_TRUNCATE)) {
|
|
340
|
+
nish_sb_say("this kernel's Landlock cannot withhold TRUNCATE; build with --sandbox=best-effort to run without it", 0, 0);
|
|
341
|
+
_exit(126);
|
|
342
|
+
}
|
|
343
|
+
if (mode == 2 && (lost || unscoped)) {
|
|
344
|
+
char line[80];
|
|
345
|
+
size_t at = 0;
|
|
346
|
+
if (lost & LL_TRUNCATE) at = nish_sb_name(line, at, "TRUNCATE");
|
|
347
|
+
if (lost & LL_IOCTL_DEV) at = nish_sb_name(line, at, "IOCTL_DEV");
|
|
348
|
+
if (unscoped) at = nish_sb_name(line, at, "the signal and abstract socket scopes");
|
|
349
|
+
line[at] = 0;
|
|
350
|
+
nish_sb_say("this kernel's Landlock cannot withhold ", line, ", so they are not confined");
|
|
351
|
+
}
|
|
352
|
+
struct nish_ll_ruleset attr = {known, 0, abi >= 6 ? LL_SCOPE_ALL : 0};
|
|
353
|
+
int ruleset = (int)syscall(LL_CREATE, &attr, sizeof attr, 0);
|
|
354
|
+
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);
|
|
358
|
+
if (syscall(LL_RESTRICT, ruleset, 0) != 0) nish_sb_fail("cannot apply the Landlock ruleset");
|
|
359
|
+
close(ruleset);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/* Appends `JEQ nr; RET action` for each number to the program at `at`. */
|
|
363
|
+
static int nish_sb_rows(struct sock_filter *prog, int at, const uint16_t *nrs, int count, uint32_t action) {
|
|
364
|
+
for (int i = 0; i < count; i++) {
|
|
365
|
+
prog[at++] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, nrs[i], 0, 1);
|
|
366
|
+
prog[at++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, action);
|
|
367
|
+
}
|
|
368
|
+
return at;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
#define NISH_SB_ROWS(prog, at, nrs, action) nish_sb_rows(prog, at, nrs, (int)(sizeof nrs / sizeof nrs[0]), action)
|
|
372
|
+
|
|
373
|
+
/* The low word of `clone`'s flags, where CLONE_THREAD lives. */
|
|
374
|
+
#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
|
|
375
|
+
#define NISH_SB_ARG0_LOW (offsetof(struct seccomp_data, args) + 4)
|
|
376
|
+
#else
|
|
377
|
+
#define NISH_SB_ARG0_LOW offsetof(struct seccomp_data, args)
|
|
378
|
+
#endif
|
|
379
|
+
|
|
380
|
+
/* Step 5: a denylist (open question 4), each row of the note's section 4.
|
|
381
|
+
* The arch check comes first and kills on a mismatch, so a 32-bit entry
|
|
382
|
+
* cannot step around the 64-bit numbers; on x86-64 the next instruction
|
|
383
|
+
* kills any number with bit 30 set, since an x32 call reports
|
|
384
|
+
* AUDIT_ARCH_X86_64 and would match none of them. */
|
|
385
|
+
static void nish_sb_seccomp(int32_t caps) {
|
|
386
|
+
uint32_t action = SECCOMP_RET_KILL_PROCESS;
|
|
387
|
+
if (syscall(SYS_seccomp, SECCOMP_GET_ACTION_AVAIL, 0, &action) != 0)
|
|
388
|
+
nish_sb_fail("this kernel has no seccomp filter that kills the process");
|
|
389
|
+
struct sock_filter prog[96];
|
|
390
|
+
int at = 0;
|
|
391
|
+
prog[at++] = (struct sock_filter)BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, arch));
|
|
392
|
+
prog[at++] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, NISH_SB_ARCH, 1, 0);
|
|
393
|
+
prog[at++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, KILL);
|
|
394
|
+
prog[at++] = (struct sock_filter)BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr));
|
|
395
|
+
#if defined(__x86_64__)
|
|
396
|
+
prog[at++] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JSET | BPF_K, 0x40000000, 0, 1);
|
|
397
|
+
prog[at++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, KILL);
|
|
398
|
+
#endif
|
|
399
|
+
at = NISH_SB_ROWS(prog, at, nish_sb_always, KILL);
|
|
400
|
+
if (!(caps & CAP_BIT_NET)) at = NISH_SB_ROWS(prog, at, nish_sb_net, KILL);
|
|
401
|
+
/* io_uring opens, connects and accepts sockets without those calls. */
|
|
402
|
+
if ((caps & (CAP_BIT_NET | CAP_BIT_SPAWN)) != (CAP_BIT_NET | CAP_BIT_SPAWN))
|
|
403
|
+
at = NISH_SB_ROWS(prog, at, nish_sb_uring, KILL);
|
|
404
|
+
if (!(caps & CAP_BIT_FS_WRITE)) at = NISH_SB_ROWS(prog, at, nish_sb_meta, EPERM_RET);
|
|
405
|
+
if (!(caps & CAP_BIT_SPAWN)) {
|
|
406
|
+
at = NISH_SB_ROWS(prog, at, nish_sb_spawn, KILL);
|
|
407
|
+
/* clone3's flags are in a struct seccomp cannot read, so it answers
|
|
408
|
+
* ENOSYS and glibc falls back to clone, where they are visible. */
|
|
409
|
+
static const uint16_t clone3[] = {NISH_SB_CLONE3};
|
|
410
|
+
at = NISH_SB_ROWS(prog, at, clone3, ENOSYS_RET);
|
|
411
|
+
/* clone is a thread when CLONE_THREAD is set and a process otherwise.
|
|
412
|
+
* The accumulator is overwritten, so this is the last row. */
|
|
413
|
+
prog[at++] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, NISH_SB_CLONE, 0, 4);
|
|
414
|
+
prog[at++] = (struct sock_filter)BPF_STMT(BPF_LD | BPF_W | BPF_ABS, NISH_SB_ARG0_LOW);
|
|
415
|
+
prog[at++] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JSET | BPF_K, CLONE_THREAD_FLAG, 0, 1);
|
|
416
|
+
prog[at++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW);
|
|
417
|
+
prog[at++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, KILL);
|
|
418
|
+
}
|
|
419
|
+
prog[at++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW);
|
|
420
|
+
struct sock_fprog fprog = {(unsigned short)at, prog};
|
|
421
|
+
/* Under TSYNC a thread that cannot take the filter is answered with its id,
|
|
422
|
+
* not -1, and errno says nothing, so that failure names no errno. */
|
|
423
|
+
long loaded = syscall(SYS_seccomp, SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_TSYNC, &fprog);
|
|
424
|
+
if (loaded > 0) {
|
|
425
|
+
nish_sb_say("cannot load the seccomp filter: a thread could not take it", 0, 0);
|
|
426
|
+
_exit(126);
|
|
427
|
+
}
|
|
428
|
+
if (loaded != 0) nish_sb_fail("cannot load the seccomp filter");
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
void nish_sandbox(int32_t caps, int32_t mode, const char *read_dirs, const char *write_dirs) {
|
|
432
|
+
nish_cpu_prime();
|
|
433
|
+
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0) nish_sb_fail("cannot set no_new_privs");
|
|
434
|
+
nish_sb_drop_capabilities();
|
|
435
|
+
nish_sb_landlock(caps, mode, read_dirs, write_dirs);
|
|
436
|
+
/* Metadata writes stay EPERM inside a scoped write directory (open
|
|
437
|
+
* question 1): Landlock has no right for chmod, chown or xattrs. */
|
|
438
|
+
nish_sb_seccomp(write_dirs ? caps & ~CAP_BIT_FS_WRITE : caps);
|
|
439
|
+
__atomic_store_n(&nish_sb_applied, 1, __ATOMIC_RELEASE);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
void nish_sandbox_check(void) {
|
|
443
|
+
if (__atomic_load_n(&nish_sb_applied, __ATOMIC_ACQUIRE)) return;
|
|
444
|
+
nish_sb_say("the C library did not run .preinit_array, so the program was not confined", 0, 0);
|
|
445
|
+
_exit(126);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
#else
|
|
449
|
+
|
|
450
|
+
/* Every other platform: a target-neutral `.ll` built with `--sandbox` and
|
|
451
|
+
* linked by hand here refuses to run rather than running unconfined. Both
|
|
452
|
+
* symbols refuse, because a platform that never runs `.preinit_array` reaches
|
|
453
|
+
* only the check. */
|
|
454
|
+
static void nish_sb_none(void) {
|
|
455
|
+
static const char line[] = "nish: --sandbox: this platform has no sandbox\n";
|
|
456
|
+
(void)!write(2, line, sizeof line - 1);
|
|
457
|
+
_exit(126);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
void nish_sandbox(int32_t caps, int32_t mode, const char *read_dirs, const char *write_dirs) {
|
|
461
|
+
(void)caps;
|
|
462
|
+
(void)mode;
|
|
463
|
+
(void)read_dirs;
|
|
464
|
+
(void)write_dirs;
|
|
465
|
+
nish_sb_none();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
void nish_sandbox_check(void) { nish_sb_none(); }
|
|
469
|
+
|
|
470
|
+
#endif
|