@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/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,9 +411,16 @@ 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
406
- one level up from wherever it was invoked, then in the working directory, so
407
- it wants a checkout or an installed package around it the way `nish` does.
414
+ binary itself. It looks for `scripts/build.sh` and the `runtime/*.c` files
415
+ one level up from wherever it was invoked (and one level up from the file a
416
+ symbolic link to it names), so it wants a checkout or an installed package
417
+ around it the way `nish` does. It falls back to the working directory only
418
+ when the compiler itself lies inside that directory, so a checkout you merely
419
+ `cd` into cannot hand it a `scripts/build.sh` to run. Keep `bin/nish` beside
420
+ `scripts/`, `runtime/` and `std/`, as every install above does, and keep that
421
+ directory's parent writable only by you: the package root is the parent of
422
+ the compiler's directory, so a compiler at `/tmp/x/nish` would trust
423
+ `/tmp/scripts/build.sh` (CLI-9 in [docs/security/cli.md](security/cli.md)).
408
424
 
409
425
  Because the seed is the last release, `src/` may only *use* in its own
410
426
  source the constructs that release compiles. A new construct is implemented
@@ -457,9 +473,11 @@ nish run hello.ts
457
473
  `$XDG_CACHE_HOME/nish/run` (or `~/.cache/nish/run`) when that program has not
458
474
  been linked before, and starts it with the arguments after the file. The first
459
475
  run of a new edit pays for one link, and later runs start the cached binary.
460
- It needs clang only for that link. It needs `HOME` or `XDG_CACHE_HOME` set,
461
- and refuses the run without them rather than keep a binary in a shared
462
- directory.
476
+ It needs clang only for that link. It needs `HOME` or `XDG_CACHE_HOME` set
477
+ to an absolute path (a relative one is ignored, as if it were unset), and
478
+ refuses the run without one rather than keep a binary in a shared directory.
479
+ The first run that builds into the cache makes its root `0700`, so no other
480
+ user can plant a binary in it ([docs/security/cli.md](security/cli.md)).
463
481
 
464
482
  ## 4. Exit codes
465
483
 
@@ -469,7 +487,7 @@ directory.
469
487
  | 1 | the program was rejected: compile error (`file:line:col: error: ...`), missing input file, or an `-o` layout that does not fit the module count |
470
488
  | 2 | usage error: unknown flag, missing argument, no input files |
471
489
  | 3 | toolchain error: `--link` found no `clang` (`CC` overrides), or `scripts/build.sh` failed (its output is shown; the `.ll` files are still written) — or the `nish` command found no prebuilt compiler for this platform, or one that would not start. Under `--json` all of these are one `NL0002` object |
472
- | | `nish run` answers these codes until the program starts, and the program's own exit status (`128 + n` for a signal) after that. With neither `HOME` nor `XDG_CACHE_HOME` set it answers 3 |
490
+ | | `nish run` answers these codes until the program starts, and the program's own exit status (`128 + n` for a signal) after that. With neither `HOME` nor `XDG_CACHE_HOME` set to an absolute path it answers 3 |
473
491
  | 70 | internal compiler error: an unexpected exception. Please report it at <https://github.com/amritk/nish/issues> with the input and command line; `NISH_DEBUG=1` prints the stack trace |
474
492
 
475
493
  ## Troubleshooting
package/bin/nish CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/nish-x86_64-linux",
3
- "version": "0.15.0",
3
+ "version": "0.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
@@ -63,6 +63,17 @@ typedef struct nish_str {
63
63
  char data[];
64
64
  } nish_str;
65
65
 
