@amritk/nish-x86_64-linux 0.15.0 → 0.16.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 CHANGED
@@ -403,8 +403,15 @@ release is expected to move it. `--stages 1` stops one link sooner.
403
403
  and `--profile` spellings as an installed `nish`, because it is the same
404
404
  program, and makes every directory in the way of the IR, a sidecar or the
405
405
  binary itself. It looks for `scripts/build.sh` and the two `runtime/*.c` files
406
- one level up from wherever it was invoked, then in the working directory, so
407
- it wants a checkout or an installed package around it the way `nish` does.
406
+ one level up from wherever it was invoked (and one level up from the file a
407
+ symbolic link to it names), so it wants a checkout or an installed package
408
+ around it the way `nish` does. It falls back to the working directory only
409
+ when the compiler itself lies inside that directory, so a checkout you merely
410
+ `cd` into cannot hand it a `scripts/build.sh` to run. Keep `bin/nish` beside
411
+ `scripts/`, `runtime/` and `std/`, as every install above does, and keep that
412
+ directory's parent writable only by you: the package root is the parent of
413
+ the compiler's directory, so a compiler at `/tmp/x/nish` would trust
414
+ `/tmp/scripts/build.sh` (CLI-9 in [docs/security/cli.md](security/cli.md)).
408
415
 
409
416
  Because the seed is the last release, `src/` may only *use* in its own
410
417
  source the constructs that release compiles. A new construct is implemented
@@ -457,9 +464,11 @@ nish run hello.ts
457
464
  `$XDG_CACHE_HOME/nish/run` (or `~/.cache/nish/run`) when that program has not
458
465
  been linked before, and starts it with the arguments after the file. The first
459
466
  run of a new edit pays for one link, and later runs start the cached binary.
460
- It needs clang only for that link. It needs `HOME` or `XDG_CACHE_HOME` set,
461
- and refuses the run without them rather than keep a binary in a shared
462
- directory.
467
+ It needs clang only for that link. It needs `HOME` or `XDG_CACHE_HOME` set
468
+ to an absolute path (a relative one is ignored, as if it were unset), and
469
+ refuses the run without one rather than keep a binary in a shared directory.
470
+ The first run that builds into the cache makes its root `0700`, so no other
471
+ user can plant a binary in it ([docs/security/cli.md](security/cli.md)).
463
472
 
464
473
  ## 4. Exit codes
465
474
 
@@ -469,7 +478,7 @@ directory.
469
478
  | 1 | the program was rejected: compile error (`file:line:col: error: ...`), missing input file, or an `-o` layout that does not fit the module count |
470
479
  | 2 | usage error: unknown flag, missing argument, no input files |
471
480
  | 3 | toolchain error: `--link` found no `clang` (`CC` overrides), or `scripts/build.sh` failed (its output is shown; the `.ll` files are still written) — or the `nish` command found no prebuilt compiler for this platform, or one that would not start. Under `--json` all of these are one `NL0002` object |
472
- | | `nish run` answers these codes until the program starts, and the program's own exit status (`128 + n` for a signal) after that. With neither `HOME` nor `XDG_CACHE_HOME` set it answers 3 |
481
+ | | `nish run` answers these codes until the program starts, and the program's own exit status (`128 + n` for a signal) after that. With neither `HOME` nor `XDG_CACHE_HOME` set to an absolute path it answers 3 |
473
482
  | 70 | internal compiler error: an unexpected exception. Please report it at <https://github.com/amritk/nish/issues> with the input and command line; `NISH_DEBUG=1` prints the stack trace |
474
483
 
475
484
  ## Troubleshooting
package/bin/nish CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/nish-x86_64-linux",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "The @amritk/nish native compiler for Linux on x86_64",
5
5
  "license": "MIT",
