@amritk/nish-x86_64-linux 0.15.0 → 0.17.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 +58 -40
- package/bin/nish +0 -0
- package/package.json +1 -1
- package/runtime/nish.d.ts +117 -8
- package/runtime/nish.h +67 -10
- package/runtime/nish.mjs +71 -7
- package/runtime/runtime-host.c +72 -26
- package/runtime/runtime-net.c +72 -19
- package/runtime/runtime-os.c +127 -45
- package/runtime/runtime-parallel.c +5 -1
- package/runtime/runtime-wasm.c +26 -2
- package/runtime/runtime.c +86 -14
- package/runtime/shim.mjs +114 -5
- package/scripts/build.sh +1 -3
- package/std/README.md +192 -36
- package/std/crypto/aes.ts +104 -21
- package/std/crypto/base64url.ts +43 -17
- package/std/crypto/chacha20poly1305.ts +52 -31
- package/std/crypto/ct.ts +14 -3
- package/std/crypto/hkdf.ts +385 -8
- package/std/crypto/hmac.ts +325 -9
- package/std/crypto/p256.ts +153 -27
- package/std/crypto/sha1.ts +147 -0
- package/std/crypto/sha256.ts +15 -3
- package/std/crypto/sha512.ts +16 -2
- package/std/crypto/x25519.ts +90 -37
- package/std/crypto/x509.ts +265 -65
- package/std/net/hpack.ts +1094 -0
- package/std/net/http-fields.ts +604 -0
- package/std/net/http1-server.ts +1389 -0
- package/std/net/http1.ts +1492 -0
- package/std/net/http2-frame.ts +745 -0
- package/std/net/http2-tls.ts +216 -0
- package/std/net/http2.ts +1433 -0
- package/std/net/http3-frame.ts +457 -0
- package/std/net/http3-server.ts +905 -0
- package/std/net/http3.ts +2145 -0
- package/std/net/qpack.ts +1163 -0
- package/std/net/quic-conn-ack.ts +206 -0
- package/std/net/quic-conn-cid.ts +425 -0
- package/std/net/quic-conn-params.ts +392 -0
- package/std/net/quic-datagram.ts +130 -0
- package/std/net/quic-frame.ts +1206 -0
- package/std/net/quic-listener.ts +653 -0
- package/std/net/quic-packet.ts +1474 -0
- package/std/net/quic-recovery.ts +849 -0
- package/std/net/quic-stream.ts +1603 -0
- package/std/net/quic.ts +3214 -0
- package/std/net/tls/codec.ts +1089 -0
- package/std/net/tls/record-server.ts +1173 -0
- package/std/net/tls/record.ts +621 -0
- package/std/net/tls/schedule.ts +429 -0
- package/std/net/tls-tcp.ts +410 -0
- package/std/net/tls.ts +1368 -0
- package/std/net/websocket.ts +823 -0
- package/std/net/webtransport.ts +1334 -0
- package/std/secret.ts +62 -0
package/runtime/runtime-os.c
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
the LEGACY symbol, for which a NULL second argument is undefined. */
|
|
35
35
|
#define _XOPEN_SOURCE 700
|
|
36
36
|
#include <dirent.h>
|
|
37
|
+
#include <errno.h>
|
|
37
38
|
#include <limits.h>
|
|
38
39
|
#include <fcntl.h>
|
|
39
40
|
#include <stdint.h>
|
|
@@ -52,6 +53,19 @@ extern char **environ;
|
|
|
52
53
|
|
|
53
54
|
#include "nish.h"
|
|
54
55
|
|
|
56
|
+
/* Darwin's <fcntl.h> declares `O_NOFOLLOW` only at its full C level, and the
|
|
57
|
+
strict level the two macros above select is the one that binds the right
|
|
58
|
+
`realpath`, so the flag is spelled here with Darwin's own value
|
|
59
|
+
(<sys/fcntl.h>: 0x00000100). Linux and WASI declare it at POSIX 2008. */
|
|
60
|
+
#if defined(__APPLE__) && !defined(O_NOFOLLOW)
|
|
61
|
+
#define O_NOFOLLOW 0x00000100
|
|
62
|
+
#endif
|
|
63
|
+
/* `O_CLOEXEC` is POSIX 2008 and declared at that level everywhere; the
|
|
64
|
+
fallback is for an older Darwin SDK, with its value from the same header. */
|
|
65
|
+
#if defined(__APPLE__) && !defined(O_CLOEXEC)
|
|
66
|
+
#define O_CLOEXEC 0x01000000
|
|
67
|
+
#endif
|
|
68
|
+
|
|
55
69
|
/* The same spelling runtime.c uses for its own cold paths: a function that
|
|
56
70
|
reports and exits is never on a path worth optimising for. */
|
|
57
71
|
#define NISH_COLD __attribute__((noreturn, cold, noinline))
|
|
@@ -63,6 +77,12 @@ static NISH_COLD void nish_io_fail(const char *what, const nish_str *path) {
|
|
|
63
77
|
_exit(1);
|
|
64
78
|
}
|
|
65
79
|
|
|
80
|
+
/* The longest string or array a read hands back: 2^31 - 1, the same limit
|
|
81
|
+
runtime.c's `NISH_ARRAY_MAX` puts on a push. Under --number-mode i32
|
|
82
|
+
`length` is an `i32`, so one byte more reads back negative and bounds-check
|
|
83
|
+
elimination trusts it (docs/security/runtime.md, RT-1). */
|
|
84
|
+
#define NISH_LENGTH_MAX 2147483647
|
|
85
|
+
|
|
66
86
|
/* `readFileSyncOrNull(path)`: null rather than a message, so a program can
|
|
67
87
|
turn a missing file into its own diagnostic and carry on.
|
|
68
88
|
*
|
|
@@ -82,20 +102,38 @@ static NISH_COLD void nish_io_fail(const char *what, const nish_str *path) {
|
|
|
82
102
|
* narrower guard would have made it `null` here and split the two compilers
|
|
83
103
|
* again for the entry a command line can name. `fstat` on the descriptor
|
|
84
104
|
* rather than `stat` on the path, so the answer is about the file that was
|
|
85
|
-
* opened and not about whatever the name means a moment later.
|
|
105
|
+
* opened and not about whatever the name means a moment later.
|
|
106
|
+
*
|
|
107
|
+
* The descriptor is `O_CLOEXEC` (docs/security/runtime.md, RT-12): a child
|
|
108
|
+
* that a `scope()` task spawns while another task reads would otherwise
|
|
109
|
+
* inherit it. A `pread` that a signal interrupts before it moves a byte is
|
|
110
|
+
* retried rather than taken for the end of the file (RT-13). A local disk
|
|
111
|
+
* never interrupts one, but FUSE and NFS can, and stopping there would hand
|
|
112
|
+
* back the bytes so far as the whole file. */
|
|
86
113
|
nish_str *nish_read_file_or_null(const nish_str *path) {
|
|
87
|
-
int fd = open(path
|
|
114
|
+
int fd = open(nish_cpath(path), O_RDONLY | O_CLOEXEC);
|
|
88
115
|
if (fd < 0) return 0;
|
|
89
116
|
struct stat st;
|
|
90
117
|
off_t len = fstat(fd, &st) == 0 && !S_ISDIR(st.st_mode) ? lseek(fd, 0, SEEK_END) : -1;
|
|
91
|
-
|
|
118
|
+
/* A file longer than a length can say is refused before anything is
|
|
119
|
+
allocated, as an unreadable one is: null here, null from
|
|
120
|
+
`readFileBytesSync`, and `cannot read` from `readFileSync`. The bytes read
|
|
121
|
+
below are never more than `len`, so a file that grows meanwhile cannot
|
|
122
|
+
pass the limit either. */
|
|
123
|
+
if (len < 0 || len > NISH_LENGTH_MAX) {
|
|
92
124
|
close(fd);
|
|
93
125
|
return 0;
|
|
94
126
|
}
|
|
95
127
|
nish_str *s = nish_alloc_struct(8 + len + 1);
|
|
96
128
|
uint64_t got = 0;
|
|
97
129
|
ssize_t n;
|
|
98
|
-
while ((n = pread(fd, s->data + got, len - got, got))
|
|
130
|
+
while ((n = pread(fd, s->data + got, len - got, got)) != 0) {
|
|
131
|
+
if (n > 0) {
|
|
132
|
+
got += n;
|
|
133
|
+
} else if (errno != EINTR) {
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
99
137
|
close(fd);
|
|
100
138
|
s->len = got;
|
|
101
139
|
s->data[got] = 0;
|
|
@@ -123,13 +161,26 @@ nish_str *nish_read_file(const nish_str *path) {
|
|
|
123
161
|
return s;
|
|
124
162
|
}
|
|
125
163
|
|
|
164
|
+
/* `O_NOFOLLOW` (docs/security/runtime.md, RT-4): a symbolic link as the last
|
|
165
|
+
component of the path is refused, as an unwritable file is, rather than
|
|
166
|
+
followed to whatever file it names. `nish file.ts` writes `file.ll` beside
|
|
167
|
+
its source, so a link planted at that name in a checkout would otherwise
|
|
168
|
+
truncate any file the user can write. A link among the directories on the
|
|
169
|
+
way is still followed.
|
|
170
|
+
|
|
171
|
+
`O_CLOEXEC` for the reason the read has it (RT-12), and `EINTR` is retried
|
|
172
|
+
as the read retries it (RT-13): a write cut short is finished, and one that
|
|
173
|
+
fails is `cannot write`, never a silently short file. */
|
|
126
174
|
static void nish_put_file(const nish_str *path, const nish_str *data, int flags) {
|
|
127
|
-
int fd = open(path
|
|
175
|
+
int fd = open(nish_cpath(path), O_WRONLY | O_CREAT | O_NOFOLLOW | O_CLOEXEC | flags, 0644);
|
|
128
176
|
if (fd < 0) nish_io_fail("write ", path);
|
|
129
177
|
for (uint64_t done = 0; done < data->len;) {
|
|
130
178
|
ssize_t n = write(fd, data->data + done, data->len - done);
|
|
131
|
-
if (n
|
|
132
|
-
|
|
179
|
+
if (n > 0) {
|
|
180
|
+
done += n;
|
|
181
|
+
} else if (n == 0 || errno != EINTR) {
|
|
182
|
+
nish_io_fail("write ", path);
|
|
183
|
+
}
|
|
133
184
|
}
|
|
134
185
|
close(fd);
|
|
135
186
|
}
|
|
@@ -149,14 +200,14 @@ void nish_append_file(const nish_str *path, const nish_str *data) { nish_put_fil
|
|
|
149
200
|
answer to that question. */
|
|
150
201
|
_Bool nish_is_dir(const nish_str *path) {
|
|
151
202
|
struct stat st;
|
|
152
|
-
return stat(path
|
|
203
|
+
return stat(nish_cpath(path), &st) == 0 && S_ISDIR(st.st_mode);
|
|
153
204
|
}
|
|
154
205
|
|
|
155
206
|
/* One directory, not recursive. The retry is the `stat` above rather than
|
|
156
207
|
`errno == EEXIST` so that a plain file at the path answers false, which is
|
|
157
208
|
what the promise "a directory is there afterwards" means. */
|
|
158
209
|
_Bool nish_mkdir(const nish_str *path) {
|
|
159
|
-
return mkdir(path
|
|
210
|
+
return mkdir(nish_cpath(path), 0777) == 0 || nish_is_dir(path);
|
|
160
211
|
}
|
|
161
212
|
|
|
162
213
|
/* `posix_spawnp` is one libc call where fork/execvp/waitpid would be three,
|
|
@@ -168,7 +219,7 @@ _Bool nish_mkdir(const nish_str *path) {
|
|
|
168
219
|
two arguments answered differently, so the argv vector, the wait and the
|
|
169
220
|
signal convention are written once; it is `static`, so a program that spawns
|
|
170
221
|
nothing still loses all three to `--gc-sections`. */
|
|
171
|
-
static int32_t nish_spawn_impl(const nish_array *argv, const
|
|
222
|
+
static int32_t nish_spawn_impl(const nish_array *argv, const nish_str *out, const nish_str *err) {
|
|
172
223
|
#ifdef __wasi__
|
|
173
224
|
(void)argv;
|
|
174
225
|
(void)out;
|
|
@@ -181,23 +232,30 @@ static int32_t nish_spawn_impl(const nish_array *argv, const char *out, const ch
|
|
|
181
232
|
path out of here has anything to free. */
|
|
182
233
|
char **v = nish_alloc_struct((argv->len + 1) * sizeof *v);
|
|
183
234
|
uint64_t i = 0;
|
|
184
|
-
|
|
235
|
+
/* An argument holding a NUL is refused rather than cut short at it, which
|
|
236
|
+
would run a different command line from the one the program built: it is
|
|
237
|
+
the one whose C string is not its own bytes. */
|
|
238
|
+
for (; i < argv->len; i++) {
|
|
239
|
+
v[i] = (char *)nish_cpath(s[i]);
|
|
240
|
+
if (v[i] != s[i]->data) return -1;
|
|
241
|
+
}
|
|
185
242
|
v[i] = 0;
|
|
243
|
+
/* One list of file actions whether or not a stream is redirected: an empty
|
|
244
|
+
list is the same spawn as none, and one path through is less code than
|
|
245
|
+
creating the list only when a stream needs it. */
|
|
186
246
|
posix_spawn_file_actions_t fa;
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
fap = &fa;
|
|
247
|
+
if (posix_spawn_file_actions_init(&fa)) return -1;
|
|
248
|
+
const nish_str *to[2] = { out, err };
|
|
249
|
+
for (int k = 0; k < 2; k++) {
|
|
191
250
|
/* The child opens the file, not this process: a redirect that this process
|
|
192
251
|
performed would have to be undone afterwards, and a failed open would
|
|
193
252
|
leave its own stdout pointing at the file. */
|
|
194
|
-
if (
|
|
195
|
-
if (err) posix_spawn_file_actions_addopen(fap, 2, err, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
|
253
|
+
if (to[k]) posix_spawn_file_actions_addopen(&fa, k + 1, nish_cpath(to[k]), O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, 0644);
|
|
196
254
|
}
|
|
197
255
|
pid_t pid;
|
|
198
256
|
int status;
|
|
199
|
-
int failed = posix_spawnp(&pid, v[0],
|
|
200
|
-
|
|
257
|
+
int failed = posix_spawnp(&pid, v[0], &fa, 0, v, environ);
|
|
258
|
+
posix_spawn_file_actions_destroy(&fa);
|
|
201
259
|
if (failed) return -1;
|
|
202
260
|
if (waitpid(pid, &status, 0) < 0) return -1;
|
|
203
261
|
return WIFSIGNALED(status) ? 128 + WTERMSIG(status) : WEXITSTATUS(status);
|
|
@@ -210,14 +268,15 @@ int32_t nish_spawn(const nish_array *argv) { return nish_spawn_impl(argv, 0, 0);
|
|
|
210
268
|
to a file, which is what comparing a program's output against a golden needs
|
|
211
269
|
— `nish_spawn` answers a status and the output is gone. An **empty** path
|
|
212
270
|
leaves that stream inherited, so one call can capture stdout and let stderr
|
|
213
|
-
through to the terminal. Each file is created or truncated at 0644
|
|
214
|
-
`writeFileSync` does
|
|
271
|
+
through to the terminal. Each file is created or truncated at 0644 and a
|
|
272
|
+
symbolic link at its name is refused, as `writeFileSync` does; the child
|
|
273
|
+
then fails to start, and the answer is -1.
|
|
215
274
|
|
|
216
275
|
Two streams must not name one path: each would be opened separately, with
|
|
217
276
|
its own offset, and the two would overwrite each other rather than
|
|
218
277
|
interleave. Capturing them apart and concatenating is the way to merge. */
|
|
219
278
|
int32_t nish_spawn_to(const nish_array *argv, const nish_str *out, const nish_str *err) {
|
|
220
|
-
return nish_spawn_impl(argv, out->len ? out
|
|
279
|
+
return nish_spawn_impl(argv, out->len ? out : 0, err->len ? err : 0);
|
|
221
280
|
}
|
|
222
281
|
|
|
223
282
|
/* `readdirSync(path)`: the listing a driver needs to discover its own inputs.
|
|
@@ -240,7 +299,7 @@ nish_array *nish_readdir(const nish_str *path) {
|
|
|
240
299
|
(void)path;
|
|
241
300
|
return 0;
|
|
242
301
|
#else
|
|
243
|
-
DIR *d = opendir(path
|
|
302
|
+
DIR *d = opendir(nish_cpath(path));
|
|
244
303
|
if (!d) return 0;
|
|
245
304
|
nish_array *a = nish_alloc_struct(sizeof *a);
|
|
246
305
|
*a = (nish_array){ 0, 0, 0 };
|
|
@@ -252,15 +311,30 @@ nish_array *nish_readdir(const nish_str *path) {
|
|
|
252
311
|
((nish_str **)a->data)[a->len++] = nish_str_new(n, strlen(n));
|
|
253
312
|
}
|
|
254
313
|
closedir(d);
|
|
255
|
-
/*
|
|
256
|
-
a
|
|
314
|
+
/* Heapsort over pointers, in place. It was an insertion sort, on the theory
|
|
315
|
+
that a directory is short, but whoever fills the directory decides that:
|
|
316
|
+
100,000 names took 2.5 billion compares and 12.5 s, where this takes 3.0
|
|
317
|
+
million and 0.07 s (docs/security/runtime.md, RT-5). It needs no scratch
|
|
318
|
+
memory, and unlike `qsort` costs no comparator symbol or unwind entry. */
|
|
257
319
|
nish_str **v = (nish_str **)a->data;
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
320
|
+
/* Each pass sifts one name down the max-heap `v[0, end)`: first every
|
|
321
|
+
parent from the middle up, which builds the heap, then the name the
|
|
322
|
+
largest is swapped out for, which shrinks the heap by one. */
|
|
323
|
+
uint64_t i = a->len / 2, end = a->len;
|
|
324
|
+
while (end > 1) {
|
|
325
|
+
nish_str *s;
|
|
326
|
+
if (i) {
|
|
327
|
+
s = v[--i];
|
|
328
|
+
} else {
|
|
329
|
+
s = v[--end];
|
|
330
|
+
v[end] = v[0];
|
|
331
|
+
}
|
|
332
|
+
uint64_t j = i, c;
|
|
333
|
+
while ((c = 2 * j + 1) < end) {
|
|
334
|
+
if (c + 1 < end && strcmp(v[c]->data, v[c + 1]->data) < 0) c++;
|
|
335
|
+
if (strcmp(s->data, v[c]->data) >= 0) break;
|
|
336
|
+
v[j] = v[c];
|
|
337
|
+
j = c;
|
|
264
338
|
}
|
|
265
339
|
v[j] = s;
|
|
266
340
|
}
|
|
@@ -279,6 +353,10 @@ int64_t nish_monotonic_nanos(void) {
|
|
|
279
353
|
return (int64_t)ts.tv_sec * 1000000000 + ts.tv_nsec;
|
|
280
354
|
}
|
|
281
355
|
|
|
356
|
+
/* A C string copied into the arena, or NULL for NULL: the shape of every
|
|
357
|
+
answer libc hands back by pointer, `getenv`'s here and `realpath`'s below. */
|
|
358
|
+
static nish_str *nish_str_or_null(const char *v) { return v ? nish_str_new(v, strlen(v)) : 0; }
|
|
359
|
+
|
|
282
360
|
/* ---- The environment (WP19 R1): `getenv(name)`, the one environment read the
|
|
283
361
|
language has. A driver needs it to honour `CC` the way `scripts/build.sh`
|
|
284
362
|
does before it spawns that script, which is the whole reason it exists.
|
|
@@ -290,13 +368,7 @@ int64_t nish_monotonic_nanos(void) {
|
|
|
290
368
|
string with the lifetime every other one has. NULL for an unset variable,
|
|
291
369
|
which is the language's `string | null`. Contract: nish.h. */
|
|
292
370
|
nish_str *nish_getenv(const nish_str *name) {
|
|
293
|
-
|
|
294
|
-
if (!v) return 0;
|
|
295
|
-
uint64_t len = strlen(v);
|
|
296
|
-
nish_str *s = nish_alloc_struct(8 + len + 1);
|
|
297
|
-
s->len = len;
|
|
298
|
-
memcpy(s->data, v, len + 1);
|
|
299
|
-
return s;
|
|
371
|
+
return nish_str_or_null(getenv(nish_cpath(name)));
|
|
300
372
|
}
|
|
301
373
|
|
|
302
374
|
/* ---- Symlinks (WP19 §5a item 4): `realpathSync(path)`, the one path
|
|
@@ -318,15 +390,25 @@ nish_str *nish_getenv(const nish_str *name) {
|
|
|
318
390
|
and the stack buffer is gone at the return. NULL when the path does not
|
|
319
391
|
resolve -- a path that does not exist included -- which is the language's
|
|
320
392
|
`string | null` rather than an error, because the caller is asking whether
|
|
321
|
-
it resolves. Contract: nish.h.
|
|
393
|
+
it resolves. Contract: nish.h.
|
|
394
|
+
|
|
395
|
+
A wasi-libc from before 2024, which is what Ubuntu and Debian package,
|
|
396
|
+
neither declares nor defines `realpath`, so there every path is one that
|
|
397
|
+
does not resolve and the answer is NULL. WebAssembly/wasi-libc#463 declared
|
|
398
|
+
it and #473 defined it a month later, with no wasi-sdk release between, and
|
|
399
|
+
#463 also gave `<dirent.h>` its `DT_SOCK`, which the old headers spell only
|
|
400
|
+
for upstream musl. So a missing `DT_SOCK` is the test: it names the libc
|
|
401
|
+
without the function, and wasi-sdk 22 and later keep the real `realpath`.
|
|
402
|
+
It is a preprocessor test rather than a declaration of our own because the
|
|
403
|
+
old libc.a has no definition to link. */
|
|
322
404
|
nish_str *nish_realpath(const nish_str *path) {
|
|
405
|
+
#if defined(__wasi__) && !defined(DT_SOCK)
|
|
406
|
+
(void)path;
|
|
407
|
+
return 0;
|
|
408
|
+
#else
|
|
323
409
|
char buf[PATH_MAX];
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
nish_str *s = nish_alloc_struct(8 + len + 1);
|
|
327
|
-
s->len = len;
|
|
328
|
-
memcpy(s->data, buf, len + 1);
|
|
329
|
-
return s;
|
|
410
|
+
return nish_str_or_null(realpath(nish_cpath(path), buf));
|
|
411
|
+
#endif
|
|
330
412
|
}
|
|
331
413
|
|
|
332
414
|
/* ---- What machine this is (WP14 §7a): `process.platform` and `process.arch`,
|
|
@@ -109,7 +109,11 @@ void nish_parallel_range(nish_par_body body, void *ctx, int64_t len, int64_t gra
|
|
|
109
109
|
if (grain < 1) grain = 1;
|
|
110
110
|
/* Never more threads than there is work for at the requested granularity,
|
|
111
111
|
* and never more than the machine has. */
|
|
112
|
-
|
|
112
|
+
/* Rounded up without `len + grain - 1`, which overflows -- undefined
|
|
113
|
+
behaviour, and in practice a negative count that ran the whole range on
|
|
114
|
+
one thread -- once `len` is within `grain` of 2^63
|
|
115
|
+
(docs/security/runtime.md, RT-8). */
|
|
116
|
+
int64_t chunks = len / grain + (len % grain != 0);
|
|
113
117
|
int64_t n = nish_cpu_count();
|
|
114
118
|
if (n > NISH_PAR_MAX) n = NISH_PAR_MAX;
|
|
115
119
|
if (chunks < n) n = chunks;
|
package/runtime/runtime-wasm.c
CHANGED
|
@@ -40,7 +40,14 @@ _Static_assert(offsetof(struct nish_arena, chunks) == 24, "arena layout is ABI")
|
|
|
40
40
|
extern unsigned char __heap_base;
|
|
41
41
|
#define NISH_PAGE ((uint64_t)65536)
|
|
42
42
|
|
|
43
|
-
/* Slow path of the inlined allocator (`size` is already 8-byte rounded).
|
|
43
|
+
/* Slow path of the inlined allocator (`size` is already 8-byte rounded).
|
|
44
|
+
*
|
|
45
|
+
* `need` has to fit the 32-bit address space, and is checked before anything
|
|
46
|
+
* is grown (docs/security/runtime.md, RT-7). Without that, an `off + size` that
|
|
47
|
+
* wrapped 2^64 looked small and moved the offset backwards, and a page count
|
|
48
|
+
* past 2^32 truncated to the `uintptr_t` `memory.grow` takes: a request of
|
|
49
|
+
* 2^48 bytes grew nothing, claimed 2^48 bytes of capacity, and the next block
|
|
50
|
+
* landed at `buf + off` reduced mod 2^32, on top of live data. */
|
|
44
51
|
void *nish_arena_grow(uint64_t size) {
|
|
45
52
|
if (!nish_arena.buf) {
|
|
46
53
|
uintptr_t base = ((uintptr_t)&__heap_base + 7) & ~(uintptr_t)7;
|
|
@@ -48,6 +55,7 @@ void *nish_arena_grow(uint64_t size) {
|
|
|
48
55
|
nish_arena.cap = (uint64_t)__builtin_wasm_memory_size(0) * NISH_PAGE - base;
|
|
49
56
|
}
|
|
50
57
|
uint64_t need = nish_arena.off + size;
|
|
58
|
+
if (need < size || need > (uintptr_t)-1 - (uintptr_t)nish_arena.buf) __builtin_trap();
|
|
51
59
|
if (need > nish_arena.cap) {
|
|
52
60
|
uint64_t pages = (need - nish_arena.cap + NISH_PAGE - 1) / NISH_PAGE;
|
|
53
61
|
if (__builtin_wasm_memory_grow(0, (uintptr_t)pages) == (uintptr_t)-1) __builtin_trap();
|
|
@@ -58,9 +66,12 @@ void *nish_arena_grow(uint64_t size) {
|
|
|
58
66
|
return p;
|
|
59
67
|
}
|
|
60
68
|
|
|
69
|
+
/* `size <= cap - off` rather than `off + size <= cap`, which a size near 2^64
|
|
70
|
+
wraps into a yes; a size that rounding wraps to 0 is trapped the same way. */
|
|
61
71
|
void *nish_alloc_struct(uint64_t size) {
|
|
72
|
+
if (size > (uint64_t)-8) __builtin_trap();
|
|
62
73
|
size = (size + 7) & ~(uint64_t)7;
|
|
63
|
-
if (nish_arena.buf &&
|
|
74
|
+
if (nish_arena.buf && size <= nish_arena.cap - nish_arena.off) {
|
|
64
75
|
void *p = nish_arena.buf + nish_arena.off;
|
|
65
76
|
nish_arena.off += size;
|
|
66
77
|
return p;
|
|
@@ -77,7 +88,15 @@ void nish_arena_release(uint64_t mark) { nish_arena.off = mark ? mark - (uintptr
|
|
|
77
88
|
/* ---- Arrays: %struct.nish_array = type { i64, i64, i8* } */
|
|
78
89
|
typedef struct nish_array { uint64_t len; uint64_t cap; char *data; } nish_array;
|
|
79
90
|
|
|
91
|
+
/* The length limit runtime.c's `nish_array_grow` and `nish_alloc_array` keep:
|
|
92
|
+
2^31 - 1 elements, so an i32-mode `length` is never negative. Past it the
|
|
93
|
+
wasm runtime traps, as it does for every other failure. */
|
|
94
|
+
#define NISH_ARRAY_MAX 2147483647u
|
|
95
|
+
|
|
80
96
|
nish_array *nish_alloc_array(uint64_t elem_size, uint64_t len) {
|
|
97
|
+
/* With `len` in range, `len * elem_size` cannot wrap for any element
|
|
98
|
+
smaller than 8 GiB, and a product past 4 GiB traps in the grow. */
|
|
99
|
+
if (len > NISH_ARRAY_MAX) __builtin_trap();
|
|
81
100
|
nish_array *a = (nish_array *)nish_alloc_struct(sizeof *a);
|
|
82
101
|
a->len = a->cap = len;
|
|
83
102
|
a->data = (char *)nish_alloc_struct(len * elem_size);
|
|
@@ -86,6 +105,10 @@ nish_array *nish_alloc_array(uint64_t elem_size, uint64_t len) {
|
|
|
86
105
|
|
|
87
106
|
void nish_array_grow(nish_array *a, uint64_t elem_size) {
|
|
88
107
|
uint64_t cap = a->cap ? a->cap * 2 : 4;
|
|
108
|
+
if (cap > NISH_ARRAY_MAX) {
|
|
109
|
+
if (a->len >= NISH_ARRAY_MAX) __builtin_trap();
|
|
110
|
+
cap = NISH_ARRAY_MAX;
|
|
111
|
+
}
|
|
89
112
|
char *data = (char *)nish_alloc_struct(cap * elem_size);
|
|
90
113
|
if (a->len) __builtin_memcpy(data, a->data, (uintptr_t)(a->len * elem_size)); /* memory.copy (-mbulk-memory) */
|
|
91
114
|
a->data = data;
|
|
@@ -96,6 +119,7 @@ void nish_array_grow(nish_array *a, uint64_t elem_size) {
|
|
|
96
119
|
void nish_panic_index(uint64_t idx, uint64_t len) { (void)idx; (void)len; __builtin_trap(); }
|
|
97
120
|
void nish_panic_slice(int64_t s, int64_t e, int64_t len) { (void)s; (void)e; (void)len; __builtin_trap(); }
|
|
98
121
|
void nish_panic_div(_Bool by_zero) { (void)by_zero; __builtin_trap(); }
|
|
122
|
+
void nish_panic_overflow(uint32_t op) { (void)op; __builtin_trap(); }
|
|
99
123
|
void nish_exit(int32_t code) { (void)code; __builtin_trap(); }
|
|
100
124
|
/* A panic's message (`panic`, `expect`, a failed range entry) has no stderr to
|
|
101
125
|
* go to here; the `nish_exit` that follows it is the trap. An exported function
|
package/runtime/runtime.c
CHANGED
|
@@ -79,11 +79,21 @@ _Static_assert(offsetof(struct nish_arena, off) == 8, "the inlined allocator bum
|
|
|
79
79
|
_Static_assert(offsetof(struct nish_arena, cap) == 16, "the inlined allocator reads field 2");
|
|
80
80
|
_Static_assert(offsetof(struct nish_arena, chunks) == 24, "arena layout is ABI");
|
|
81
81
|
|
|
82
|
+
/* `fputs` rather than one `write`: libc's own loop finishes a short write,
|
|
83
|
+
which a bare `write` would cut off (docs/security/runtime.md, RT-11), and
|
|
84
|
+
stderr is unbuffered, so the message leaves before `_exit`. A C host that
|
|
85
|
+
gives stderr a buffer of its own would lose it here. */
|
|
82
86
|
static NISH_COLD void nish_die(const char *msg) {
|
|
83
|
-
(
|
|
87
|
+
fputs(msg, stderr);
|
|
84
88
|
_exit(1);
|
|
85
89
|
}
|
|
86
|
-
|
|
90
|
+
/* A function rather than a macro around `nish_die`: seven call sites each
|
|
91
|
+
loading the message's address cost more than one more symbol does. */
|
|
92
|
+
static NISH_COLD void nish_oom(void) { nish_die("nish: out of memory\n"); }
|
|
93
|
+
|
|
94
|
+
/* The most elements an array and the most bytes a string may hold: see
|
|
95
|
+
`nish_array_grow` and `nish_str_concat`. */
|
|
96
|
+
#define NISH_ARRAY_MAX 2147483647u
|
|
87
97
|
|
|
88
98
|
/* Slow path (size 8-byte rounded): push a chunk (>= 64 KB), bump from it. */
|
|
89
99
|
void *nish_arena_grow(uint64_t size) {
|
|
@@ -196,12 +206,19 @@ nish_str *nish_str_new(const char *bytes, uint64_t len) {
|
|
|
196
206
|
return s;
|
|
197
207
|
}
|
|
198
208
|
|
|
209
|
+
/* A string never passes 2^31 - 1 bytes, for the reason an array never passes
|
|
210
|
+
that many elements (`NISH_ARRAY_MAX`, below): under --number-mode i32 a
|
|
211
|
+
longer one's `length` reads back negative and bounds-check elimination
|
|
212
|
+
trusts it (docs/security/runtime.md, RT-2). `a + b` past it fails as an
|
|
213
|
+
allocation does. */
|
|
199
214
|
nish_str *nish_str_concat(const nish_str *a, const nish_str *b) {
|
|
200
|
-
|
|
201
|
-
|
|
215
|
+
uint64_t len = a->len + b->len;
|
|
216
|
+
if (len > NISH_ARRAY_MAX) nish_oom();
|
|
217
|
+
nish_str *s = nish_alloc_struct(8 + len + 1);
|
|
218
|
+
s->len = len;
|
|
202
219
|
memcpy(s->data, a->data, a->len);
|
|
203
220
|
memcpy(s->data + a->len, b->data, b->len);
|
|
204
|
-
s->data[
|
|
221
|
+
s->data[len] = 0;
|
|
205
222
|
return s;
|
|
206
223
|
}
|
|
207
224
|
|
|
@@ -249,9 +266,22 @@ int64_t nish_str_index_of(const nish_str *s, const nish_str *sub) {
|
|
|
249
266
|
}
|
|
250
267
|
|
|
251
268
|
/* `console.log` / `console.error` / `write` / `writeError` and the message of
|
|
252
|
-
`panic`: fd 1 or 2, with or without the trailing newline.
|
|
269
|
+
`panic`: fd 1 or 2, with or without the trailing newline.
|
|
270
|
+
|
|
271
|
+
A pipe or a socket can take part of a long string and answer the count it
|
|
272
|
+
took: a signal that lands mid-write ends the call early, and a full pipe
|
|
273
|
+
does the same. So the bytes are written until all of them are out, or until
|
|
274
|
+
a write fails (docs/security/runtime.md, RT-11). A failure still ends the
|
|
275
|
+
output, `EAGAIN` on a non-blocking stdout included, since spinning on it
|
|
276
|
+
would burn a core. `EINTR` is not retried here: it needs a handler
|
|
277
|
+
installed without `SA_RESTART`, and the only one this runtime installs
|
|
278
|
+
(runtime-host.c) has it, so an interrupted write either restarts in the
|
|
279
|
+
kernel or comes back short. The newline needs no loop: one byte is never
|
|
280
|
+
written in part. */
|
|
253
281
|
void nish_write(const nish_str *s, int32_t fd, _Bool newline) {
|
|
254
|
-
|
|
282
|
+
uint64_t done = 0;
|
|
283
|
+
ssize_t k;
|
|
284
|
+
while (done < s->len && (k = write(fd, s->data + done, s->len - done)) > 0) done += k;
|
|
255
285
|
if (newline) (void)!write(fd, "\n", 1);
|
|
256
286
|
}
|
|
257
287
|
/* console.log(s) */
|
|
@@ -1106,8 +1136,12 @@ double nish_random(void) {
|
|
|
1106
1136
|
/* ---- Arrays: %struct.nish_array = type { i64, i64, i8* }, cold paths */
|
|
1107
1137
|
typedef struct nish_array { uint64_t len; uint64_t cap; char *data; } nish_array;
|
|
1108
1138
|
|
|
1109
|
-
/* Host entry (WP8): `len` uninitialised elements, len == cap
|
|
1139
|
+
/* Host entry (WP8): `len` uninitialised elements, len == cap. A `len` past
|
|
1140
|
+
`NISH_ARRAY_MAX` fails as an allocation does, which also keeps
|
|
1141
|
+
`len * elem_size` from wrapping into a short block behind a long header
|
|
1142
|
+
(docs/security/runtime.md, RT-7). */
|
|
1110
1143
|
nish_array *nish_alloc_array(uint64_t elem_size, uint64_t len) {
|
|
1144
|
+
if (len > NISH_ARRAY_MAX) nish_oom();
|
|
1111
1145
|
nish_array *a = nish_alloc_struct(sizeof *a);
|
|
1112
1146
|
*a = (nish_array){ len, len, nish_alloc_struct(len * elem_size) };
|
|
1113
1147
|
return a;
|
|
@@ -1160,15 +1194,46 @@ double nish_parse_number(const nish_str *s, int32_t mode) {
|
|
|
1160
1194
|
* `sizeof` one element, which for a record element type (WP15 section 2a) is
|
|
1161
1195
|
* the whole struct, so this relocates the elements themselves rather than a
|
|
1162
1196
|
* block of pointers to them. Compiled code may therefore hold no pointer into
|
|
1163
|
-
* `data` across a push, and the checker refuses the programs that would.
|
|
1164
|
-
|
|
1197
|
+
* `data` across a push, and the checker refuses the programs that would.
|
|
1198
|
+
*
|
|
1199
|
+
* A push never takes an array past 2^31 - 1 elements (docs/security/codegen.md,
|
|
1200
|
+
* K1-6). Under --number-mode i32 `length` is an `i32`, and one element more
|
|
1201
|
+
* read back as a negative or a short length everywhere, a hash or a tag
|
|
1202
|
+
* compare included. This file cannot tell which mode compiled its caller, so
|
|
1203
|
+
* the limit holds in both, and the push past it fails as an allocation does,
|
|
1204
|
+
* through `nish_oom`. `new Array(n)` checks its own `n` in the IR.
|
|
1205
|
+
*
|
|
1206
|
+
* `noinline`: under LTO the grown body was inlined into every push site, and
|
|
1207
|
+
* the extra compare moved register allocation in the hot loops around them
|
|
1208
|
+
* (+0.2% to +2% instructions over bench/instructions.json's map rows). Out of
|
|
1209
|
+
* line, every one of those rows runs fewer instructions than it did before, and
|
|
1210
|
+
* the call costs nothing on a path that runs once per doubling. */
|
|
1211
|
+
__attribute__((noinline)) void nish_array_grow(nish_array *a, uint64_t elem_size) {
|
|
1165
1212
|
uint64_t cap = a->cap ? a->cap * 2 : 4;
|
|
1213
|
+
if (cap > NISH_ARRAY_MAX) {
|
|
1214
|
+
if (a->len >= NISH_ARRAY_MAX) nish_oom();
|
|
1215
|
+
cap = NISH_ARRAY_MAX;
|
|
1216
|
+
}
|
|
1166
1217
|
char *data = nish_alloc_struct(cap * elem_size);
|
|
1167
1218
|
if (a->len) memcpy(data, a->data, a->len * elem_size);
|
|
1168
1219
|
a->data = data;
|
|
1169
1220
|
a->cap = cap;
|
|
1170
1221
|
}
|
|
1171
1222
|
|
|
1223
|
+
/* `secureZero(bytes)` (#385): every byte of the array set to zero, in stores
|
|
1224
|
+
* no optimisation may remove. A `memset` here would be a dead store wherever
|
|
1225
|
+
* the buffer's life ends straight after the call -- a key in a frame that
|
|
1226
|
+
* returns, a block that is freed -- and links use `-flto`, which inlines this
|
|
1227
|
+
* body into that caller and lets dead-store elimination see it. Each store goes
|
|
1228
|
+
* through a `volatile` pointer, so each is behaviour the C standard keeps
|
|
1229
|
+
* whatever the optimiser can see, and `noinline` keeps the call itself in the
|
|
1230
|
+
* binary for a reader of the disassembly. A byte at a time, because a secret
|
|
1231
|
+
* is a key or a scalar of tens of bytes, not a buffer worth a vector loop. */
|
|
1232
|
+
__attribute__((noinline)) void nish_wipe(nish_array *bytes) {
|
|
1233
|
+
volatile char *p = bytes->data;
|
|
1234
|
+
for (uint64_t i = 0; i < bytes->len; i++) p[i] = 0;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1172
1237
|
void nish_panic_index(uint64_t idx, uint64_t len) {
|
|
1173
1238
|
dprintf(2, "index out of range: %" PRIu64 " >= %" PRIu64 "\n", idx, len);
|
|
1174
1239
|
_exit(1);
|
|
@@ -1187,7 +1252,14 @@ void nish_panic_slice(int64_t start, int64_t end, int64_t len) {
|
|
|
1187
1252
|
_exit(1);
|
|
1188
1253
|
}
|
|
1189
1254
|
|
|
1190
|
-
/* ---- Checked
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1255
|
+
/* ---- Checked arithmetic (Rust semantics): the failed-check paths. One table
|
|
1256
|
+
for both panics, because each message is "attempt to <what>" and the two
|
|
1257
|
+
functions then share everything but the index. `nish_panic_overflow`'s `op`
|
|
1258
|
+
is the operator whose `llvm.s*.with.overflow` flagged: 0 add, 1 subtract,
|
|
1259
|
+
2 multiply, 3 negate, the order `src/emit-ops.ts` numbers them in. */
|
|
1260
|
+
static const char *const nish_arith_panics[6] = {
|
|
1261
|
+
"attempt to add with overflow\n", "attempt to subtract with overflow\n",
|
|
1262
|
+
"attempt to multiply with overflow\n", "attempt to negate with overflow\n",
|
|
1263
|
+
"attempt to divide with overflow\n", "attempt to divide by zero\n"};
|
|
1264
|
+
void nish_panic_overflow(uint32_t op) { nish_die(nish_arith_panics[op]); }
|
|
1265
|
+
void nish_panic_div(_Bool by_zero) { nish_panic_overflow(4 + by_zero); }
|