@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
package/runtime/nish.mjs CHANGED
@@ -16,14 +16,14 @@
16
16
  * That is enough for a real and useful subset — **f64 mode**, where JavaScript's
17
17
  * `+ - * / %` on doubles *are* `fadd/fsub/fmul/fdiv/frem` and `Math.*` are the
18
18
  * same IEEE operations, so the arithmetic needs no help. It is not enough for
19
- * i32 mode, and it never will be: `number` is a wrapping 32-bit integer there
20
- * and every operator would have to change.
19
+ * i32 mode, and it never will be: `number` is a 32-bit integer there, whose
20
+ * signed overflow panics, and every operator would have to change.
21
21
  *
22
22
  * `docs/RUN_UNDER_NODE.md` states the overlap and the whole list of what stays
23
23
  * divergent. The short version, because it belongs next to the code too:
24
24
  *
25
- * - **i32 mode is out.** Wrapping, truncating `/`, and the divide-by-zero
26
- * panic are all operator-level and cannot be reached from here.
25
+ * - **i32 mode is out.** The overflow panic, truncating `/`, and the
26
+ * divide-by-zero panic are all operator-level and cannot be reached from here.
27
27
  * - **`s.length` is UTF-16 units under Node** and UTF-8 bytes natively, and
28
28
  * so is every string offset. ASCII agrees; nothing else does.
29
29
  * - **`a[i]` is unchecked**: out of range is `undefined` here and an exit-1
