@amritk/nish-x86_64-linux 0.16.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 (55) hide show
  1. package/INSTALL.md +43 -34
  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 +30 -7
  6. package/runtime/nish.mjs +71 -7
  7. package/runtime/runtime-host.c +43 -24
  8. package/runtime/runtime-net.c +57 -10
  9. package/runtime/runtime-os.c +62 -20
  10. package/runtime/runtime-wasm.c +1 -0
  11. package/runtime/runtime.c +46 -8
  12. package/runtime/shim.mjs +114 -5
  13. package/scripts/build.sh +1 -3
  14. package/std/README.md +166 -35
  15. package/std/crypto/aes.ts +51 -8
  16. package/std/crypto/base64url.ts +25 -12
  17. package/std/crypto/chacha20poly1305.ts +43 -29
  18. package/std/crypto/ct.ts +6 -1
  19. package/std/crypto/hkdf.ts +364 -3
  20. package/std/crypto/hmac.ts +307 -7
  21. package/std/crypto/p256.ts +148 -27
  22. package/std/crypto/sha1.ts +147 -0
  23. package/std/crypto/sha256.ts +3 -2
  24. package/std/crypto/x25519.ts +88 -36
  25. package/std/crypto/x509.ts +130 -47
  26. package/std/net/hpack.ts +1094 -0
  27. package/std/net/http-fields.ts +604 -0
  28. package/std/net/http1-server.ts +1389 -0
  29. package/std/net/http1.ts +1492 -0
  30. package/std/net/http2-frame.ts +745 -0
  31. package/std/net/http2-tls.ts +216 -0
  32. package/std/net/http2.ts +1433 -0
  33. package/std/net/http3-frame.ts +457 -0
  34. package/std/net/http3-server.ts +905 -0
  35. package/std/net/http3.ts +2145 -0
  36. package/std/net/qpack.ts +1163 -0
  37. package/std/net/quic-conn-ack.ts +206 -0
  38. package/std/net/quic-conn-cid.ts +425 -0
  39. package/std/net/quic-conn-params.ts +392 -0
  40. package/std/net/quic-datagram.ts +130 -0
  41. package/std/net/quic-frame.ts +1206 -0
  42. package/std/net/quic-listener.ts +653 -0
  43. package/std/net/quic-packet.ts +1474 -0
  44. package/std/net/quic-recovery.ts +849 -0
  45. package/std/net/quic-stream.ts +1603 -0
  46. package/std/net/quic.ts +3214 -0
  47. package/std/net/tls/codec.ts +1089 -0
  48. package/std/net/tls/record-server.ts +1173 -0
  49. package/std/net/tls/record.ts +621 -0
  50. package/std/net/tls/schedule.ts +429 -0
  51. package/std/net/tls-tcp.ts +410 -0
  52. package/std/net/tls.ts +1368 -0
  53. package/std/net/websocket.ts +823 -0
  54. package/std/net/webtransport.ts +1334 -0
  55. package/std/secret.ts +62 -0
