@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.
Files changed (57) hide show
  1. package/INSTALL.md +58 -40
  2. package/bin/nish +0 -0
  3. package/package.json +1 -1
  4. package/runtime/nish.d.ts +117 -8
  5. package/runtime/nish.h +67 -10
  6. package/runtime/nish.mjs +71 -7
  7. package/runtime/runtime-host.c +72 -26
  8. package/runtime/runtime-net.c +72 -19
  9. package/runtime/runtime-os.c +127 -45
  10. package/runtime/runtime-parallel.c +5 -1
  11. package/runtime/runtime-wasm.c +26 -2
  12. package/runtime/runtime.c +86 -14
  13. package/runtime/shim.mjs +114 -5
  14. package/scripts/build.sh +1 -3
  15. package/std/README.md +192 -36
  16. package/std/crypto/aes.ts +104 -21
  17. package/std/crypto/base64url.ts +43 -17
  18. package/std/crypto/chacha20poly1305.ts +52 -31
  19. package/std/crypto/ct.ts +14 -3
  20. package/std/crypto/hkdf.ts +385 -8
  21. package/std/crypto/hmac.ts +325 -9
  22. package/std/crypto/p256.ts +153 -27
  23. package/std/crypto/sha1.ts +147 -0
  24. package/std/crypto/sha256.ts +15 -3
  25. package/std/crypto/sha512.ts +16 -2
  26. package/std/crypto/x25519.ts +90 -37
  27. package/std/crypto/x509.ts +265 -65
  28. package/std/net/hpack.ts +1094 -0
  29. package/std/net/http-fields.ts +604 -0
  30. package/std/net/http1-server.ts +1389 -0
  31. package/std/net/http1.ts +1492 -0
  32. package/std/net/http2-frame.ts +745 -0
  33. package/std/net/http2-tls.ts +216 -0
  34. package/std/net/http2.ts +1433 -0
  35. package/std/net/http3-frame.ts +457 -0
  36. package/std/net/http3-server.ts +905 -0
  37. package/std/net/http3.ts +2145 -0
  38. package/std/net/qpack.ts +1163 -0
  39. package/std/net/quic-conn-ack.ts +206 -0
  40. package/std/net/quic-conn-cid.ts +425 -0
  41. package/std/net/quic-conn-params.ts +392 -0
  42. package/std/net/quic-datagram.ts +130 -0
  43. package/std/net/quic-frame.ts +1206 -0
  44. package/std/net/quic-listener.ts +653 -0
  45. package/std/net/quic-packet.ts +1474 -0
  46. package/std/net/quic-recovery.ts +849 -0
  47. package/std/net/quic-stream.ts +1603 -0
  48. package/std/net/quic.ts +3214 -0
  49. package/std/net/tls/codec.ts +1089 -0
  50. package/std/net/tls/record-server.ts +1173 -0
  51. package/std/net/tls/record.ts +621 -0
  52. package/std/net/tls/schedule.ts +429 -0
  53. package/std/net/tls-tcp.ts +410 -0
  54. package/std/net/tls.ts +1368 -0
  55. package/std/net/websocket.ts +823 -0
  56. package/std/net/webtransport.ts +1334 -0
  57. package/std/secret.ts +62 -0
@@ -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->data, O_RDONLY);
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
- if (len < 0) {
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)) > 0) got += n;
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->data, O_WRONLY | O_CREAT | flags, 0644);
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 <= 0) nish_io_fail("write ", path);
132
- done += n;
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->data, &st) == 0 && S_ISDIR(st.st_mode);
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->data, 0777) == 0 || nish_is_dir(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 char *out, const char *err) {
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
- for (; i < argv->len; i++) v[i] = s[i]->data;
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
- posix_spawn_file_actions_t *fap = 0;
188
- if (out || err) {
189
- if (posix_spawn_file_actions_init(&fa)) return -1;
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 (out) posix_spawn_file_actions_addopen(fap, 1, out, O_WRONLY | O_CREAT | O_TRUNC, 0644);
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], fap, 0, v, environ);
200
- if (fap) posix_spawn_file_actions_destroy(fap);
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, as
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->data : 0, err->len ? err->data : 0);
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->data);
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
- /* Insertion sort over pointers: a directory is short, and `qsort` would cost
256
- a comparator symbol and its unwind entry for a call that is never hot. */
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
- for (uint64_t i = 1; i < a->len; i++) {
259
- nish_str *s = v[i];
260
- uint64_t j = i;
261
- while (j && strcmp(v[j - 1]->data, s->data) > 0) {
262
- v[j] = v[j - 1];
263
- j--;
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
- const char *v = getenv(name->data);
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
- if (!realpath(path->data, buf)) return 0;
325
- uint64_t len = strlen(buf);
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
- int64_t chunks = (len + grain - 1) / grain;
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;
@@ -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 && nish_arena.off + size <= nish_arena.cap) {
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
- (void)!write(2, msg, strlen(msg));
87
+ fputs(msg, stderr);
84
88
  _exit(1);
85
89
  }
86
- #define nish_oom() nish_die("nish: out of memory\n")
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
- nish_str *s = nish_alloc_struct(8 + a->len + b->len + 1);
201
- s->len = a->len + b->len;
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[s->len] = 0;
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
- (void)!write(fd, s->data, s->len);
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
- void nish_array_grow(nish_array *a, uint64_t elem_size) {
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 division (Rust semantics): the failed-check path */
1191
- void nish_panic_div(_Bool by_zero) {
1192
- nish_die(by_zero ? "attempt to divide by zero\n" : "attempt to divide with overflow\n");
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); }