@@ -109,6 +109,8 @@ for (const name of [
109
109
  "netWrite",
110
110
  "netShutdown",
111
111
  "netClose",
112
+ "tcpConnect",
113
+ "connectResult",
112
114
  "udpBind",
113
115
  "udpSendTo",
114
116
  "udpRecvFrom",
@@ -174,6 +176,49 @@ provide("bitsToF64", shim.bitsToF64);
174
176
  // agree with a native run; the timing does not, and is not claimed here.
175
177
  provide("ctSelect", shim.ctSelect);
176
178
  provide("ctEq", shim.ctEq);
179
+ // `secureZero` (#385): the bytes are zero afterwards here too, though what a
180
+ // collector may have copied before is out of reach.
181
+ provide("secureZero", shim.secureZero);
182
+ // The RT-9 owner checks (#386): one `lstat`, the effective uid and
183
+ // `access(X_OK)`, answered as `runtime-host.c` answers them.
184
+ provide("lstatOwnerModeSync", shim.lstatOwnerModeSync);
185
+ provide("geteuid", shim.geteuid);
186
+ provide("isExecutableSync", shim.isExecutableSync);
187
+
188
+ // `nish:unsafe`. The element access is JavaScript's own, which never checks
189
+ // anything; the wraps are the two's-complement ones the native instructions
190
+ // perform: `| 0` and `Math.imul` at `i32`, and `BigInt.asIntN(64, ...)` on
191
+ // the BigInt an `i64` is here. Pure functions of their operands, so a program
192
+ // that stays in range agrees with a native run.
193
+ const wrapAt = (a, b, wide, narrow) => (typeof a === "bigint" ? BigInt.asIntN(64, wide(a, b)) : narrow(a, b));
194
+ provide("uncheckedGet", (xs, i) => xs[i]);
195
+ provide("uncheckedSet", (xs, i, v) => {
196
+ xs[i] = v;
197
+ });
198
+ provide("wrappingAdd", (a, b) =>
199
+ wrapAt(
200
+ a,
201
+ b,
202
+ (x, y) => x + y,
203
+ (x, y) => (x + y) | 0
204
+ )
205
+ );
206
+ provide("wrappingSub", (a, b) =>
207
+ wrapAt(
208
+ a,
209
+ b,
210
+ (x, y) => x - y,
211
+ (x, y) => (x - y) | 0
212
+ )
213
+ );
214
+ provide("wrappingMul", (a, b) =>
215
+ wrapAt(
216
+ a,
217
+ b,
218
+ (x, y) => x * y,
219
+ (x, y) => Math.imul(x, y)
220
+ )
221
+ );
177
222
 
178
223
  // `Result`. Everything works but `orReturn`, which needs the caller's control
179
224
  // flow and therefore the rewriter; see the header.
@@ -200,15 +245,34 @@ provide("Arena", {
200
245
  reset: shim.arenaReset,
201
246
  used: shim.arenaUsed,
202
247
  });
248
+ // `using a = arena()` releases nothing here either: what the block allocated
249
+ // is the garbage collector's, and the disposal is a no-op.
250
+ provide("arena", shim.arena);
203
251
 
204
252
  // The standard library. A program imports it as `nish/<module>`, which the
205
253
  // compiler resolves to `std/<module>.ts` beside itself; this does the same for
206
254
  // Node, relative to this file rather than to the program, so the same
207
255
  // specifier works from any directory. Only the `nish/` package is answered
208
256
  // here, and every other specifier goes to Node as it was written.
257
+ //
258
+ // `nish:secret` is answered here as well, because the standard library imports
259
+ // it (`std/crypto/p256.ts` and its neighbours): its names are
260
+ // `runtime/shim.mjs`'s, where a `Secret` is a plain wrapper and `wipe`
261
+ // zero-fills it. `nish:unsafe` is answered by a module that hands back the
262
+ // globals installed above: its functions are globals of this module either
263
+ // way, and the import is what the compiler requires a program to write.
264
+ const UNSAFE_EXPORTS = ["uncheckedGet", "uncheckedSet", "wrappingAdd", "wrappingSub", "wrappingMul"];
265
+ const unsafeSource = UNSAFE_EXPORTS.map((name) => `export const ${name} = globalThis.${name};`).join("\n");
209
266
  registerHooks({
210
- resolve: (specifier, context, next) =>
211
- specifier.startsWith("nish/")
267
+ resolve: (specifier, context, next) => {
268
+ if (specifier === "nish:secret") {
269
+ return next(new URL("./shim.mjs", import.meta.url).href, context);
270
+ }
271
+ if (specifier === "nish:unsafe") {
272
+ return { url: `data:text/javascript,${encodeURIComponent(unsafeSource)}`, shortCircuit: true };
273
+ }
274
+ return specifier.startsWith("nish/")
212
275
  ? next(new URL(`../std/${specifier.slice("nish/".length)}.ts`, import.meta.url).href, context)
213
- : next(specifier, context),
276
+ : next(specifier, context);
277
+ },
214
278
  });
@@ -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
@@ -129,46 +156,65 @@ double nish_stat_mtime(const nish_str *path) {
129
156
  * async-signal-safe. The write end is non-blocking, so a thousand unread
130
157
  * signals lose the newest rather than wedge the handler, and `errno` is put
131
158
  * back for the code the signal interrupted. The descriptor is made once and
132
- * every later `signalFd()` answers the same one. */
133
- static int nish_signal_read_end = -1;
134
- 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;
135
171
 
136
172
  static void nish_on_signal(int sig) {
137
173
  int saved = errno;
138
174
  unsigned char b = (unsigned char)sig;
139
- (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);
140
177
  errno = saved;
141
178
  }
142
179
 
143
180
  int32_t nish_signal_fd(void) {
144
- if (nish_signal_read_end >= 0) return nish_signal_read_end;
145
- int p[2];
181
+ int64_t cur = __atomic_load_n(&nish_signal_pipe, __ATOMIC_ACQUIRE);
182
+ if (cur < 0) {
183
+ int p[2];
146
184
  #if defined(__linux__)
147
- /* Both ends close-on-exec from the moment they exist, so a child a sibling
148
- thread spawns meanwhile cannot inherit either. */
149
- 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;
150
188
  #else
151
- /* Darwin has no `pipe2`, so there is a window between `pipe` and the two
152
- `fcntl`s in which a child spawned by another thread inherits the ends. */
153
- if (pipe(p) != 0) return -1;
154
- fcntl(p[0], F_SETFD, FD_CLOEXEC);
155
- 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);
156
194
  #endif
157
- fcntl(p[1], F_SETFL, O_NONBLOCK);
158
- nish_signal_write_end = p[1];
159
- struct sigaction sa;
160
- sa.sa_handler = nish_on_signal;
161
- sa.sa_flags = SA_RESTART;
162
- sigemptyset(&sa.sa_mask);
163
- sigaction(SIGINT, &sa, 0);
164
- sigaction(SIGTERM, &sa, 0);
165
- 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);
166
212
  }
167
213
 
168
214
  /* Block until a signal's byte arrives and answer it: 15 or 2, or -1 for any
169
215
  `fd` that is not the signal descriptor and for a read that fails. */
170
216
  int32_t nish_read_signal(int32_t fd) {
171
- if (fd != nish_signal_read_end) return -1;
217
+ if (fd != (int32_t)(__atomic_load_n(&nish_signal_pipe, __ATOMIC_ACQUIRE) >> 32)) return -1;
172
218
  unsigned char b;
173
219
  ssize_t n;
174
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;
@@ -229,18 +239,24 @@ int32_t nish_tcp_listen(const nish_str *host, int32_t port, int32_t backlog) {
229
239
  return nish_net_bound(host, port, SOCK_STREAM, backlog);
230
240
  }
231
241
 
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) {
242
+ /* A socket address of `n` bytes into the caller's 18-byte form. An IPv4 peer
243
+ is written mapped, and the port is already in network order, which is the
244
+ form's big-endian. Anything else -- no address at all, which is what
245
+ `recvmsg` reports on a connected or Unix socket, or a family that is not
246
+ IP -- is eighteen zeros: those bytes of `s` were never written, and copying
247
+ them handed the caller whatever the stack held (docs/security/runtime.md,
248
+ RT-6). */
249
+ static void nish_net_put(nish_array *out, const nish_sockaddr *s, socklen_t n) {
236
250
  unsigned char *p = (unsigned char *)out->data;
237
- uint16_t port = s->v6.sin6_port;
238
- if (s->sa.sa_family == AF_INET) {
251
+ uint16_t port = 0;
252
+ memset(p, 0, NISH_ADDR_BYTES);
253
+ if (n >= sizeof s->v4 && s->sa.sa_family == AF_INET) {
239
254
  memcpy(p, nish_mapped_prefix, 12);
240
255
  memcpy(p + 12, &s->v4.sin_addr, 4);
241
256
  port = s->v4.sin_port;
242
- } else {
257
+ } else if (n >= sizeof s->v6 && s->sa.sa_family == AF_INET6) {
243
258
  memcpy(p, &s->v6.sin6_addr, 16);
259
+ port = s->v6.sin6_port;
244
260
  }
245
261
  memcpy(p + 16, &port, 2);
246
262
  }
@@ -259,10 +275,45 @@ int32_t nish_tcp_accept(int32_t fd, nish_array *peer) {
259
275
  int c = nish_net_setup(accept(fd, &s.sa, &n));
260
276
  #endif
261
277
  if (c < 0) return nish_net_fail();
262
- nish_net_put(peer, &s);
278
+ nish_net_put(peer, &s, n);
263
279
  return c;
264
280
  }
265
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
+
266
317
  /* `netRead(fd, buf, off, len)`: at most `len` bytes into `buf` from `off`.
267
318
  The compiled call has already checked `0 <= off <= off + len <=
268
319
  buf.length`, unless the program was built with `--unchecked-indexing`. */
@@ -384,7 +435,9 @@ int32_t nish_udp_recv_from(int32_t fd, nish_array *buf, int64_t off, int64_t len
384
435
  .msg_name = &s, .msg_namelen = sizeof s, .msg_iov = &iov, .msg_iovlen = 1, .msg_control = c.b, .msg_controllen = sizeof c};
385
436
  ssize_t got = recvmsg(fd, &m, 0);
386
437
  if (got < 0) return nish_net_fail();
387
- /* 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. */
388
441
  int32_t info[2] = {0, 0};
389
442
  #if defined(__linux__)
390
443
  for (struct cmsghdr *h = CMSG_FIRSTHDR(&m); h != NULL; h = CMSG_NXTHDR(&m, h)) {
@@ -399,7 +452,7 @@ int32_t nish_udp_recv_from(int32_t fd, nish_array *buf, int64_t off, int64_t len
399
452
  }
400
453
  #endif
401
454
  memcpy(meta->data, info, sizeof info);
402
- nish_net_put(from, &s);
455
+ nish_net_put(from, &s, m.msg_namelen);
403
456
  return (int32_t)got;
404
457
  }
405
458