@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
package/INSTALL.md CHANGED
@@ -61,8 +61,9 @@ The `wasi` profile links the runtime against wasi-libc so that whole programs
61
61
  needs a WASI sysroot and compiler-rt's wasm32 builtins next to your clang:
62
62
 
63
63
  ```bash
64
- # Ubuntu / Debian: the packaged sysroot lands in /usr/lib/wasi-sysroot
64
+ # Ubuntu / Debian: the packaged sysroot lands in /usr (/usr/include/wasm32-wasi, /usr/lib/wasm32-wasi)
65
65
  sudo apt-get install -y wasi-libc libclang-rt-18-dev-wasm32
66
+ export WASI_SYSROOT=/usr # add to your shell profile
66
67
 
67
68
  # Any platform: wasi-sdk's sysroot and builtins tarballs (versions that match your clang)
68
69
  curl -L -o - https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-24/wasi-sysroot-24.0.tar.gz | tar -xz -C /opt
@@ -74,7 +75,11 @@ export WASI_SYSROOT=/opt/wasi-sysroot-24.0 # add to your shell profile
74
75
  `/usr/lib/wasi-sysroot`, `/opt/wasi-sdk/share/wasi-sysroot` and
75
76
  `/usr/share/wasi-sysroot`, and for `libclang_rt.builtins-wasm32.a` in clang's
76
77
  resource directory, next to the sysroot (as the tarball above unpacks it),
77
- in `<sysroot>/lib/wasm32-wasi/`, or at `WASI_BUILTINS=<file>`. Then:
78
+ in `<sysroot>/lib/wasm32-wasi/`, or at `WASI_BUILTINS=<file>`.
79
+
80
+ The packaged wasi-libc predates wasi-libc's `realpath`, so a module linked
81
+ against it answers `realpathSync` with `null` for every path. Use wasi-sdk's
82
+ sysroot (22 or later) when a program needs `realpathSync` to resolve. Then:
78
83
 
79
84
  ```bash
80
85
  nish examples/argv.ts --link build/argv.wasm --profile wasi
@@ -218,7 +223,8 @@ routes are
218
223
  # in the glibc container, where build/nish runs
219
224
  build/nish app.ts -o app.ll
220
225
  # on the musl host, with its own clang and the runtime from this repository
221
- clang app.ll runtime/runtime.c runtime/runtime-os.c -lm -o app
226
+ clang app.ll runtime/runtime.c runtime/runtime-os.c runtime/runtime-parallel.c \
227
+ runtime/runtime-host.c runtime/runtime-net.c -lm -o app
222
228
  ```
223
229
 
224
230
  `--link`ing inside the container is the mistake to avoid: it shells out to the
@@ -285,21 +291,20 @@ to the machine that built it.
285
291
 
286
292
  The last column is there because a release is a past event and a workflow is
287
293
  not. `release.yml` builds all four, but a release already published cannot grow
288
- an asset: **v0.2.0, the current release, attaches `x86_64-linux` only**. Those
294
+ an asset: **the releases before v0.4.0 attach `x86_64-linux` only**. Those
289
295
  versions are not prose — they are `attachedSince` in
290
296
  [`.github/seed-targets.json`](https://github.com/amritk/nish/blob/main/.github/seed-targets.json),
291
297
  the same file the release workflow builds its matrix from, so this table and
292
298
  the assets cannot drift apart without a test failing.
293
299
 
294
- **The three new rows say v0.4.0 rather than v0.3.0 on purpose.** The workflow
295
- builds all four now, but nothing in this repository has ever run on macOS or on
296
- ARM Linux, and a row that has never run is not something to put in front of a
297
- release: `release` needs the whole matrix, so one red row blocks the publish.
298
- They wait for a release after the run that exercises them. The two macOS rows
299
- wait on a second thing as well — the bootstrap's `stage3 == stage2` check does
300
- not hold as a raw byte comparison under `ld64`, and which bytes actually differ
301
- has not been measured on real hardware, so a darwin row may turn up work rather
302
- than a green tick. `scripts/verify-binaries.sh`'s header is the long version.
300
+ **The three newer rows say v0.4.0 rather than v0.3.0 on purpose.** Nothing in
301
+ this repository had run on macOS or on ARM Linux before, and a row that has
302
+ never run is not something to put in front of a release: `release` needs the
303
+ whole matrix, so one red row blocks the publish. So they waited until a run on
304
+ their own hardware had exercised them. The two macOS rows waited on a second
305
+ thing as well: the bootstrap's `stage3 == stage2` check did not hold as a raw
306
+ byte comparison under `ld64` until both stages were linked at one output path.
307
+ `scripts/verify-binaries.sh`'s header is the long version.
303
308
 
304
309
  Check [the releases page](https://github.com/amritk/nish/releases/latest) for
305
310
  what a given version actually carries; on a platform whose row has not shipped
@@ -307,15 +312,15 @@ yet, take the npm package above, or build from source below.
307
312
 
308
313
  ```bash