@@ -156,46 +156,65 @@ _Bool nish_is_executable(const nish_str *path) {
156
156
  * async-signal-safe. The write end is non-blocking, so a thousand unread
157
157
  * signals lose the newest rather than wedge the handler, and `errno` is put
158
158
  * back for the code the signal interrupted. The descriptor is made once and
159
- * every later `signalFd()` answers the same one. */
160
- static int nish_signal_read_end = -1;
161
- static int nish_signal_write_end = -1;
159
+ * every later `signalFd()` answers the same one.
160
+ *
161
+ * Both ends live in one word, the read end in the high half and the write end
162
+ * in the low, -1 until the pipe exists, so that publishing them is a single
163
+ * compare-and-swap (docs/security/runtime.md, RT-10). Two threads whose first
164
+ * calls overlap can each make a pipe; one swap wins, the other caller closes
165
+ * its own pipe and answers the winner's read end. With two separate words the
166
+ * call that finished last answered its own pipe while the handler wrote to
167
+ * the other, so one caller never heard a signal and a pipe leaked. Each first
168
+ * caller installs the handler itself before it returns, so neither can hand
169
+ * back a descriptor before a signal reaches it. */
170
+ static int64_t nish_signal_pipe = -1;
162
171
 
163
172
  static void nish_on_signal(int sig) {
164
173
  int saved = errno;
165
174
  unsigned char b = (unsigned char)sig;
166
- (void)!write(nish_signal_write_end, &b, 1);
175
+ /* The low half: the write end. */
176
+ (void)!write((int)__atomic_load_n(&nish_signal_pipe, __ATOMIC_RELAXED), &b, 1);
167
177
  errno = saved;
168
178
  }
169
179
 
170
180
  int32_t nish_signal_fd(void) {
171
- if (nish_signal_read_end >= 0) return nish_signal_read_end;
172
- int p[2];
181
+ int64_t cur = __atomic_load_n(&nish_signal_pipe, __ATOMIC_ACQUIRE);
182
+ if (cur < 0) {
183
+ int p[2];
173
184
  #if defined(__linux__)
174
- /* Both ends close-on-exec from the moment they exist, so a child a sibling
175
- thread spawns meanwhile cannot inherit either. */
176
- if (pipe2(p, O_CLOEXEC) != 0) return -1;
185
+ /* Both ends close-on-exec from the moment they exist, so a child a sibling
186
+ thread spawns meanwhile cannot inherit either. */
187
+ if (pipe2(p, O_CLOEXEC) != 0) return -1;
177
188
  #else
178
- /* Darwin has no `pipe2`, so there is a window between `pipe` and the two
179
- `fcntl`s in which a child spawned by another thread inherits the ends. */
180
- if (pipe(p) != 0) return -1;
181
- fcntl(p[0], F_SETFD, FD_CLOEXEC);
182
- fcntl(p[1], F_SETFD, FD_CLOEXEC);
189
+ /* Darwin has no `pipe2`, so there is a window between `pipe` and the two
190
+ `fcntl`s in which a child spawned by another thread inherits the ends. */
191
+ if (pipe(p) != 0) return -1;
192
+ fcntl(p[0], F_SETFD, FD_CLOEXEC);
193
+ fcntl(p[1], F_SETFD, FD_CLOEXEC);
183
194
  #endif
184
- fcntl(p[1], F_SETFL, O_NONBLOCK);
185
- nish_signal_write_end = p[1];
186
- struct sigaction sa;
187
- sa.sa_handler = nish_on_signal;
188
- sa.sa_flags = SA_RESTART;
189
- sigemptyset(&sa.sa_mask);
190
- sigaction(SIGINT, &sa, 0);
191
- sigaction(SIGTERM, &sa, 0);
192
- return nish_signal_read_end = p[0];
195
+ fcntl(p[1], F_SETFL, O_NONBLOCK);
196
+ int64_t mine = (int64_t)p[0] << 32 | (uint32_t)p[1];
197
+ /* A failed swap leaves the winner's pair in `cur`. */
198
+ if (__atomic_compare_exchange_n(&nish_signal_pipe, &cur, mine, 0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) {
199
+ cur = mine;
200
+ } else {
201
+ close(p[0]);
202
+ close(p[1]);
203
+ }
204
+ struct sigaction sa;
205
+ sa.sa_handler = nish_on_signal;
206
+ sa.sa_flags = SA_RESTART;
207
+ sigemptyset(&sa.sa_mask);
208
+ sigaction(SIGINT, &sa, 0);
209
+ sigaction(SIGTERM, &sa, 0);
210
+ }
211
+ return (int32_t)(cur >> 32);
193
212
  }
194
213
 
195
214
  /* Block until a signal's byte arrives and answer it: 15 or 2, or -1 for any
196
215
  `fd` that is not the signal descriptor and for a read that fails. */
197
216
  int32_t nish_read_signal(int32_t fd) {
198
- if (fd != nish_signal_read_end) return -1;
217
+ if (fd != (int32_t)(__atomic_load_n(&nish_signal_pipe, __ATOMIC_ACQUIRE) >> 32)) return -1;
199
218
  unsigned char b;
200
219
  ssize_t n;
201
220
  do n = read(fd, &b, 1);
@@ -12,13 +12,15 @@
12
12
  * scripts/build.sh pairs this file with runtime.c like the other halves, so a
13
13
  * link line still names one runtime.
14
14
  *
15
- * The error convention is the one `signalFd` set: an `i32`, `>= 0` for
16
- * success (a descriptor, a byte count, 0), and a negative errno otherwise.
17
- * The codes a loop branches on are Linux's numbers on every platform (-11
15
+ * Errors take the shape `signalFd` set, an `i32` that is `>= 0` for success
16
+ * (a descriptor, a byte count, 0) and negative for a failure, and add the
17
+ * errno to it: where `signalFd` and `readSignal` answer a plain -1, a call
18
+ * here answers the failure's errno, negated. The codes a loop branches on are Linux's numbers on every platform (-11
18
19
  * would block, -95 unsupported, -32 the peer is gone, -104 reset, -98 the
19
- * address is in use, -22 a bad argument), so `nish_net_err` translates
20
- * Darwin's; any other failure is the host's own `-errno`. Nothing here
21
- * allocates: the addresses a call reads or writes are the caller's `u8[]`.
20
+ * address is in use, -111 refused, -110 timed out, -22 a bad argument), so
21
+ * `nish_net_err` translates Darwin's; any other failure is the host's own
22
+ * `-errno`. Nothing here allocates: the addresses a call reads or writes are
23
+ * the caller's `u8[]`.
22
24
  *
23
25
  * An address is 18 bytes of that array: the 16 bytes of an IPv6 address, an
24
26
  * IPv4 one as `::ffff:a.b.c.d`, then the port, big-endian. An IPv4 address is
@@ -33,7 +35,10 @@
33
35
  * both, never SIGPIPE), the errno numbers, UDP's segmentation offload
34
36
  * (`UDP_SEGMENT`, `UDP_GRO`) and ECN marks, which Linux has and for which
35
37
  * Darwin answers -95, and the readiness loop, which is epoll on Linux and
36
- * kqueue on Darwin. The Darwin branch is compiled by CI's Darwin bootstrap
38
+ * kqueue on Darwin. Only the sending side and the GRO bind say -95 there: a
39
+ * receive leaves `meta` at 0, 0, and a `meta[1]` of 0 is also what a real
40
+ * Not-ECT datagram reads as, so a program learns that Darwin has no ECN from
41
+ * the send, never from the receive. The Darwin branch is compiled by CI's Darwin bootstrap
37
42
  * rows and run by nothing. A WASI build has none of this (the checker refuses
38
43
  * every `nish:net` call under a wasm target), so there the file is empty.
39
44
  */
@@ -91,6 +96,8 @@ static int32_t nish_net_err(int e) {
91
96
  if (e == EOPNOTSUPP || e == ENOTSUP) return -95;
92
97
  if (e == ECONNRESET) return -104;
93
98
  if (e == EADDRINUSE) return -98;
99
+ if (e == ECONNREFUSED) return -111;
100
+ if (e == ETIMEDOUT) return -110;
94
101
  #endif
95
102
  return -e;
96
103
  }
@@ -111,8 +118,11 @@ static int nish_net_parse(const nish_str *s, unsigned char a[16]) {
111
118
  return inet_pton(AF_INET6, host, a) == 1;
112
119
  }
113
120
 
114
- /* `a` and `port` as a socket address of the family `v4` names. */
115
- static socklen_t nish_net_sockaddr(nish_sockaddr *s, const unsigned char a[16], int32_t port, int v4) {
121
+ /* `a` and `port` as a socket address of the family `v4` names. `noinline`:
122
+ three calls share one body, which inlined into each was 42 bytes more of
123
+ `.text` than the calls cost. */
124
+ __attribute__((noinline)) static socklen_t nish_net_sockaddr(nish_sockaddr *s, const unsigned char a[16],
125
+ int32_t port, int v4) {
116
126
  memset(s, 0, sizeof *s);
117
127
  if (v4) {
118
128
  s->v4.sin_family = AF_INET;
@@ -269,6 +279,41 @@ int32_t nish_tcp_accept(int32_t fd, nish_array *peer) {
269
279
  return c;
270
280
  }
271
281
 
282
+ /* `tcpConnect(addr)`: a new socket, non-blocking and close-on-exec, connecting
283
+ to the address in `addr`; an IPv4 one is an `AF_INET` socket, any other
284
+ `AF_INET6`. A non-blocking `connect` seldom finishes at once (on Linux not
285
+ even over loopback), and its `EINPROGRESS` (-115) is not a failure: the
286
+ descriptor is answered all the same, and the connection is made once the
287
+ loop reports it writable, when `connectResult` says how it went. Any other
288
+ failure closes the socket and is answered instead of it. An `addr` shorter
289
+ than 18 bytes is -22 before any socket exists. */
290
+ int32_t nish_tcp_connect(const nish_array *addr) {
291
+ if (addr->len < NISH_ADDR_BYTES) return -22;
292
+ const unsigned char *p = (const unsigned char *)addr->data;
293
+ int v4 = memcmp(p, nish_mapped_prefix, 12) == 0;
294
+ int fd = nish_net_socket(v4 ? AF_INET : AF_INET6, SOCK_STREAM);
295
+ if (fd < 0) return nish_net_fail();
296
+ nish_sockaddr s;
297
+ socklen_t n = nish_net_sockaddr(&s, p, (p[16] << 8) | p[17], v4);
298
+ if (connect(fd, &s.sa, n) != 0 && errno != EINPROGRESS) {
299
+ int e = errno;
300
+ close(fd);
301
+ return nish_net_err(e);
302
+ }
303
+ return fd;
304
+ }
305
+
306
+ /* `connectResult(fd)`: how the connection `tcpConnect` started went, once the
307
+ descriptor is writable: 0 when it is made, or its failure (-111 refused,
308
+ -110 timed out) from `SO_ERROR`. The kernel clears `SO_ERROR` as it is
309
+ read, so a failure is answered once. */
310
+ int32_t nish_connect_result(int32_t fd) {
311
+ int e = 0;
312
+ socklen_t n = sizeof e;
313
+ if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &e, &n) != 0) return nish_net_fail();
314
+ return e != 0 ? nish_net_err(e) : 0;
315
+ }
316
+
272
317
  /* `netRead(fd, buf, off, len)`: at most `len` bytes into `buf` from `off`.
273
318
  The compiled call has already checked `0 <= off <= off + len <=
274
319
  buf.length`, unless the program was built with `--unchecked-indexing`. */
@@ -390,7 +435,9 @@ int32_t nish_udp_recv_from(int32_t fd, nish_array *buf, int64_t off, int64_t len
390
435
  .msg_name = &s, .msg_namelen = sizeof s, .msg_iov = &iov, .msg_iovlen = 1, .msg_control = c.b, .msg_controllen = sizeof c};
391
436
  ssize_t got = recvmsg(fd, &m, 0);
392
437
  if (got < 0) return nish_net_fail();
393
- /* Darwin sets neither option, so both stay 0 there. */
438
+ /* Darwin sets neither option, so both stay 0 there: a `meta[1]` that reads
439
+ as Not-ECT whatever arrived, which is why the -95 of a marked send, not
440
+ this, is how a program learns ECN is unsupported. */
394
441
  int32_t info[2] = {0, 0};
395
442
  #if defined(__linux__)
396
443
  for (struct cmsghdr *h = CMSG_FIRSTHDR(&m); h != NULL; h = CMSG_NXTHDR(&m, h)) {
@@ -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>
@@ -59,6 +60,11 @@ extern char **environ;
59
60
  #if defined(__APPLE__) && !defined(O_NOFOLLOW)
60
61
  #define O_NOFOLLOW 0x00000100
61
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
62
68
 
63
69
  /* The same spelling runtime.c uses for its own cold paths: a function that
64
70
  reports and exits is never on a path worth optimising for. */
@@ -96,9 +102,16 @@ static NISH_COLD void nish_io_fail(const char *what, const nish_str *path) {
96
102
  * narrower guard would have made it `null` here and split the two compilers
97
103
  * again for the entry a command line can name. `fstat` on the descriptor
98
104
  * rather than `stat` on the path, so the answer is about the file that was
99
- * 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. */
100
113
  nish_str *nish_read_file_or_null(const nish_str *path) {
101
- int fd = open(nish_cpath(path), O_RDONLY);
114
+ int fd = open(nish_cpath(path), O_RDONLY | O_CLOEXEC);
102
115
  if (fd < 0) return 0;
103
116
  struct stat st;
104
117
  off_t len = fstat(fd, &st) == 0 && !S_ISDIR(st.st_mode) ? lseek(fd, 0, SEEK_END) : -1;
@@ -114,7 +127,13 @@ nish_str *nish_read_file_or_null(const nish_str *path) {
114
127
  nish_str *s = nish_alloc_struct(8 + len + 1);
115
128
  uint64_t got = 0;
116
129
  ssize_t n;
117
- 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
+ }
118
137
  close(fd);
119
138
  s->len = got;
120
139
  s->data[got] = 0;
@@ -147,14 +166,21 @@ nish_str *nish_read_file(const nish_str *path) {
147
166
  followed to whatever file it names. `nish file.ts` writes `file.ll` beside
148
167
  its source, so a link planted at that name in a checkout would otherwise
149
168
  truncate any file the user can write. A link among the directories on the
150
- way is still followed. */
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. */
151
174
  static void nish_put_file(const nish_str *path, const nish_str *data, int flags) {
152
- int fd = open(nish_cpath(path), O_WRONLY | O_CREAT | O_NOFOLLOW | flags, 0644);
175
+ int fd = open(nish_cpath(path), O_WRONLY | O_CREAT | O_NOFOLLOW | O_CLOEXEC | flags, 0644);
153
176
  if (fd < 0) nish_io_fail("write ", path);
154
177
  for (uint64_t done = 0; done < data->len;) {
155
178
  ssize_t n = write(fd, data->data + done, data->len - done);
156
- if (n <= 0) nish_io_fail("write ", path);
157
- done += n;
179
+ if (n > 0) {
180
+ done += n;
181
+ } else if (n == 0 || errno != EINTR) {
182
+ nish_io_fail("write ", path);
183
+ }
158
184
  }
159
185
  close(fd);
160
186
  }
@@ -214,24 +240,22 @@ static int32_t nish_spawn_impl(const nish_array *argv, const nish_str *out, cons
214
240
  if (v[i] != s[i]->data) return -1;
215
241
  }
216
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. */
217
246
  posix_spawn_file_actions_t fa;
218
- posix_spawn_file_actions_t *fap = 0;
247
+ if (posix_spawn_file_actions_init(&fa)) return -1;
219
248
  const nish_str *to[2] = { out, err };
220
249
  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
- }
226
250
  /* The child opens the file, not this process: a redirect that this process
227
251
  performed would have to be undone afterwards, and a failed open would
228
252
  leave its own stdout pointing at the file. */
229
- posix_spawn_file_actions_addopen(fap, k + 1, nish_cpath(to[k]), O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, 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);
230
254
  }
231
255
  pid_t pid;
232
256
  int status;
233
- int failed = posix_spawnp(&pid, v[0], fap, 0, v, environ);
234
- 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);
235
259
  if (failed) return -1;
236
260
  if (waitpid(pid, &status, 0) < 0) return -1;
237
261
  return WIFSIGNALED(status) ? 128 + WTERMSIG(status) : WEXITSTATUS(status);
@@ -329,6 +353,10 @@ int64_t nish_monotonic_nanos(void) {
329
353
  return (int64_t)ts.tv_sec * 1000000000 + ts.tv_nsec;
330
354
  }
331
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
+
332
360
  /* ---- The environment (WP19 R1): `getenv(name)`, the one environment read the
333
361
  language has. A driver needs it to honour `CC` the way `scripts/build.sh`
334
362
  does before it spawns that script, which is the whole reason it exists.
@@ -340,8 +368,7 @@ int64_t nish_monotonic_nanos(void) {
340
368
  string with the lifetime every other one has. NULL for an unset variable,
341
369
  which is the language's `string | null`. Contract: nish.h. */
342
370
  nish_str *nish_getenv(const nish_str *name) {
343
- const char *v = getenv(nish_cpath(name));
344
- return v ? nish_str_new(v, strlen(v)) : 0;
371
+ return nish_str_or_null(getenv(nish_cpath(name)));
345
372
  }
346
373
 
347
374
  /* ---- Symlinks (WP19 §5a item 4): `realpathSync(path)`, the one path
@@ -363,10 +390,25 @@ nish_str *nish_getenv(const nish_str *name) {
363
390
  and the stack buffer is gone at the return. NULL when the path does not
364
391
  resolve -- a path that does not exist included -- which is the language's
365
392
  `string | null` rather than an error, because the caller is asking whether
366
- 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. */
367
404
  nish_str *nish_realpath(const nish_str *path) {
405
+ #if defined(__wasi__) && !defined(DT_SOCK)
406
+ (void)path;
407
+ return 0;
408
+ #else
368
409
  char buf[PATH_MAX];
369
- return realpath(nish_cpath(path), buf) ? nish_str_new(buf, strlen(buf)) : 0;
410
+ return nish_str_or_null(realpath(nish_cpath(path), buf));
411
+ #endif
370
412
  }
371
413
 
372
414
  /* ---- What machine this is (WP14 §7a): `process.platform` and `process.arch`,
@@ -119,6 +119,7 @@ void nish_array_grow(nish_array *a, uint64_t elem_size) {
119
119
  void nish_panic_index(uint64_t idx, uint64_t len) { (void)idx; (void)len; __builtin_trap(); }
120
120
  void nish_panic_slice(int64_t s, int64_t e, int64_t len) { (void)s; (void)e; (void)len; __builtin_trap(); }
121
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(); }
122
123
  void nish_exit(int32_t code) { (void)code; __builtin_trap(); }
123
124
  /* A panic's message (`panic`, `expect`, a failed range entry) has no stderr to
124
125
  * go to here; the `nish_exit` that follows it is the trap. An exported function
package/runtime/runtime.c CHANGED
@@ -79,8 +79,12 @@ _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
90
  /* A function rather than a macro around `nish_die`: seven call sites each
@@ -214,7 +218,7 @@ nish_str *nish_str_concat(const nish_str *a, const nish_str *b) {
214
218
  s->len = len;
215
219
  memcpy(s->data, a->data, a->len);
216
220
  memcpy(s->data + a->len, b->data, b->len);
217
- s->data[s->len] = 0;
221
+ s->data[len] = 0;
218
222
  return s;
219
223
  }
220
224
 
@@ -262,9 +266,22 @@ int64_t nish_str_index_of(const nish_str *s, const nish_str *sub) {
262
266
  }
263
267
 
264
268
  /* `console.log` / `console.error` / `write` / `writeError` and the message of
265
- `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. */
266
281
  void nish_write(const nish_str *s, int32_t fd, _Bool newline) {
267
- (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;
268
285
  if (newline) (void)!write(fd, "\n", 1);
269
286
  }
270
287
  /* console.log(s) */
@@ -1203,6 +1220,20 @@ __attribute__((noinline)) void nish_array_grow(nish_array *a, uint64_t elem_size
1203
1220
  a->cap = cap;
1204
1221
  }
1205
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
+
1206
1237
  void nish_panic_index(uint64_t idx, uint64_t len) {
1207
1238
  dprintf(2, "index out of range: %" PRIu64 " >= %" PRIu64 "\n", idx, len);
1208
1239
  _exit(1);
@@ -1221,7 +1252,14 @@ void nish_panic_slice(int64_t start, int64_t end, int64_t len) {
1221
1252
  _exit(1);
1222
1253
  }
1223
1254
 
1224
- /* ---- Checked division (Rust semantics): the failed-check path */
1225
- void nish_panic_div(_Bool by_zero) {
1226
- nish_die(by_zero ? "attempt to divide by zero\n" : "attempt to divide with overflow\n");
1227
- }
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); }
package/runtime/shim.mjs CHANGED
@@ -123,6 +123,52 @@ export function ctEq(a, b) {
123
123
  return (a ^ b) === 0 ? 0xffffffff : 0;
124
124
  }
125
125
 
126
+ /**
127
+ * `nish:secret` (docs/LANGUAGE.md, "Secrets"). Natively the compiler holds a
128
+ * `Secret` to its rules and `wipe` is a volatile store of zeros; here the rules
129
+ * have already been checked by the compiler or by nobody, so a `Secret` is a
130
+ * plain wrapper and `wipe` zero-fills what it holds. `runtime/nish.mjs`
131
+ * resolves the `nish:secret` specifier to this module, so these are its
132
+ * exports by name. A zero-fill under Node promises nothing about copies the
133
+ * engine made, which is the native wipe's whole point and is not claimed here.
134
+ */
135
+ export class Secret {
136
+ constructor(value) {
137
+ this.value = value;
138
+ }
139
+ }
140
+
141
+ export function secret(value) {
142
+ return new Secret(value);
143
+ }
144
+
145
+ export function expose(s, f) {
146
+ return f(s.value);
147
+ }
148
+
149
+ export function exposeWith(s, arg, f) {
150
+ return f(s.value, arg);
151
+ }
152
+
153
+ /** Zero an array of integers, or every integer field of a record: `0` for a number, `0n` for an `i64` or `u64`. */
154
+ function zeroFill(target) {
155
+ if (Array.isArray(target) || ArrayBuffer.isView(target)) {
156
+ for (let i = 0; i < target.length; i++) {
157
+ target[i] = typeof target[i] === "bigint" ? 0n : 0;
158
+ }
159
+ return;
160
+ }
161
+ for (const key of Object.keys(target)) {
162
+ const field = target[key];
163
+ if (typeof field === "bigint") target[key] = 0n;
164
+ else if (typeof field === "number") target[key] = 0;
165
+ }
166
+ }
167
+
168
+ export function wipe(target) {
169
+ zeroFill(target instanceof Secret ? target.value : target);
170
+ }
171
+
126
172
  /** Wrap a BigInt to the i64 range: every i64 `+ - * /` and unary minus goes through here. */
127
173
  export function wrapI64(x) {
128
174
  return BigInt.asIntN(64, x);
@@ -497,9 +543,22 @@ export function readFileBytesSync(path) {
497
543
  }
498
544
  }
499
545
 
546
+ /**
547
+ * The flags `runtime-os.c` opens a file it writes with, `O_NOFOLLOW` included
548
+ * (docs/security/runtime.md, RT-4): a symbolic link as the last component of
549
+ * the path is refused rather than followed, so `writeFileSync`,
550
+ * `appendFileSync` and `spawnSyncTo` fail here where they fail natively. Node's
551
+ * `"w"` and `"a"` follow the link. A link among the directories on the way is
552
+ * still followed, as it is natively. Windows has no `O_NOFOLLOW`, and there the
553
+ * flag is 0.
554
+ */
555
+ const { O_WRONLY, O_CREAT, O_TRUNC, O_APPEND, O_NOFOLLOW = 0 } = fs.constants;
556
+ const WRITE_FLAGS = O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW;
557
+ const APPEND_FLAGS = O_WRONLY | O_CREAT | O_APPEND | O_NOFOLLOW;
558
+
500
559
  export function writeFileSync(path, data) {
501
560
  try {
502
- fs.writeFileSync(path, data, "utf8");
561
+ fs.writeFileSync(path, data, { encoding: "utf8", flag: WRITE_FLAGS });
503
562
  } catch {
504
563
  ioFail("write", path);
505
564
  }
@@ -507,7 +566,7 @@ export function writeFileSync(path, data) {
507
566
 
508
567
  export function appendFileSync(path, data) {
509
568
  try {
510
- fs.appendFileSync(path, data, "utf8");
569
+ fs.appendFileSync(path, data, { encoding: "utf8", flag: APPEND_FLAGS });
511
570
  } catch {
512
571
  ioFail("write", path);
513
572
  }
@@ -623,8 +682,9 @@ export function getenv(name) {
623
682
  *
624
683
  * `out` and `err` are paths for the child's stdout and stderr, and an **empty**
625
684
  * string leaves that stream inherited. Each file is created or truncated at
626
- * 0644, which is what `"w"` asks `open(2)` for (`O_WRONLY | O_CREAT | O_TRUNC`)
627
- * and what the runtime's file actions ask for. The descriptors opened here are
685
+ * 0644 with `WRITE_FLAGS`, which is what the runtime's file actions ask for:
686
+ * `O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW`, so a symbolic link at the path
687
+ * is refused and the answer is -1, on both sides. The descriptors opened here are
628
688
  * closed again whichever way the child went; natively the child opens them and
629
689
  * its exit drops them.
630
690
  */
@@ -633,7 +693,7 @@ function spawnImpl(argv, out, err) {
633
693
  const opened = [];
634
694
  const stream = (target) => {
635
695
  if (target.length === 0) return "inherit";
636
- const fd = fs.openSync(target, "w", 0o644);
696
+ const fd = fs.openSync(target, WRITE_FLAGS, 0o644);
637
697
  opened.push(fd);
638
698
  return fd;
639
699
  };
@@ -696,6 +756,51 @@ export function statMtimeSync(path) {
696
756
  return st.mtimeMs;
697
757
  }
698
758
 
759
+ /**
760
+ * The RT-9 owner checks (#386): `lstatOwnerModeSync(path)` is the owner's uid
761
+ * in the high 32 bits and `st_mode` in the low 32 of one `lstat`, so a
762
+ * symbolic link answers for itself, or -1 when the path does not resolve or
763
+ * holds a NUL; `geteuid()` is the effective uid; `isExecutableSync(path)` is
764
+ * `access(path, X_OK)`. Each answers what `runtime-host.c` answers, the two
765
+ * `i64`s as BigInts because that is how an `i64` is held on this side. Node's
766
+ * `fs` throws on a NUL in a path where the runtime names nothing, so the
767
+ * `catch` is what makes the two agree there too.
768
+ */
769
+ export function lstatOwnerModeSync(path) {
770
+ try {
771
+ const st = fs.lstatSync(path);
772
+ return BigInt.asIntN(64, (BigInt(st.uid >>> 0) << 32n) | BigInt(st.mode >>> 0));
773
+ } catch {
774
+ return -1n;
775
+ }
776
+ }
777
+
778
+ export function geteuid() {
779
+ return BigInt(process.geteuid());
780
+ }
781
+
782
+ export function isExecutableSync(path) {
783
+ try {
784
+ fs.accessSync(path, fs.constants.X_OK);
785
+ return true;
786
+ } catch {
787
+ return false;
788
+ }
789
+ }
790
+
791
+ /**
792
+ * `secureZero(bytes)` (#385): every element set to zero, which is what the
793
+ * native `nish_wipe` leaves. Nothing here can drop a store to an array the
794
+ * caller still holds, so a loop is the whole of it. What it cannot promise is
795
+ * what the native build does: a collector that moved the array may have left a
796
+ * copy of the old bytes behind, and JavaScript has no way to reach it.
797
+ */
798
+ export function secureZero(bytes) {
799
+ for (let i = 0; i < bytes.length; i++) {
800
+ bytes[i] = 0;
801
+ }
802
+ }
803
+
699
804
  /**
700
805
  * Node's own fill, taken before `runtime/nish.mjs` puts the one below in its
701
806
  * place on the same object: `webcrypto` is the global `crypto`.
@@ -807,3 +912,7 @@ export function arenaUsed() { return 0; }
807
912
  export function arenaMark() { return 0; }
808
913
  export function arenaRelease() {}
809
914
  export function arenaReset() {}
915
+ /** `using a = arena()`: a disposable whose disposal does nothing, because there is no arena to release. */
916
+ export function arena() {
917
+ return { [Symbol.dispose]() {} };
918
+ }
package/scripts/build.sh CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  # Build an Nish .ll module (plus optional C sources) into a native binary.
3
3
  #
4
- # scripts/build.sh <module.ll> [more .ll/.c files...] -o <out> [--profile debug|speed|size|wasm]
4
+ # scripts/build.sh <module.ll> [more .ll/.c files...] -o <out> [--profile debug|speed|size|wasm|wasi|napi]
5
5
  #
6
6
  # The C runtime is five translation units and is named as one: an input
7
7
  # <dir>/runtime.c also compiles <dir>/runtime-os.c, the half that wraps the
@@ -20,8 +20,6 @@
20
20
  # load it from Node. Add runtime/runtime-wasm.c to the inputs when a
21
21
  # function uses arrays (the arena and the array cold paths, no libc);
22
22
  # strings and I/O still need a WASI runtime and are not available.
23
- # wasm wasm32 freestanding module exporting every non-internal function
24
- # (for modules that do not use the C runtime); load it from Node.
25
23
  # wasi wasm32-wasi command module: runtime.c linked against wasi-libc, so
26
24
  # string programs run under any WASI host (`_start` runs `main`;
27
25
  # `node examples/wasi-host.mjs app.wasm args...`). Needs a WASI