66
+ /* The bytes of `s` as the C string a system call takes, or "" when `s` holds
67
+ * a NUL. The kernel reads a path to its first NUL, so `"secret\0.txt"` would
68
+ * pass a `.txt` check and open `secret`; "" names nothing, so each caller
69
+ * answers it as it answers a missing path. The one test every path, name and
70
+ * argument the runtime hands the OS goes through (docs/security/runtime.md,
71
+ * RT-3). `__builtin_strlen`, so this header needs no libc header of its own,
72
+ * and `unused` because a host that includes it may never call it. */
73
+ static inline __attribute__((unused)) const char *nish_cpath(const nish_str *s) {
74
+ return __builtin_strlen(s->data) == s->len ? s->data : "";
75
+ }
76
+
66
77
  /* ---- Arena --------------------------------------------------------------
67
78
  * One bump allocator per process, or one per thread under `-DNISH_THREADS`
68
79
  * (WP20 T0; the layout and every function below are the same either way).
@@ -139,7 +150,16 @@ nish_str *nish_str_from_i64(int64_t v);
139
150
  nish_str *nish_str_from_u64(uint64_t v);
140
151
 
141
152
  /* Process and file I/O (WP7). `nish_exit` never returns; the file functions
142
- * print a message to stderr and exit(1) on a fatal error. Everything from here
153
+ * print a message to stderr and exit(1) on a fatal error.
154
+ *
155
+ * Every function here that takes a path, a name or an argument vector refuses
156
+ * one holding a NUL, answering as it answers a path that does not exist: the
157
+ * kernel would read only the bytes before the NUL, which is a different path
158
+ * from the one the program checked (docs/security/runtime.md, RT-3). A file
159
+ * longer than 2^31 - 1 bytes is unreadable, as it is to `nish_read_file_or_null`
160
+ * and `nish_read_file_bytes` too, because no string or array may be longer
161
+ * (RT-1). The two writers refuse a symbolic link as the last component of the
162
+ * path rather than follow it (RT-4). Everything from here
143
163
  * to the end of the clock section below is implemented in runtime-os.c, with
144
164
  * `nish_random` and `nish_argv_init` the exceptions: neither asks the operating
145
165
  * system anything (the clock only seeds the first, and the entry point hands the
@@ -194,12 +214,22 @@ typedef struct nish_array { uint64_t len; uint64_t cap; char *data; } nish_array
194
214
  * `elem_size` is `sizeof` one element, so for a record element type it is
195
215
  * `sizeof(struct Point)` and the block is a C array of them. */
196
216
  nish_array *nish_alloc_array(uint64_t elem_size, uint64_t len);
217
+ /* No array grows past 2^31 - 1 elements and no string past 2^31 - 1 bytes:
218
+ * `nish_array_grow`, `nish_alloc_array` and `nish_str_concat` fail as an
219
+ * allocation does (`nish: out of memory`, exit 1; a trap on wasm) rather than
220
+ * make one, because under --number-mode i32 its `length` would read back
221
+ * negative (docs/security/codegen.md K1-6, docs/security/runtime.md RT-2). */
197
222
  /* The cold paths compiled code calls: `push` when len == cap, a failed bounds
198
223
  * check. `nish_array_grow` moves `cap * elem_size` bytes into a fresh arena
199
224
  * block and writes `data`/`cap`, so for a record element type it relocates the
200
225
  * elements themselves — which is why the checker refuses to let a program hold
201
226
  * a pointer to one across a `push` (WP15 section 2a). */
202
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);
203
233
  void nish_panic_index(uint64_t idx, uint64_t len);
204
234
  /* The failed range check of `s.slice(start, end)` (WP15 section 4): prints the
205
235
  half-open interval that was asked for and the byte length it left, then
@@ -254,12 +284,12 @@ nish_array *nish_read_file_bytes(const nish_str *path);
254
284
  * `128 + n` when signal `n` killed it. -1 when `argv` is empty, when the
255
285
  * child cannot be started or waited for, and always under WASI, which has no
256
286
  * processes. The elements are `nish_str *`; the child receives their bytes,
257
- * so an argument containing a NUL is truncated at it. */
287
+ * and an argument containing a NUL is refused (-1) rather than truncated. */
258
288
  int32_t nish_spawn(const nish_array *argv);
259
289
  /* `spawnSyncTo(argv, stdoutPath, stderrPath)`: exactly `nish_spawn` — the same
260
290
  * `PATH` search, the same wait, the same status, `128 + n` and -1 — except that
261
291
  * each non-empty path receives that stream, created or truncated at 0644 as
262
- * `nish_write_file` would leave it. An **empty** string leaves that stream
292
+ * `nish_write_file` would leave it, a symbolic link at the path refused. An **empty** string leaves that stream
263
293
  * inherited, so one call can capture stdout and let stderr through to the
264
294
  * terminal. The child opens the files, so a failed open is a child that could
265
295
  * not start and answers -1 rather than leaving this process redirected. The two
@@ -275,8 +305,8 @@ int32_t nish_spawn_to(const nish_array *argv, const nish_str *out, const nish_st
275
305
  * arena (so a later `setenv` cannot change a string the program still holds),
276
306
  * or NULL when it is unset — the language's `string | null`. An empty value
277
307
  * is a set variable and answers a zero-length string, not NULL. `name` is
278
- * read and never retained; a NUL inside it truncates the lookup, as it does
279
- * for every other path-like argument here. */
308
+ * read and never retained; a name holding a NUL is unset, as it is for every
309
+ * other path-like argument here. */
280
310
  nish_str *nish_getenv(const nish_str *name);