309
314
  # pick the row above that matches `uname -s` and `uname -m`
310
- curl -LO https://github.com/amritk/nish/releases/download/v0.2.0/nish-0.2.0-x86_64-linux.tar.gz
311
- tar -xzf nish-0.2.0-x86_64-linux.tar.gz
312
- nish-0.2.0-x86_64-linux/bin/nish --version
315
+ curl -LO https://github.com/amritk/nish/releases/download/v0.16.0/nish-0.16.0-x86_64-linux.tar.gz
316
+ tar -xzf nish-0.16.0-x86_64-linux.tar.gz
317
+ nish-0.16.0-x86_64-linux/bin/nish --version
313
318
  ```
314
319
 
315
320
  Both URLs name the version rather than using GitHub's version-neutral
316
321
  `/releases/latest/download/` form, because that form needs the asset's exact
317
- file name and every asset name carries the version in it. **v0.2.0 is the
318
- current release**; v0.1.1 before it was the first one with a binary attached.
322
+ file name and every asset name carries the version in it. The example names
323
+ v0.16.0; v0.1.1 was the first release with a binary attached.
319
324
  The `v0.1.0` tag exists but has no release behind it and no assets, so every
320
325
  `v0.1.0` download URL is a 404 — the tag was pushed by a workflow, and GitHub
321
326
  raises no event for that, so nothing ever built it
@@ -333,8 +338,7 @@ find `scripts/build.sh`.
333
338
 
334
339
  It still needs `clang` and `lld` on `PATH` for `--link` (§1), because linking
335
340
  is the C toolchain's job, not the compiler's; what it does not need is Node.
336
- x86_64 Linux is the only platform built today — on anything else, take the
337
- `.tgz` above or build from a checkout.
341
+ On a platform outside the table, build from a checkout.
338
342
 
339
343
  From a checkout:
340
344
 
@@ -354,7 +358,7 @@ it into `build/seed/`; `NISH_BOOTSTRAP=<path>` names one you already have and
354
358
  skips the download. §2a has what the build does with it.
355
359
 
356
360
  The published package ships `bin/` (the `nish` command, which is a launcher),
357
- `runtime/` (the C runtime — two translation units and their header),
361
+ `runtime/` (the C runtime — five translation units and their header),
358
362
  `scripts/build.sh` (the link pipeline) and `std/` (the standard library). The
359
363
  compiler itself is not in it: it arrives as one prebuilt
360
364
  `@amritk/nish-<asset>` package per platform, and `bin/nish` hands over to
@@ -364,22 +368,27 @@ working directory — and the binary runs from inside its own platform package
364
368
  for exactly that reason.
365
369
 
366
370
  Building the IR yourself rather than through `--link` means naming the runtime
367
- on the `clang` line, and it is two files:
371
+ on the `clang` line, and it is five files:
368
372
 
369
373
  ```bash