6
6
  "os": [
package/runtime/nish.h CHANGED
@@ -63,6 +63,17 @@ typedef struct nish_str {
63
63
  char data[];
64
64
  } nish_str;
65
65
 
66
+ /* The bytes of `s` as the C string a system call takes, or "" when `s` holds
67
+ * a NUL. The kernel reads a path to its first NUL, so `"secret\0.txt"` would
68
+ * pass a `.txt` check and open `secret`; "" names nothing, so each caller
69
+ * answers it as it answers a missing path. The one test every path, name and
70
+ * argument the runtime hands the OS goes through (docs/security/runtime.md,
71
+ * RT-3). `__builtin_strlen`, so this header needs no libc header of its own,
72
+ * and `unused` because a host that includes it may never call it. */
73
+ static inline __attribute__((unused)) const char *nish_cpath(const nish_str *s) {
74
+ return __builtin_strlen(s->data) == s->len ? s->data : "";
75
+ }
76
+
66
77
  /* ---- Arena --------------------------------------------------------------
67
78
  * One bump allocator per process, or one per thread under `-DNISH_THREADS`
68
79
  * (WP20 T0; the layout and every function below are the same either way).
@@ -139,7 +150,16 @@ nish_str *nish_str_from_i64(int64_t v);
139
150
  nish_str *nish_str_from_u64(uint64_t v);
140
151
 
141
152
  /* Process and file I/O (WP7). `nish_exit` never returns; the file functions
142
- * print a message to stderr and exit(1) on a fatal error. Everything from here
153
+ * print a message to stderr and exit(1) on a fatal error.
154
+ *
155
+ * Every function here that takes a path, a name or an argument vector refuses
156
+ * one holding a NUL, answering as it answers a path that does not exist: the
157
+ * kernel would read only the bytes before the NUL, which is a different path
158
+ * from the one the program checked (docs/security/runtime.md, RT-3). A file
159
+ * longer than 2^31 - 1 bytes is unreadable, as it is to `nish_read_file_or_null`
160
+ * and `nish_read_file_bytes` too, because no string or array may be longer
161
+ * (RT-1). The two writers refuse a symbolic link as the last component of the
162
+ * path rather than follow it (RT-4). Everything from here
143
163
  * to the end of the clock section below is implemented in runtime-os.c, with
144
164
  * `nish_random` and `nish_argv_init` the exceptions: neither asks the operating
145
165
  * system anything (the clock only seeds the first, and the entry point hands the
@@ -194,6 +214,11 @@ typedef struct nish_array { uint64_t len; uint64_t cap; char *data; } nish_array
194
214
  * `elem_size` is `sizeof` one element, so for a record element type it is
195
215
  * `sizeof(struct Point)` and the block is a C array of them. */
196
216
  nish_array *nish_alloc_array(uint64_t elem_size, uint64_t len);
217
+ /* No array grows past 2^31 - 1 elements and no string past 2^31 - 1 bytes:
218
+ * `nish_array_grow`, `nish_alloc_array` and `nish_str_concat` fail as an
219
+ * allocation does (`nish: out of memory`, exit 1; a trap on wasm) rather than
220
+ * make one, because under --number-mode i32 its `length` would read back
221
+ * negative (docs/security/codegen.md K1-6, docs/security/runtime.md RT-2). */
197
222
  /* The cold paths compiled code calls: `push` when len == cap, a failed bounds
198
223
  * check. `nish_array_grow` moves `cap * elem_size` bytes into a fresh arena
199
224
  * block and writes `data`/`cap`, so for a record element type it relocates the
@@ -254,12 +279,12 @@ nish_array *nish_read_file_bytes(const nish_str *path);
254
279
  * `128 + n` when signal `n` killed it. -1 when `argv` is empty, when the
255
280
  * child cannot be started or waited for, and always under WASI, which has no
256
281
  * processes. The elements are `nish_str *`; the child receives their bytes,
257
- * so an argument containing a NUL is truncated at it. */
282
+ * and an argument containing a NUL is refused (-1) rather than truncated. */
258
283
  int32_t nish_spawn(const nish_array *argv);
259
284
  /* `spawnSyncTo(argv, stdoutPath, stderrPath)`: exactly `nish_spawn` — the same
260
285
  * `PATH` search, the same wait, the same status, `128 + n` and -1 — except that
261
286
  * each non-empty path receives that stream, created or truncated at 0644 as
262
- * `nish_write_file` would leave it. An **empty** string leaves that stream
287
+ * `nish_write_file` would leave it, a symbolic link at the path refused. An **empty** string leaves that stream
263
288
  * inherited, so one call can capture stdout and let stderr through to the
264
289
  * terminal. The child opens the files, so a failed open is a child that could
265
290
  * not start and answers -1 rather than leaving this process redirected. The two
@@ -275,8 +300,8 @@ int32_t nish_spawn_to(const nish_array *argv, const nish_str *out, const nish_st
275
300
  * arena (so a later `setenv` cannot change a string the program still holds),
276
301
  * or NULL when it is unset — the language's `string | null`. An empty value
277
302
  * is a set variable and answers a zero-length string, not NULL. `name` is
278
- * read and never retained; a NUL inside it truncates the lookup, as it does
279
- * for every other path-like argument here. */
303
+ * read and never retained; a name holding a NUL is unset, as it is for every
304
+ * other path-like argument here. */
280
305
  nish_str *nish_getenv(const nish_str *name);
281
306
 
282
307
  /* ---- Symlinks (WP19 §5a item 4) -----------------------------------------
@@ -336,6 +361,15 @@ double nish_stat_mtime(const nish_str *path);
336
361
  * or a read that fails. */
337
362
  int32_t nish_signal_fd(void);
338
363
  int32_t nish_read_signal(int32_t fd);
364
+ /* Who owns a path, and whether it runs (docs/security/runtime.md, RT-9; no
365
+ * builtin reaches these yet). `nish_lstat_owner_mode(path)`: `lstat`, so a
366
+ * symbolic link answers for itself, as the owner's uid in the high 32 bits
367
+ * and `st_mode` in the low 32, or exactly -1 when the path does not resolve.
368
+ * `nish_euid()`: the effective uid. `nish_is_executable(path)`:
369
+ * `access(path, X_OK)`, for the real uid. */
370
+ int64_t nish_lstat_owner_mode(const nish_str *path);
371
+ int64_t nish_euid(void);
372
+ bool nish_is_executable(const nish_str *path);
339
373
 
340
374
  /* ---- The network (WP34 N5), runtime/runtime-net.c, `nish:net` ------------
341
375
  * Every call answers an `int32_t`: `>= 0` on success (a descriptor, a byte
@@ -1,7 +1,8 @@
1
1
  /* Nish runtime, the host half: the wall clock, entropy, a file's modification
2
2
  * time and a descriptor that becomes readable when SIGTERM or SIGINT arrives
3
3
  * (WP34 N3) — the four facts a program that runs for days asks of the machine
4
- * it runs on.
4
+ * it runs on — and who owns a path and whether it runs, which a driver asks
5
+ * before it trusts one.
5
6
  *
6
7
  * A translation unit of its own for the reason runtime-os.c is one: each file
7
8
  * carries its own measured `.text*` ceiling in tests/run.js. runtime-os.c had
@@ -33,6 +34,7 @@ typedef int nish_host_unused;
33
34
  #include <signal.h>
34
35
  #include <stdint.h>
35
36
  #include <stdio.h>
37
+ #include <string.h>
36
38
  #include <sys/stat.h>
37
39
  #include <time.h>
38
40
  #include <unistd.h>
@@ -98,7 +100,7 @@ void nish_random_fill(nish_array *bytes) {
98
100
  digits. `stat` follows a symbolic link, as `fs.statSync` does. */
99
101
  double nish_stat_mtime(const nish_str *path) {
100
102
  struct stat st;
101
- if (stat(path->data, &st) != 0) return __builtin_nan("");
103
+ if (stat(nish_cpath(path), &st) != 0) return __builtin_nan("");
102
104
  #if defined(__APPLE__)
103
105
  struct timespec t = st.st_mtimespec;
104
106
  #else
@@ -107,6 +109,31 @@ double nish_stat_mtime(const nish_str *path) {
107
109
  return (double)t.tv_sec * 1e3 + (double)t.tv_nsec / 1e6;
108
110
  }
109
111
 
112
+ /* ---- Who owns a path, and whether it runs (docs/security/runtime.md, RT-9)
113
+ *
114
+ * The primitives a driver needs before it trusts a directory or a program it
115
+ * found on its own. `nish_lstat_owner_mode` answers for the path itself, a
116
+ * symbolic link included, rather than for what it names: the owner's uid in
117
+ * the high 32 bits and `st_mode` in the low 32, or exactly -1 when the path
118
+ * does not resolve or holds a NUL. `nish_euid` is the user to compare the
119
+ * owner with, and `nish_is_executable` is `access(X_OK)`: whether the real
120
+ * user may run the file, which is what tells an executable `PATH` entry from
121
+ * one that is only readable. Nothing in the language reaches them yet: `src/` may call
122
+ * a runtime function only once the last release declares it, so they are here
123
+ * for the change after the next release (docs/security/cli.md, CLI-7 and
124
+ * CLI-9). */
125
+ int64_t nish_lstat_owner_mode(const nish_str *path) {
126
+ struct stat st;
127
+ if (lstat(nish_cpath(path), &st) != 0) return -1;
128
+ return (int64_t)((uint64_t)(uint32_t)st.st_uid << 32 | (uint32_t)st.st_mode);
129
+ }
130
+
131
+ int64_t nish_euid(void) { return (int64_t)geteuid(); }
132
+
133
+ _Bool nish_is_executable(const nish_str *path) {
134
+ return access(nish_cpath(path), X_OK) == 0;
135
+ }
136
+
110
137
  /* ---- Signals: `signalFd()` and `readSignal(fd)`
111
138
  *
112
139
  * A descriptor rather than a handler in the language, because a handler would
@@ -229,18 +229,24 @@ int32_t nish_tcp_listen(const nish_str *host, int32_t port, int32_t backlog) {
229
229
  return nish_net_bound(host, port, SOCK_STREAM, backlog);
230
230
  }
231
231
 
232
- /* A socket address into the caller's 18-byte form. An IPv4 peer is written
233
- mapped, and the port is already in network order, which is the form's
234
- big-endian. */
235
- static void nish_net_put(nish_array *out, const nish_sockaddr *s) {
232
+ /* A socket address of `n` bytes into the caller's 18-byte form. An IPv4 peer
233
+ is written mapped, and the port is already in network order, which is the
234
+ form's big-endian. Anything else -- no address at all, which is what
235
+ `recvmsg` reports on a connected or Unix socket, or a family that is not
236
+ IP -- is eighteen zeros: those bytes of `s` were never written, and copying
237
+ them handed the caller whatever the stack held (docs/security/runtime.md,
238
+ RT-6). */
239
+ static void nish_net_put(nish_array *out, const nish_sockaddr *s, socklen_t n) {
236
240
  unsigned char *p = (unsigned char *)out->data;
237
- uint16_t port = s->v6.sin6_port;
238
- if (s->sa.sa_family == AF_INET) {
241
+ uint16_t port = 0;
242
+ memset(p, 0, NISH_ADDR_BYTES);
243
+ if (n >= sizeof s->v4 && s->sa.sa_family == AF_INET) {
239
244
  memcpy(p, nish_mapped_prefix, 12);
240
245
  memcpy(p + 12, &s->v4.sin_addr, 4);
241
246
  port = s->v4.sin_port;
242
- } else {
247
+ } else if (n >= sizeof s->v6 && s->sa.sa_family == AF_INET6) {
243
248
  memcpy(p, &s->v6.sin6_addr, 16);
249
+ port = s->v6.sin6_port;
244
250
  }
245
251
  memcpy(p + 16, &port, 2);
246
252
  }
@@ -259,7 +265,7 @@ int32_t nish_tcp_accept(int32_t fd, nish_array *peer) {
259
265
  int c = nish_net_setup(accept(fd, &s.sa, &n));
260
266
  #endif
261
267
  if (c < 0) return nish_net_fail();
262
- nish_net_put(peer, &s);
268
+ nish_net_put(peer, &s, n);
263
269
  return c;
264
270
  }
265
271
 
@@ -399,7 +405,7 @@ int32_t nish_udp_recv_from(int32_t fd, nish_array *buf, int64_t off, int64_t len
399
405
  }
400
406
  #endif
401
407
  memcpy(meta->data, info, sizeof info);
402
- nish_net_put(from, &s);
408
+ nish_net_put(from, &s, m.msg_namelen);
403
409
  return (int32_t)got;
404
410
  }
405
411
 
@@ -52,6 +52,14 @@ extern char **environ;
52
52
 
53
53
  #include "nish.h"
54
54
 
55
+ /* Darwin's <fcntl.h> declares `O_NOFOLLOW` only at its full C level, and the
56
+ strict level the two macros above select is the one that binds the right
57
+ `realpath`, so the flag is spelled here with Darwin's own value
58
+ (<sys/fcntl.h>: 0x00000100). Linux and WASI declare it at POSIX 2008. */
59
+ #if defined(__APPLE__) && !defined(O_NOFOLLOW)
60
+ #define O_NOFOLLOW 0x00000100
61
+ #endif
62
+
55
63
  /* The same spelling runtime.c uses for its own cold paths: a function that
56
64
  reports and exits is never on a path worth optimising for. */
57
65
  #define NISH_COLD __attribute__((noreturn, cold, noinline))
@@ -63,6 +71,12 @@ static NISH_COLD void nish_io_fail(const char *what, const nish_str *path) {
63
71
  _exit(1);
64
72
  }
65
73
 
74
+ /* The longest string or array a read hands back: 2^31 - 1, the same limit
75
+ runtime.c's `NISH_ARRAY_MAX` puts on a push. Under --number-mode i32
76
+ `length` is an `i32`, so one byte more reads back negative and bounds-check
77
+ elimination trusts it (docs/security/runtime.md, RT-1). */
78
+ #define NISH_LENGTH_MAX 2147483647
79
+
66
80
  /* `readFileSyncOrNull(path)`: null rather than a message, so a program can
67
81
  turn a missing file into its own diagnostic and carry on.
68
82
  *
@@ -84,11 +98,16 @@ static NISH_COLD void nish_io_fail(const char *what, const nish_str *path) {
84
98
  * rather than `stat` on the path, so the answer is about the file that was
85
99
  * opened and not about whatever the name means a moment later. */
86
100
  nish_str *nish_read_file_or_null(const nish_str *path) {
87
- int fd = open(path->data, O_RDONLY);
101
+ int fd = open(nish_cpath(path), O_RDONLY);
88
102
  if (fd < 0) return 0;
89
103
  struct stat st;
90
104
  off_t len = fstat(fd, &st) == 0 && !S_ISDIR(st.st_mode) ? lseek(fd, 0, SEEK_END) : -1;
91
- if (len < 0) {
105
+ /* A file longer than a length can say is refused before anything is
106
+ allocated, as an unreadable one is: null here, null from
107
+ `readFileBytesSync`, and `cannot read` from `readFileSync`. The bytes read
108
+ below are never more than `len`, so a file that grows meanwhile cannot
109
+ pass the limit either. */
110
+ if (len < 0 || len > NISH_LENGTH_MAX) {
92
111
  close(fd);
93
112
  return 0;
94
113
  }
@@ -123,8 +142,14 @@ nish_str *nish_read_file(const nish_str *path) {
123
142
  return s;
124
143
  }
125
144
 
145
+ /* `O_NOFOLLOW` (docs/security/runtime.md, RT-4): a symbolic link as the last
146
+ component of the path is refused, as an unwritable file is, rather than
147
+ followed to whatever file it names. `nish file.ts` writes `file.ll` beside
148
+ its source, so a link planted at that name in a checkout would otherwise
149
+ truncate any file the user can write. A link among the directories on the
150
+ way is still followed. */
126
151
  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);
152
+ int fd = open(nish_cpath(path), O_WRONLY | O_CREAT | O_NOFOLLOW | flags, 0644);
128
153
  if (fd < 0) nish_io_fail("write ", path);
129
154
  for (uint64_t done = 0; done < data->len;) {
130
155
  ssize_t n = write(fd, data->data + done, data->len - done);
@@ -149,14 +174,14 @@ void nish_append_file(const nish_str *path, const nish_str *data) { nish_put_fil
149
174
  answer to that question. */
150
175
  _Bool nish_is_dir(const nish_str *path) {
151
176
  struct stat st;
152
- return stat(path->data, &st) == 0 && S_ISDIR(st.st_mode);
177
+ return stat(nish_cpath(path), &st) == 0 && S_ISDIR(st.st_mode);
153
178
  }
154
179
 
155
180
  /* One directory, not recursive. The retry is the `stat` above rather than
156
181
  `errno == EEXIST` so that a plain file at the path answers false, which is
157
182
  what the promise "a directory is there afterwards" means. */
158
183
  _Bool nish_mkdir(const nish_str *path) {
159
- return mkdir(path->data, 0777) == 0 || nish_is_dir(path);
184
+ return mkdir(nish_cpath(path), 0777) == 0 || nish_is_dir(path);
160
185
  }
161
186
 
162
187
  /* `posix_spawnp` is one libc call where fork/execvp/waitpid would be three,
@@ -168,7 +193,7 @@ _Bool nish_mkdir(const nish_str *path) {
168
193
  two arguments answered differently, so the argv vector, the wait and the
169
194
  signal convention are written once; it is `static`, so a program that spawns
170
195
  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) {
196
+ static int32_t nish_spawn_impl(const nish_array *argv, const nish_str *out, const nish_str *err) {
172
197
  #ifdef __wasi__
173
198
  (void)argv;
174
199
  (void)out;
@@ -181,18 +206,27 @@ static int32_t nish_spawn_impl(const nish_array *argv, const char *out, const ch
181
206
  path out of here has anything to free. */
182
207
  char **v = nish_alloc_struct((argv->len + 1) * sizeof *v);
183
208
  uint64_t i = 0;
184
- for (; i < argv->len; i++) v[i] = s[i]->data;
209
+ /* An argument holding a NUL is refused rather than cut short at it, which
210
+ would run a different command line from the one the program built: it is
211
+ the one whose C string is not its own bytes. */
212
+ for (; i < argv->len; i++) {
213
+ v[i] = (char *)nish_cpath(s[i]);
214
+ if (v[i] != s[i]->data) return -1;
215
+ }
185
216
  v[i] = 0;
186
217
  posix_spawn_file_actions_t fa;
187
218
  posix_spawn_file_actions_t *fap = 0;
188
- if (out || err) {
189
- if (posix_spawn_file_actions_init(&fa)) return -1;
190
- fap = &fa;
219
+ const nish_str *to[2] = { out, err };
220
+ for (int k = 0; k < 2; k++) {
221
+ if (!to[k]) continue;
222
+ if (!fap) {
223
+ if (posix_spawn_file_actions_init(&fa)) return -1;
224
+ fap = &fa;
225
+ }
191
226
  /* The child opens the file, not this process: a redirect that this process
192
227
  performed would have to be undone afterwards, and a failed open would
193
228
  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);
229
+ posix_spawn_file_actions_addopen(fap, k + 1, nish_cpath(to[k]), O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, 0644);
196
230
  }
197
231
  pid_t pid;
198
232
  int status;
@@ -210,14 +244,15 @@ int32_t nish_spawn(const nish_array *argv) { return nish_spawn_impl(argv, 0, 0);
210
244
  to a file, which is what comparing a program's output against a golden needs
211
245
  — `nish_spawn` answers a status and the output is gone. An **empty** path
212
246
  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.
247
+ through to the terminal. Each file is created or truncated at 0644 and a
248
+ symbolic link at its name is refused, as `writeFileSync` does; the child
249
+ then fails to start, and the answer is -1.
215
250
 
216
251
  Two streams must not name one path: each would be opened separately, with
217
252
  its own offset, and the two would overwrite each other rather than
218
253
  interleave. Capturing them apart and concatenating is the way to merge. */
219
254
  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);
255
+ return nish_spawn_impl(argv, out->len ? out : 0, err->len ? err : 0);
221
256
  }
222
257
 
223
258
  /* `readdirSync(path)`: the listing a driver needs to discover its own inputs.
@@ -240,7 +275,7 @@ nish_array *nish_readdir(const nish_str *path) {
240
275
  (void)path;
241
276
  return 0;
242
277
  #else
243
- DIR *d = opendir(path->data);
278
+ DIR *d = opendir(nish_cpath(path));
244
279
  if (!d) return 0;
245
280
  nish_array *a = nish_alloc_struct(sizeof *a);
246
281
  *a = (nish_array){ 0, 0, 0 };
@@ -252,15 +287,30 @@ nish_array *nish_readdir(const nish_str *path) {
252
287
  ((nish_str **)a->data)[a->len++] = nish_str_new(n, strlen(n));
253
288
  }
254
289
  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. */
290
+ /* Heapsort over pointers, in place. It was an insertion sort, on the theory
291
+ that a directory is short, but whoever fills the directory decides that:
292
+ 100,000 names took 2.5 billion compares and 12.5 s, where this takes 3.0
293
+ million and 0.07 s (docs/security/runtime.md, RT-5). It needs no scratch
294
+ memory, and unlike `qsort` costs no comparator symbol or unwind entry. */
257
295
  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--;
296
+ /* Each pass sifts one name down the max-heap `v[0, end)`: first every
297
+ parent from the middle up, which builds the heap, then the name the
298
+ largest is swapped out for, which shrinks the heap by one. */
299
+ uint64_t i = a->len / 2, end = a->len;
300
+ while (end > 1) {
301
+ nish_str *s;
302
+ if (i) {
303
+ s = v[--i];
304
+ } else {
305
+ s = v[--end];
306
+ v[end] = v[0];
307
+ }
308
+ uint64_t j = i, c;
309
+ while ((c = 2 * j + 1) < end) {
310
+ if (c + 1 < end && strcmp(v[c]->data, v[c + 1]->data) < 0) c++;
311
+ if (strcmp(s->data, v[c]->data) >= 0) break;
312
+ v[j] = v[c];
313
+ j = c;
264
314
  }
265
315
  v[j] = s;
266
316
  }
@@ -290,13 +340,8 @@ int64_t nish_monotonic_nanos(void) {
290
340
  string with the lifetime every other one has. NULL for an unset variable,
291
341
  which is the language's `string | null`. Contract: nish.h. */
292
342
  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;
343
+ const char *v = getenv(nish_cpath(name));
344
+ return v ? nish_str_new(v, strlen(v)) : 0;
300
345
  }
301
346
 
302
347
  /* ---- Symlinks (WP19 §5a item 4): `realpathSync(path)`, the one path
@@ -321,12 +366,7 @@ nish_str *nish_getenv(const nish_str *name) {
321
366
  it resolves. Contract: nish.h. */
322
367
  nish_str *nish_realpath(const nish_str *path) {
323
368
  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;
369
+ return realpath(nish_cpath(path), buf) ? nish_str_new(buf, strlen(buf)) : 0;
330
370
  }
331
371
 
332
372
  /* ---- 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;
package/runtime/runtime.c CHANGED
@@ -83,7 +83,13 @@ static NISH_COLD void nish_die(const char *msg) {
83
83
  (void)!write(2, msg, strlen(msg));
84
84
  _exit(1);
85
85
  }
86
- #define nish_oom() nish_die("nish: out of memory\n")
86
+ /* A function rather than a macro around `nish_die`: seven call sites each
87
+ loading the message's address cost more than one more symbol does. */
88
+ static NISH_COLD void nish_oom(void) { nish_die("nish: out of memory\n"); }
89
+
90
+ /* The most elements an array and the most bytes a string may hold: see
91
+ `nish_array_grow` and `nish_str_concat`. */
92
+ #define NISH_ARRAY_MAX 2147483647u
87
93
 
88
94
  /* Slow path (size 8-byte rounded): push a chunk (>= 64 KB), bump from it. */
89
95
  void *nish_arena_grow(uint64_t size) {
@@ -196,9 +202,16 @@ nish_str *nish_str_new(const char *bytes, uint64_t len) {
196
202
  return s;
197
203
  }
198
204
 
205
+ /* A string never passes 2^31 - 1 bytes, for the reason an array never passes
206
+ that many elements (`NISH_ARRAY_MAX`, below): under --number-mode i32 a
207
+ longer one's `length` reads back negative and bounds-check elimination
208
+ trusts it (docs/security/runtime.md, RT-2). `a + b` past it fails as an
209
+ allocation does. */
199
210
  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;
211
+ uint64_t len = a->len + b->len;
212
+ if (len > NISH_ARRAY_MAX) nish_oom();
213
+ nish_str *s = nish_alloc_struct(8 + len + 1);
214
+ s->len = len;
202
215
  memcpy(s->data, a->data, a->len);
203
216
  memcpy(s->data + a->len, b->data, b->len);
204
217
  s->data[s->len] = 0;
@@ -1106,8 +1119,12 @@ double nish_random(void) {
1106
1119
  /* ---- Arrays: %struct.nish_array = type { i64, i64, i8* }, cold paths */
1107
1120
  typedef struct nish_array { uint64_t len; uint64_t cap; char *data; } nish_array;
1108
1121
 
1109
- /* Host entry (WP8): `len` uninitialised elements, len == cap */
1122
+ /* Host entry (WP8): `len` uninitialised elements, len == cap. A `len` past
1123
+ `NISH_ARRAY_MAX` fails as an allocation does, which also keeps
1124
+ `len * elem_size` from wrapping into a short block behind a long header
1125
+ (docs/security/runtime.md, RT-7). */
1110
1126
  nish_array *nish_alloc_array(uint64_t elem_size, uint64_t len) {
1127
+ if (len > NISH_ARRAY_MAX) nish_oom();
1111
1128
  nish_array *a = nish_alloc_struct(sizeof *a);
1112
1129
  *a = (nish_array){ len, len, nish_alloc_struct(len * elem_size) };
1113
1130
  return a;
@@ -1160,9 +1177,26 @@ double nish_parse_number(const nish_str *s, int32_t mode) {
1160
1177
  * `sizeof` one element, which for a record element type (WP15 section 2a) is
1161
1178
  * the whole struct, so this relocates the elements themselves rather than a
1162
1179
  * 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) {
1180
+ * `data` across a push, and the checker refuses the programs that would.
1181
+ *
1182
+ * A push never takes an array past 2^31 - 1 elements (docs/security/codegen.md,
1183
+ * K1-6). Under --number-mode i32 `length` is an `i32`, and one element more
1184
+ * read back as a negative or a short length everywhere, a hash or a tag
1185
+ * compare included. This file cannot tell which mode compiled its caller, so
1186
+ * the limit holds in both, and the push past it fails as an allocation does,
1187
+ * through `nish_oom`. `new Array(n)` checks its own `n` in the IR.
1188
+ *
1189
+ * `noinline`: under LTO the grown body was inlined into every push site, and
1190
+ * the extra compare moved register allocation in the hot loops around them
1191
+ * (+0.2% to +2% instructions over bench/instructions.json's map rows). Out of
1192
+ * line, every one of those rows runs fewer instructions than it did before, and
1193
+ * the call costs nothing on a path that runs once per doubling. */
1194
+ __attribute__((noinline)) void nish_array_grow(nish_array *a, uint64_t elem_size) {
1165
1195
  uint64_t cap = a->cap ? a->cap * 2 : 4;
1196
+ if (cap > NISH_ARRAY_MAX) {
1197
+ if (a->len >= NISH_ARRAY_MAX) nish_oom();
1198
+ cap = NISH_ARRAY_MAX;
1199
+ }
1166
1200
  char *data = nish_alloc_struct(cap * elem_size);
1167
1201
  if (a->len) memcpy(data, a->data, a->len * elem_size);
1168
1202
  a->data = data;