281
311
 
282
312
  /* ---- Symlinks (WP19 §5a item 4) -----------------------------------------
@@ -288,7 +318,12 @@ nish_str *nish_getenv(const nish_str *name);
288
318
  * but the last must exist for POSIX `realpath` to answer at all.
289
319
  *
290
320
  * `path` is read and never retained. The answer is a fresh arena string, so
291
- * 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. */
292
327
  nish_str *nish_realpath(const nish_str *path);
293
328
 
294
329
  /* ---- What machine this is (WP14 §7a) ------------------------------------
@@ -336,15 +371,26 @@ double nish_stat_mtime(const nish_str *path);
336
371
  * or a read that fails. */
337
372
  int32_t nish_signal_fd(void);
338
373
  int32_t nish_read_signal(int32_t fd);
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
377
+ * symbolic link answers for itself, as the owner's uid in the high 32 bits
378
+ * and `st_mode` in the low 32, or exactly -1 when the path does not resolve.
379
+ * `nish_euid()`: the effective uid. `nish_is_executable(path)`:
380
+ * `access(path, X_OK)`, for the real uid. */
381
+ int64_t nish_lstat_owner_mode(const nish_str *path);
382
+ int64_t nish_euid(void);
383
+ bool nish_is_executable(const nish_str *path);
339
384
 
340
385
  /* ---- The network (WP34 N5), runtime/runtime-net.c, `nish:net` ------------
341
386
  * Every call answers an `int32_t`: `>= 0` on success (a descriptor, a byte
342
387
  * count, 0) and a negative errno otherwise, in Linux's numbering on every
343
388
  * platform for -11 (would block), -95 (unsupported), -32 (the peer is gone),
344
- * -104 (reset), -98 (address in use) and -22 (a bad argument). Every socket
345
- * is non-blocking and close-on-exec. An address is 18 bytes of a `u8[]`
346
- * (element type `uint8_t`): 16 of IPv6 address, IPv4 as `::ffff:a.b.c.d`,
347
- * 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.
348
394
  *
349
395
  * `nish_net_address(out, host, port)`: the form of a numeric `host`.
350
396
  * `nish_net_local_port(fd)`: the port a socket is bound to.
@@ -355,6 +401,10 @@ int32_t nish_read_signal(int32_t fd);
355
401
  * len)` of `buf`, which the compiled call has range-checked; a write to a
356
402
  * gone peer is -32, never SIGPIPE. `nish_net_shutdown(fd, how)`: 0 read, 1
357
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.
358
408
  *
359
409
  * UDP: `nish_udp_bind(host, port, flags)` binds a datagram socket, flag 1
360
410
  * `SO_REUSEPORT` and flag 2 `UDP_GRO` (-95 on Darwin). `nish_udp_send_to(fd,
@@ -384,6 +434,8 @@ int32_t nish_net_read(int32_t fd, nish_array *buf, int64_t off, int64_t len);
384
434
  int32_t nish_net_write(int32_t fd, const nish_array *buf, int64_t off, int64_t len);
385
435
  int32_t nish_net_shutdown(int32_t fd, int32_t how);
386
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);
387
439
  int32_t nish_udp_bind(const nish_str *host, int32_t port, int32_t flags);
388
440
  int32_t nish_udp_send_to(int32_t fd, const nish_array *buf, int64_t off, int64_t len, const nish_array *to,
389
441
  int32_t segment, int32_t ecn);
@@ -405,6 +457,11 @@ double nish_parse_number(const nish_str *s, int32_t mode);
405
457
  /* Checked integer division (Rust semantics): the failed-check path. */
406
458
  void nish_panic_div(bool by_zero);
407
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
+
408
465
  /* ---- Parallel work (WP20 T1 / wp29 stage P1), runtime/runtime-parallel.c ----
409
466
  *
410
467
  * One region of work, divided. `nish_parallel_range` calls `body(lo, hi, ctx)`