370
- clang app.ll runtime/runtime.c runtime/runtime-os.c -lm -o app
374
+ clang app.ll runtime/runtime.c runtime/runtime-os.c runtime/runtime-parallel.c \
375
+ runtime/runtime-host.c runtime/runtime-net.c -lm -o app
371
376
  ```
372
377
 
373
- `runtime.c` is the half every program touches — the arena, strings, arrays,
374
- number formatting, the panics — and `runtime-os.c` is the half that wraps the
375
- system calls: files, directories, subprocesses, `getenv`, the monotonic clock.
376
- They are separate so that each carries its own measured size ceiling
377
- ([docs/wp7-runtime.md](wp7-runtime.md)); nothing in the core calls into the
378
- system-call half, so an older line that names `runtime.c` alone still links a
379
- program that reads no files and spawns nothing. `scripts/build.sh` compiles
380
- `runtime-os.c` beside any `runtime.c` it is handed, so a build that goes
381
- through it — every `--link`, and every `--profile` recipe in these documents —
382
- needs to name only the one.
378
+ `runtime.c` is the part every program touches — the arena, strings, arrays,
379
+ number formatting, the panics — and `runtime-os.c` wraps the system calls:
380
+ files, directories, subprocesses, `getenv`, the monotonic clock.
381
+ `runtime-parallel.c` divides work across threads for `nish/threads` (a
382
+ program built with `--threads`, as every importer of `nish/threads` is, also
383
+ needs `-DNISH_THREADS=1 -pthread` on the line), `runtime-host.c` holds the
384
+ wall clock, entropy, file times and signals, and `runtime-net.c` the sockets
385
+ of `nish:net`. They are separate so that each
386
+ carries its own measured size ceiling ([docs/wp7-runtime.md](wp7-runtime.md));
387
+ nothing in the core calls into the others, so a line that names only
388
+ `runtime.c` and `runtime-os.c` still links a program that uses none of the
389
+ other three. `scripts/build.sh` compiles the other four beside any
390
+ `runtime.c` it is handed, so a build that goes through it — every `--link`,
391
+ and every `--profile` recipe in these documents — needs to name only the one.
383
392
 
384
393
  ## 2a. What `npm run build` does
385
394
 
@@ -402,7 +411,7 @@ release is expected to move it. `--stages 1` stops one link sooner.
402
411
  `build/nish` is then the compiler you run: it takes the same `-o`, `--link`
403
412
  and `--profile` spellings as an installed `nish`, because it is the same
404
413
  program, and makes every directory in the way of the IR, a sidecar or the
405
- binary itself. It looks for `scripts/build.sh` and the two `runtime/*.c` files
414
+ binary itself. It looks for `scripts/build.sh` and the `runtime/*.c` files
406
415
  one level up from wherever it was invoked (and one level up from the file a
407
416
  symbolic link to it names), so it wants a checkout or an installed package
408
417
  around it the way `nish` does. It falls back to the working directory only
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.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "The @amritk/nish native compiler for Linux on x86_64",
5
5
  "license": "MIT",
6
6
  "os": [
package/runtime/nish.d.ts CHANGED
@@ -112,9 +112,10 @@ declare function Err<T, E>(error: E): Result<T, E>;
112
112
 
113
113
  /**
114
114
  * The disposable protocol `using` reads (WP29 P2, docs/wp29-thread-surface.md
115
- * §5): declared here so that a program using `nish/threads`'s scope needs no
116
- * `"ESNext.Disposable"` in its `lib`. `nish` itself takes `using` only for a
117
- * `scope()`, and `[Symbol.dispose]` only in `nish/threads`.
115
+ * §5): declared here so that a program using `nish/threads`'s scope or
116
+ * `arena()` needs no `"ESNext.Disposable"` in its `lib`. `nish` itself takes
117
+ * `using` only for a `scope()` or an `arena()`, and `[Symbol.dispose]` only in
118
+ * `nish/threads`.
118
119
  */
119
120
  interface SymbolConstructor {
120
121
  readonly dispose: unique symbol;
@@ -172,6 +173,39 @@ declare function bitsToF64(bits: i64): f64;
172
173
  declare function ctSelect<T extends u32 | u64>(mask: T, a: T, b: T): T;
173
174
  /** All-ones of the operands' type when `a === b`, zero otherwise, without a branch. */
174
175
  declare function ctEq<T extends u32 | u64>(a: T, b: T): T;
176
+ /**
177
+ * Every byte of `bytes` set to zero, in stores neither `-O2` nor `-flto` may
178
+ * drop as dead (#385): how a key, a secret scalar or another secret is cleared
179
+ * once it is used. A statement.
180
+ */
181
+ declare function secureZero(bytes: u8[]): void;
182
+ /**
183
+ * One `lstat` of `path`, so a symbolic link answers for itself: the owner's uid
184
+ * in the high 32 bits and `st_mode` in the low 32, or -1 when the path does not
185
+ * resolve (#386).
186
+ */
187
+ declare function lstatOwnerModeSync(path: string): i64;
188
+ /** The effective user id, the one to compare an owner from `lstatOwnerModeSync` with. */
189
+ declare function geteuid(): i64;
190
+ /** Whether the real user may run `path`: `access(path, X_OK)`. */
191
+ declare function isExecutableSync(path: string): boolean;
192
+
193
+ // ---- Unsafe (`nish:unsafe`) --------------------------------------------------
194
+ //
195
+ // Declared as globals so `tsc` reads a program that imports them, as every
196
+ // `nish:` export is, but `nish` refuses a call to any of the five that is not
197
+ // imported from `nish:unsafe`: the import is the module's visible opt-in.
198
+
199
+ /** `xs[i]` with no bounds check: undefined behaviour when `i` is out of range. */
200
+ declare function uncheckedGet<T extends number>(xs: readonly T[], i: i32): T;
201
+ /** `xs[i] = v` with no bounds check: undefined behaviour when `i` is out of range. */
202
+ declare function uncheckedSet<T extends number>(xs: T[], i: i32, v: T): void;
203
+ /** `a + b` wrapped to the operands' width, `i32` or `i64`: defined for every pair. */
204
+ declare function wrappingAdd<T extends i32 | i64>(a: T, b: T): T;
205
+ /** `a - b` wrapped to the operands' width. */
206
+ declare function wrappingSub<T extends i32 | i64>(a: T, b: T): T;
207
+ /** `a * b` wrapped to the operands' width. */
208
+ declare function wrappingMul<T extends i32 | i64>(a: T, b: T): T;
175
209
 
176
210
  // ---- Streams and files (globals: Nish has no package resolution) ---------
177
211
 
@@ -200,7 +234,14 @@ declare function readFileSyncOrNull(path: string): string | null;
200
234
  * `readFileSyncOrNull` answers `null` for.
201
235
  */
202
236
  declare function readFileBytesSync(path: string): u8[] | null;
237
+ /**
238
+ * The file's contents replaced by `data`, created at 0644 when missing. A
239
+ * symbolic link as the last component of the path is refused rather than
240
+ * followed, natively and under Node alike, and a path that cannot be written
241
+ * prints `nish: cannot write <path>` and exits 1.
242
+ */
203
243
  declare function writeFileSync(path: string, data: string): void;
244
+ /** `data` added at the end of the file, with the same refusal of a symbolic link. */
204
245
  declare function appendFileSync(path: string, data: string): void;
205
246
  /** One directory, not recursive; whether a directory is there afterwards. */
206
247
  declare function mkdirSync(path: string): boolean;
@@ -221,7 +262,8 @@ declare function realpathSync(path: string): string | null;
221
262
  declare function spawnSync(argv: string[]): number;
222
263
  /**
223
264
  * The same run with each non-empty path receiving that stream, created or
224
- * truncated; an empty string leaves that stream inherited.
265
+ * truncated; an empty string leaves that stream inherited. A symbolic link at
266
+ * either path is refused, as `writeFileSync` refuses one, and the answer is -1.
225
267
  */
226
268
  declare function spawnSyncTo(argv: string[], stdoutPath: string, stderrPath: string): number;
227
269
  /** One environment variable, or `null` when it is unset (an empty value is a set variable). */
@@ -252,9 +294,10 @@ declare function readSignal(fd: i32): i32;
252
294
  //
253
295
  // Every call answers an `i32`: `>= 0` on success, a negative errno otherwise, in
254
296
  // Linux's numbering on every platform for -11 (would block), -95, -32, -104,
255
- // -98 and -22. An address is 18 bytes of a `u8[]`: 16 of IPv6 address (IPv4 as
256
- // `::ffff:a.b.c.d`), then the port, big-endian. Every socket is non-blocking and
257
- // close-on-exec. No reading under Node, where each throws.
297
+ // -98, -111 (refused), -110 (timed out) and -22. An address is 18 bytes of a
298
+ // `u8[]`: 16 of IPv6 address (IPv4 as `::ffff:a.b.c.d`), then the port,
299
+ // big-endian. Every socket is non-blocking and close-on-exec. No reading under
300
+ // Node, where each throws.
258
301
 
259
302
  /** Write the address form of a numeric `host` and `port` into `out`: 0, or -22. */
260
303
  declare function netAddress(out: u8[], host: string, port: i32): i32;
@@ -272,6 +315,10 @@ declare function netWrite(fd: i32, buf: readonly u8[], off: i32, len: i32): i32;
272
315
  declare function netShutdown(fd: i32, how: i32): i32;
273
316
  /** Close the descriptor. */
274
317
  declare function netClose(fd: i32): i32;
318
+ /** A TCP socket connecting to `addr`, answered while it connects: writable once it has, or has failed. */
319
+ declare function tcpConnect(addr: readonly u8[]): i32;
320
+ /** Once `fd` is writable: 0 when the connection is made, else its failure (-111 refused). */
321
+ declare function connectResult(fd: i32): i32;
275
322
  /** A UDP socket: flag 1 `SO_REUSEPORT`, flag 2 `UDP_GRO` (-95 on Darwin); ECN is always read. */
276
323
  declare function udpBind(host: string, port: i32, flags: i32): i32;
277
324
  /** `buf[off, off + len)` to `to`, cut into `segment`-byte datagrams when `segment > 0`, with ECN bits `ecn`. */
@@ -325,7 +372,9 @@ declare module "nish:fs" {
325
372
  export function readFileSync(path: string): string;
326
373
  export function readFileSyncOrNull(path: string): string | null;
327
374
  export function readFileBytesSync(path: string): u8[] | null;
375
+ /** Refuses a symbolic link as the last component of the path, as the global does. */
328
376
  export function writeFileSync(path: string, data: string): void;
377
+ /** Refuses a symbolic link as the last component of the path, as the global does. */
329
378
  export function appendFileSync(path: string, data: string): void;
330
379
  /** `true` when the directory was created, `false` when it already existed. */
331
380
  export function mkdirSync(path: string): boolean;
@@ -347,7 +396,8 @@ declare module "nish:process" {
347
396
  export function spawnSync(argv: string[]): number;
348
397
  /**
349
398
  * The same run with each non-empty path receiving that stream, created or
350
- * truncated; an empty string leaves that stream inherited.
399
+ * truncated; an empty string leaves that stream inherited. A symbolic link
400
+ * at either path is refused, and the answer is -1.
351
401
  */
352
402
  export function spawnSyncTo(argv: string[], stdoutPath: string, stderrPath: string): number;
353
403
  /**
@@ -383,6 +433,10 @@ declare module "nish:net" {
383
433
  /** Shut the read side (0), the write side (1) or both (2). */
384
434
  export function netShutdown(fd: i32, how: i32): i32;
385
435
  export function netClose(fd: i32): i32;
436
+ /** A TCP socket connecting to `addr`, answered while it connects: writable once it has, or has failed. */
437
+ export function tcpConnect(addr: readonly u8[]): i32;
438
+ /** Once `fd` is writable: 0 when the connection is made, else its failure (-111 refused). */
439
+ export function connectResult(fd: i32): i32;
386
440
  /** A UDP socket: flag 1 `SO_REUSEPORT`, flag 2 `UDP_GRO` (-95 on Darwin); ECN is always read. */
387
441
  export function udpBind(host: string, port: i32, flags: i32): i32;
388
442
  /** `buf[off, off + len)` to `to`, cut into `segment`-byte datagrams when `segment > 0`, with ECN bits `ecn`. */
@@ -409,6 +463,19 @@ declare module "nish:net" {
409
463
  export function pollWait(loop: i32, ready: i32[], timeoutMs: i32): i32;
410
464
  }
411
465
 
466
+ declare module "nish:unsafe" {
467
+ /** `xs[i]` with no bounds check: undefined behaviour when `i` is out of range. */
468
+ export function uncheckedGet<T extends number>(xs: readonly T[], i: i32): T;
469
+ /** `xs[i] = v` with no bounds check: undefined behaviour when `i` is out of range. */
470
+ export function uncheckedSet<T extends number>(xs: T[], i: i32, v: T): void;
471
+ /** `a + b` wrapped to the operands' width, `i32` or `i64`. */
472
+ export function wrappingAdd<T extends i32 | i64>(a: T, b: T): T;
473
+ /** `a - b` wrapped to the operands' width. */
474
+ export function wrappingSub<T extends i32 | i64>(a: T, b: T): T;
475
+ /** `a * b` wrapped to the operands' width. */
476
+ export function wrappingMul<T extends i32 | i64>(a: T, b: T): T;
477
+ }
478
+
412
479
  declare module "nish:io" {
413
480
  /** `s` to stdout with no trailing newline and no conversion. */
414
481
  export function write(s: string): void;
@@ -418,6 +485,39 @@ declare module "nish:io" {
418
485
  export function panic(message: string): never;
419
486
  }
420
487
 
488
+ // `nish:secret` has source behind it, `std/secret.ts`, but a program imports it
489
+ // by this name, so `tsc` reads these declarations rather than that file. The
490
+ // class is declared with a private constructor and a private field: `tsc` then
491
+ // refuses `new Secret(...)`, `s.value` and a structural look-alike, as `nish`
492
+ // does. Everything else `nish` refuses about a `Secret` — printing it,
493
+ // comparing it, branching on it, dropping it unwiped, an `expose` whose
494
+ // function does I/O — is flow, which `tsc` cannot see
495
+ // (docs/LANGUAGE.md -> Secrets).
496
+ declare module "nish:secret" {
497
+ /**
498
+ * Key material: an array of integers or a record of integer fields, held
499
+ * opaque. Made by `secret`, read only through `expose`, and wiped or
500
+ * returned by the function that made it.
501
+ */
502
+ export class Secret<T> {
503
+ private constructor();
504
+ private readonly value: T;
505
+ }
506
+ /** `value`, wrapped. A local handed in is moved: it may not be read again. */
507
+ export function secret<T>(value: T): Secret<T>;
508
+ /**
509
+ * `f(value)`. `f` reaches no I/O and no C, keeps nothing of `value`, and
510
+ * declares a return type that holds no `Secret`: that type is what leaves.
511
+ */
512
+ export function expose<T, R>(s: Secret<T>, f: (value: T) => R): R;
513
+ /** `f(value, arg)`, under `expose`'s rules; `f` may not write `arg` either. */
514
+ export function exposeWith<T, A, R>(s: Secret<T>, arg: A, f: (value: T, arg: A) => R): R;
515
+ /** Zero the value's whole storage, with a store the optimiser may not remove. */
516
+ export function wipe<T>(target: Secret<T>): void;
517
+ /** Zero an array or record of integers that `expose`'s function holds, likewise. */
518
+ export function wipe(target: object): void;
519
+ }
520
+
421
521
  // ---- Arena (docs/LANGUAGE.md -> Arena) ---------------------------------------
422
522
 
423
523
  declare const Arena: {
@@ -431,6 +531,15 @@ declare const Arena: {
431
531
  used(): i64;
432
532
  };
433
533
 
534
+ /**
535
+ * `using a = arena()` (docs/LANGUAGE.md -> Arena): everything the block
536
+ * allocates after this declaration is released when the block ends, on every
537
+ * exit, and the compiler refuses the block if anything allocated in it could
538
+ * outlive it. Only a `using` initialiser, and the binding is never read; under
539
+ * Node there is no arena and the disposal does nothing.
540
+ */
541
+ declare function arena(): Disposable;
542
+
434
543
  /**
435
544
  * `CPtr` (WP27 S2): the address a `declare function` hands back, opaque and
436
545
  * eight bytes wide. `docs/LANGUAGE.md` has the rules — it may be written in a
package/runtime/nish.h CHANGED
@@ -225,6 +225,11 @@ nish_array *nish_alloc_array(uint64_t elem_size, uint64_t len);
225
225
  * elements themselves — which is why the checker refuses to let a program hold
226
226
  * a pointer to one across a `push` (WP15 section 2a). */
227
227
  void nish_array_grow(nish_array *a, uint64_t elem_size);
228
+ /* `secureZero(bytes)` (#385): sets every byte of `bytes` to zero through
229
+ * `volatile` stores, out of line, so that neither `-O2` nor `-flto` can drop
230
+ * them as dead when the buffer's life ends straight after the call. For
231
+ * secret material -- a private key, a secret scalar -- once it is used. */
232
+ void nish_wipe(nish_array *bytes);
228
233
  void nish_panic_index(uint64_t idx, uint64_t len);
229
234
  /* The failed range check of `s.slice(start, end)` (WP15 section 4): prints the
230
235
  half-open interval that was asked for and the byte length it left, then
@@ -313,7 +318,12 @@ nish_str *nish_getenv(const nish_str *name);
313
318
  * but the last must exist for POSIX `realpath` to answer at all.
314
319
  *
315
320
  * `path` is read and never retained. The answer is a fresh arena string, so
316
- * it outlives the call the way every other string here does. */
321
+ * it outlives the call the way every other string here does.
322
+ *
323
+ * Under the wasi profile the answer is the sysroot's `realpath`, which
324
+ * wasi-sdk 22 and later have. A wasi-libc without one (the one Ubuntu and
325
+ * Debian package) answers NULL for every path, which a caller already
326
+ * handles. */
317
327
  nish_str *nish_realpath(const nish_str *path);
318
328
 
319
329
  /* ---- What machine this is (WP14 §7a) ------------------------------------
@@ -361,8 +371,9 @@ double nish_stat_mtime(const nish_str *path);
361
371
  * or a read that fails. */
362
372
  int32_t nish_signal_fd(void);
363
373
  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
374
+ /* Who owns a path, and whether it runs (docs/security/runtime.md, RT-9; the
375
+ * builtins `lstatOwnerModeSync`, `geteuid` and `isExecutableSync`, #386).
376
+ * `nish_lstat_owner_mode(path)`: `lstat`, so a
366
377
  * symbolic link answers for itself, as the owner's uid in the high 32 bits
367
378
  * and `st_mode` in the low 32, or exactly -1 when the path does not resolve.
368
379
  * `nish_euid()`: the effective uid. `nish_is_executable(path)`:
@@ -375,10 +386,11 @@ bool nish_is_executable(const nish_str *path);
375
386
  * Every call answers an `int32_t`: `>= 0` on success (a descriptor, a byte
376
387
  * count, 0) and a negative errno otherwise, in Linux's numbering on every
377
388
  * platform for -11 (would block), -95 (unsupported), -32 (the peer is gone),
378
- * -104 (reset), -98 (address in use) and -22 (a bad argument). Every socket
379
- * is non-blocking and close-on-exec. An address is 18 bytes of a `u8[]`
380
- * (element type `uint8_t`): 16 of IPv6 address, IPv4 as `::ffff:a.b.c.d`,
381
- * then the port, big-endian; a shorter array is -22. Nothing allocates.
389
+ * -104 (reset), -98 (address in use), -111 (refused), -110 (timed out) and
390
+ * -22 (a bad argument). Every socket is non-blocking and close-on-exec. An
391
+ * address is 18 bytes of a `u8[]` (element type `uint8_t`): 16 of IPv6
392
+ * address, IPv4 as `::ffff:a.b.c.d`, then the port, big-endian; a shorter
393
+ * array is -22. Nothing allocates.
382
394
  *
383
395
  * `nish_net_address(out, host, port)`: the form of a numeric `host`.
384
396
  * `nish_net_local_port(fd)`: the port a socket is bound to.
@@ -389,6 +401,10 @@ bool nish_is_executable(const nish_str *path);
389
401
  * len)` of `buf`, which the compiled call has range-checked; a write to a
390
402
  * gone peer is -32, never SIGPIPE. `nish_net_shutdown(fd, how)`: 0 read, 1
391
403
  * write, 2 both. `nish_net_close(fd)`.
404
+ * `nish_tcp_connect(addr)`: a new socket connecting to `addr`, answered at
405
+ * once; its `EINPROGRESS` is not a failure, and any other closes it.
406
+ * `nish_connect_result(fd)`: `SO_ERROR` once the socket is writable, 0 when
407
+ * the connection is made.
392
408
  *
393
409
  * UDP: `nish_udp_bind(host, port, flags)` binds a datagram socket, flag 1
394
410
  * `SO_REUSEPORT` and flag 2 `UDP_GRO` (-95 on Darwin). `nish_udp_send_to(fd,
@@ -418,6 +434,8 @@ int32_t nish_net_read(int32_t fd, nish_array *buf, int64_t off, int64_t len);
418
434
  int32_t nish_net_write(int32_t fd, const nish_array *buf, int64_t off, int64_t len);
419
435
  int32_t nish_net_shutdown(int32_t fd, int32_t how);
420
436
  int32_t nish_net_close(int32_t fd);
437
+ int32_t nish_tcp_connect(const nish_array *addr);
438
+ int32_t nish_connect_result(int32_t fd);
421
439
  int32_t nish_udp_bind(const nish_str *host, int32_t port, int32_t flags);
422
440
  int32_t nish_udp_send_to(int32_t fd, const nish_array *buf, int64_t off, int64_t len, const nish_array *to,
423
441
  int32_t segment, int32_t ecn);
@@ -439,6 +457,11 @@ double nish_parse_number(const nish_str *s, int32_t mode);
439
457
  /* Checked integer division (Rust semantics): the failed-check path. */
440
458
  void nish_panic_div(bool by_zero);
441
459
 
460
+ /* Checked signed arithmetic: the failed-check path. `op` is 0 add, 1
461
+ * subtract, 2 multiply, 3 negate; prints "attempt to <op> with overflow" and
462
+ * exits 1. */
463
+ void nish_panic_overflow(uint32_t op);
464
+
442
465
  /* ---- Parallel work (WP20 T1 / wp29 stage P1), runtime/runtime-parallel.c ----
443
466
  *
444
467
  * One region of work, divided. `nish_parallel_range` calls `body(lo, hi, ctx)`
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
  });