@amritk/nish-x86_64-linux 0.20.0 → 0.21.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 (70) hide show
  1. package/INSTALL.md +22 -10
  2. package/bin/nish +0 -0
  3. package/docs/AI.md +2759 -0
  4. package/package.json +2 -1
  5. package/runtime/browser.d.ts +14 -0
  6. package/runtime/browser.mjs +88 -0
  7. package/runtime/core/arrays.d.ts +18 -0
  8. package/runtime/core/arrays.mjs +125 -0
  9. package/runtime/core/checks.mjs +20 -0
  10. package/runtime/core/host.d.ts +70 -0
  11. package/runtime/core/host.mjs +311 -0
  12. package/runtime/core/numbers.d.ts +53 -0
  13. package/runtime/core/numbers.mjs +316 -0
  14. package/runtime/core/result.d.ts +41 -0
  15. package/runtime/core/result.mjs +127 -0
  16. package/runtime/core/strings.d.ts +14 -0
  17. package/runtime/core/strings.mjs +169 -0
  18. package/runtime/core.d.ts +9 -0
  19. package/runtime/core.mjs +55 -0
  20. package/runtime/host.d.ts +6 -0
  21. package/runtime/nish.d.ts +44 -0
  22. package/runtime/nish.h +75 -2
  23. package/runtime/nish.mjs +23 -3
  24. package/runtime/node.mjs +383 -0
  25. package/runtime/runtime-bytes.c +401 -0
  26. package/runtime/runtime-cross.c +1 -1
  27. package/runtime/runtime-host.c +1 -1
  28. package/runtime/runtime-os.c +1 -1
  29. package/runtime/runtime-parallel.c +1 -1
  30. package/runtime/runtime-simd.c +231 -7
  31. package/runtime/runtime-term.c +504 -0
  32. package/runtime/runtime-wasm.c +0 -11
  33. package/runtime/runtime.c +190 -6
  34. package/runtime/shim.mjs +6 -985
  35. package/runtime/sqlite/LICENSE.md +85 -0
  36. package/runtime/sqlite/README.md +45 -0
  37. package/runtime/sqlite/nish-sqlite.c +359 -0
  38. package/runtime/sqlite/sqlite3.c +271131 -0
  39. package/runtime/sqlite/sqlite3.h +14447 -0
  40. package/scripts/build.sh +261 -53
  41. package/std/LICENSE-fast-float +23 -0
  42. package/std/LICENSE-fdlibm +8 -0
  43. package/std/README.md +31 -11
  44. package/std/base64.ts +116 -0
  45. package/std/bytes.ts +256 -0
  46. package/std/cli.ts +328 -0
  47. package/std/collections.ts +6 -5
  48. package/std/crypto/LICENSE-crypt_blowfish +26 -0
  49. package/std/crypto/base64url.ts +125 -0
  50. package/std/crypto/bcrypt.ts +760 -0
  51. package/std/crypto/hmac.ts +138 -2
  52. package/std/crypto/x509.ts +11 -122
  53. package/std/json-tokenizer.ts +2998 -57
  54. package/std/json-value.ts +22 -3
  55. package/std/json.ts +83 -1
  56. package/std/math.ts +608 -0
  57. package/std/net/http1-server.ts +85 -8
  58. package/std/net/http3-server.ts +50 -12
  59. package/std/net/postgres.ts +1875 -0
  60. package/std/net/quic-listener.ts +20 -4
  61. package/std/regex.ts +4274 -0
  62. package/std/sqlite.ts +391 -0
  63. package/std/term-io.ts +978 -0
  64. package/std/term-pixels.ts +874 -0
  65. package/std/term-probe.ts +459 -0
  66. package/std/term-rate.ts +993 -0
  67. package/std/term.ts +2453 -0
  68. package/std/text.ts +90 -7
  69. package/std/yaml-lines.ts +141 -0
  70. package/std/yaml.ts +4675 -0
package/runtime/runtime.c CHANGED
@@ -41,6 +41,25 @@ int __main_argc_argv(int argc, char **argv) { return nish_c_main(argc, argv); }
41
41
 
42
42
  #define NISH_COLD __attribute__((noreturn, cold, noinline))
43
43
 
44
+ /* ---- AddressSanitizer build (`-DNISH_ASAN`, tests/differential/asan.js)
45
+ A bump arena hides every overflow from ASan: one 64 KB malloc holds many
46
+ blocks, so a read past one block lands in the next and nothing is poisoned.
47
+ Under `NISH_ASAN` each arena block is a malloc of its own (`nish_asan_alloc`,
48
+ which asan.js also points compiled code's inline bump at), so a read one past
49
+ it hits ASan's redzone; the bytes the 8-byte rounding adds, an array's spare
50
+ capacity and an array's block left behind by growth are poisoned by hand.
51
+ Off, the macro is empty and the file compiles to the same object code it
52
+ always did. What it cannot see: a `pop` leaves the popped element
53
+ addressable below `cap` (the pop is inline IR), and a string's NUL is one
54
+ addressable byte past its `length`. `nish_arena_used` reads differently
55
+ under ASan, since every block is a chunk of its own. */
56
+ #ifdef NISH_ASAN
57
+ #include <sanitizer/asan_interface.h>
58
+ #define NISH_POISON(p, n) __asan_poison_memory_region((p), (n))
59
+ #else
60
+ #define NISH_POISON(p, n) ((void)0)
61
+ #endif
62
+
44
63
  /* ---- Thread-local state (WP20 T0)
45
64
  The arena and the RNG seed are the only mutable process-wide objects here,
46
65
  and both are a data race the moment a second thread runs compiled code. With
@@ -116,6 +135,27 @@ void nish_panic_claim(void) {
116
135
  static inline void nish_panic_claim(void) {}
117
136
  #endif
118
137
 
138
+ /* How a panic that skips atexit ends: `_exit(1)`, after `nish_fatal_hook` when
139
+ it is set. `setRawMode(true)` and `setTerminalRestore` (runtime-term.c) set
140
+ the hook to the terminal's restore, and without it a failed bounds check
141
+ would leave the shell in raw mode. Each
142
+ panic path calls this where it called `_exit(1)`, so a call site costs no
143
+ more than it did; `exit(1)` paths need nothing, since the restore is also an
144
+ atexit handler. Built with `-DNISH_FUZZ_ABORT` (scripts/fuzz.sh), this and
145
+ `nish_panic` end in `abort()` instead, so libFuzzer records a panic, a failed
146
+ bounds or division check and an out-of-memory as a crash; the default build
147
+ compiles neither. */
148
+ __attribute__((noreturn)) void nish_fatal_exit(void);
149
+ void (*nish_fatal_hook)(void);
150
+ void nish_fatal_exit(void) {
151
+ void (*hook)(void) = __atomic_load_n(&nish_fatal_hook, __ATOMIC_ACQUIRE);
152
+ if (hook) hook();
153
+ #ifdef NISH_FUZZ_ABORT
154
+ abort();
155
+ #endif
156
+ _exit(1);
157
+ }
158
+
119
159
  /* The message goes out through `nish_write_all`, below, which finishes a
120
160
  short write and retries one a signal interrupts (docs/security/runtime.md,
121
161
  RT-11); it is unbuffered, so the message leaves before `_exit`. It used
@@ -124,7 +164,7 @@ static void nish_write_all(int fd, const char *p, uint64_t n);
124
164
  static NISH_COLD void nish_die(const char *msg) {
125
165
  nish_panic_claim();
126
166
  nish_write_all(2, msg, strlen(msg));
127
- _exit(1);
167
+ nish_fatal_exit();
128
168
  }
129
169
  /* A function rather than a macro around `nish_die`: seven call sites each
130
170
  loading the message's address cost more than one more symbol does. */
@@ -136,7 +176,13 @@ static NISH_COLD void nish_oom(void) { nish_die("nish: out of memory\n"); }
136
176
 
137
177
  /* Slow path (size 8-byte rounded): push a chunk (>= 64 KB), bump from it. */
138
178
  void *nish_arena_grow(uint64_t size) {
179
+ #ifdef NISH_ASAN
180
+ /* An exact chunk leaves off == cap, so the inline bump never fits again and
181
+ every block comes through here: one malloc, one redzone, per block. */
182
+ uint64_t cap = size;
183
+ #else
139
184
  uint64_t cap = size > 65536 ? size : 65536;
185
+ #endif
140
186
  /* A 64-bit request cannot always be asked for: wasm32's `size_t` is 32 bits,
141
187
  and malloc would silently take the low half of a chunk nobody can address. */
142
188
  if (cap > (uint64_t)(SIZE_MAX - sizeof(nish_chunk))) nish_oom();
@@ -151,8 +197,22 @@ void *nish_arena_grow(uint64_t size) {
151
197
  return nish_arena.buf;
152
198
  }
153
199
 
200
+ #ifdef NISH_ASAN
201
+ /* An instrumented program's every arena block: an exact chunk, so the inline
202
+ bump never fits again, with the bytes the rounding adds poisoned. */
203
+ void *nish_asan_alloc(uint64_t size) {
204
+ uint64_t rounded = (size + 7) & ~(uint64_t)7;
205
+ char *p = nish_arena_grow(rounded);
206
+ NISH_POISON(p + size, rounded - size);
207
+ return p;
208
+ }
209
+ #endif
210
+
154
211
  /* Bump allocation, 8-byte rounded/aligned, uninitialised; the compiler inlines it. */
155
212
  void *nish_alloc_struct(uint64_t size) {
213
+ #ifdef NISH_ASAN
214
+ return nish_asan_alloc(size);
215
+ #endif
156
216
  size = (size + 7) & ~(uint64_t)7;
157
217
  if (nish_arena.off + size <= nish_arena.cap) {
158
218
  void *p = nish_arena.buf + nish_arena.off;
@@ -162,12 +222,91 @@ void *nish_alloc_struct(uint64_t size) {
162
222
  return nish_arena_grow(size);
163
223
  }
164
224
 
225
+ #ifdef NISH_ASAN
226
+ /* The capacity a normal build would give each array `nish_array_grow` grew.
227
+ `cap` stays `len + 1`, since the push writes `data[len]` inline and spare
228
+ capacity would be addressable, but growing one slot at a time copies the
229
+ whole array on every push. So a block is allocated at the capacity a normal
230
+ build would choose, its tail poisoned, and each later grow of the same array
231
+ unpoisons one more slot in place until that capacity is used up. The block
232
+ then moves at exactly the push where a normal build reallocates, so a
233
+ pointer kept across that push reads poison.
234
+ The table is keyed by the grown block and loses an entry only when that
235
+ block dies: the grow that moves it, or `nish_free_until` freeing its chunk
236
+ (every ASan block is a chunk of its own). It is one table for every thread,
237
+ behind a spin lock, so an array grown on one thread and pushed on another
238
+ still hits. A miss is therefore an array no grow has moved yet, whose `cap`
239
+ is the one a normal build gave it, so the block never moves late. Open
240
+ addressing, linear probes, a tombstone for a dead entry, rebuilt at half
241
+ full. */
242
+ #define NISH_GROWN_DEAD ((char *)1)
243
+ static struct nish_grown { char *data; const void *owner; uint64_t native; } *nish_grown;
244
+ static uint64_t nish_grown_cap, nish_grown_used, nish_grown_live; /* used = live + tombstones */
245
+ static int nish_grown_lock;
246
+
247
+ static void nish_grown_enter(void) {
248
+ while (__atomic_exchange_n(&nish_grown_lock, 1, __ATOMIC_ACQUIRE)) {}
249
+ }
250
+ static void nish_grown_leave(void) { __atomic_store_n(&nish_grown_lock, 0, __ATOMIC_RELEASE); }
251
+
252
+ static uint64_t nish_grown_hash(const char *data) {
253
+ return ((uintptr_t)data * 0x9E3779B97F4A7C15ull >> 32) & (nish_grown_cap - 1);
254
+ }
255
+
256
+ static struct nish_grown *nish_grown_find(const char *data) {
257
+ if (!nish_grown_cap || !data) return 0;
258
+ for (uint64_t i = nish_grown_hash(data);; i = (i + 1) & (nish_grown_cap - 1)) {
259
+ if (nish_grown[i].data == data) return &nish_grown[i];
260
+ if (!nish_grown[i].data) return 0;
261
+ }
262
+ }
263
+
264
+ static void nish_grown_drop(const char *data) {
265
+ struct nish_grown *g = nish_grown_find(data);
266
+ if (g) g->data = NISH_GROWN_DEAD, nish_grown_live--;
267
+ }
268
+
269
+ static void nish_grown_put(char *data, const void *owner, uint64_t native) {
270
+ if ((nish_grown_used + 1) * 2 > nish_grown_cap) {
271
+ struct nish_grown *old = nish_grown;
272
+ uint64_t n = nish_grown_cap, cap = 64;
273
+ while (cap < (nish_grown_live + 1) * 4) cap *= 2;
274
+ nish_grown = calloc(cap, sizeof *nish_grown);
275
+ if (!nish_grown) nish_oom();
276
+ nish_grown_cap = cap, nish_grown_used = nish_grown_live = 0;
277
+ for (uint64_t i = 0; i < n; i++)
278
+ if (old[i].data > NISH_GROWN_DEAD) nish_grown_put(old[i].data, old[i].owner, old[i].native);
279
+ free(old);
280
+ }
281
+ uint64_t i = nish_grown_hash(data);
282
+ while (nish_grown[i].data > NISH_GROWN_DEAD) i = (i + 1) & (nish_grown_cap - 1);
283
+ if (!nish_grown[i].data) nish_grown_used++;
284
+ nish_grown[i] = (struct nish_grown){ data, owner, native };
285
+ nish_grown_live++;
286
+ }
287
+ #endif
288
+ #ifdef NISH_ASAN
289
+ void nish_free_arena(void);
290
+ #endif
165
291
  static void nish_free_until(nish_chunk *c, const nish_chunk *end) {
292
+ #ifdef NISH_ASAN
293
+ /* A freed block's address comes back from malloc: forget the grown ones. */
294
+ if (nish_grown_live) {
295
+ nish_grown_enter();
296
+ for (nish_chunk *d = c; d != end; d = d->next) nish_grown_drop((char *)(d + 1));
297
+ nish_grown_leave();
298
+ }
299
+ #endif
166
300
  while (c != end) { nish_chunk *n = c->next; free(c); c = n; }
167
301
  }
168
302
 
169
303
  /* O(1) recycle: keep the newest chunk, free the rest. */
170
304
  void nish_reset_arena(void) {
305
+ #ifdef NISH_ASAN
306
+ /* Free everything, so a value read after the reset is a use after free. */
307
+ nish_free_arena();
308
+ return;
309
+ #endif
171
310
  nish_chunk *keep = nish_arena.chunks;
172
311
  if (!keep) return;
173
312
  nish_free_until(keep->next, 0);
@@ -323,8 +462,9 @@ int64_t nish_str_index_of(const nish_str *s, const nish_str *sub) { return nish_
323
462
  a write fails (docs/security/runtime.md, RT-11). A failure still ends the
324
463
  output, `EAGAIN` on a non-blocking stdout included, since spinning on it
325
464
  would burn a core. `EINTR` is retried, the newline's included: the only
326
- handler this runtime installs (runtime-host.c) has `SA_RESTART`, but a C
327
- host may install one without it. `panic`'s message, `expect`'s, and the
465
+ handlers this runtime installs that return (runtime-host.c's and
466
+ runtime-term.c's SIGTSTP) have `SA_RESTART`, but a C host may install one
467
+ without it. `panic`'s message, `expect`'s, and the
328
468
  arithmetic and out-of-memory panics (`nish_die`) are written here. The
329
469
  index and slice panics and the file and entropy failures use `dprintf`, and
330
470
  runtime-parallel.c's out of memory one `write`: neither retries `EINTR`. */
@@ -348,6 +488,9 @@ void nish_print(const nish_str *s) { nish_write(s, 1, 1); }
348
488
  __attribute__((noreturn)) void nish_panic(const nish_str *s) {
349
489
  nish_panic_claim();
350
490
  nish_write(s, 2, 1);
491
+ #ifdef NISH_FUZZ_ABORT
492
+ abort();
493
+ #endif
351
494
  exit(1);
352
495
  }
353
496
 
@@ -1219,10 +1362,18 @@ nish_array *nish_alloc_array(uint64_t elem_size, uint64_t len) {
1219
1362
  this first. */
1220
1363
  nish_array *nish_argv;
1221
1364
 
1365
+ /* The key of every `Map` and `Set` string hash (docs/security/json.md, JSON-13):
1366
+ SipHash-1-3's 128 bits, zero until the constructor each module that hashes
1367
+ a string registers fills it from `getentropy` before `main`. Data, not code,
1368
+ so it costs this file's `.text` budget nothing, and not NISH_TLS, for the
1369
+ reason `nish_argv` is not: it is written once before any thread exists, and
1370
+ a table one thread fills is probed by another under the same key. */
1371
+ uint64_t nish_hash_key[2];
1372
+
1222
1373
  void nish_argv_init(int32_t argc, char **argv) {
1223
1374
  nish_array *a = malloc(sizeof *a + argc * sizeof(nish_str *));
1224
- nish_str **d = (nish_str **)(a + 1);
1225
1375
  if (!a) nish_oom();
1376
+ nish_str **d = (nish_str **)(a + 1);
1226
1377
  *a = (nish_array){ argc, argc, (char *)d };
1227
1378
  nish_argv = a;
1228
1379
  while (argc-- > 0) {
@@ -1273,13 +1424,46 @@ double nish_parse_number(const nish_str *s, int32_t mode) {
1273
1424
  * line, every one of those rows runs fewer instructions than it did before, and
1274
1425
  * the call costs nothing on a path that runs once per doubling. */
1275
1426
  __attribute__((noinline)) void nish_array_grow(nish_array *a, uint64_t elem_size) {
1427
+ #ifdef NISH_ASAN
1428
+ /* One more slot, not double (see `nish_grown`): in place while a normal build
1429
+ would still have room, moved where it would reallocate. */
1430
+ uint64_t native = a->cap, cap = a->len + 1;
1431
+ nish_grown_enter();
1432
+ struct nish_grown *g = nish_grown_find(a->data);
1433
+ if (g && g->owner == a) native = g->native;
1434
+ else g = 0;
1435
+ if (g && cap <= native) {
1436
+ nish_grown_leave();
1437
+ __asan_unpoison_memory_region(a->data + a->len * elem_size, elem_size);
1438
+ a->cap = cap;
1439
+ return;
1440
+ }
1441
+ uint64_t room = native ? native * 2 : 4;
1442
+ if (room > NISH_ARRAY_MAX) room = NISH_ARRAY_MAX;
1443
+ #else
1276
1444
  uint64_t cap = a->cap ? a->cap * 2 : 4;
1445
+ #endif
1277
1446
  if (cap > NISH_ARRAY_MAX) {
1278
1447
  if (a->len >= NISH_ARRAY_MAX) nish_oom();
1279
1448
  cap = NISH_ARRAY_MAX;
1280
1449
  }
1450
+ #ifdef NISH_ASAN
1451
+ if (room < cap) room = cap;
1452
+ char *data = nish_alloc_struct(room * elem_size);
1453
+ NISH_POISON(data + cap * elem_size, (room - cap) * elem_size);
1454
+ #else
1281
1455
  char *data = nish_alloc_struct(cap * elem_size);
1456
+ #endif
1282
1457
  if (a->len) memcpy(data, a->data, a->len * elem_size);
1458
+ #ifdef NISH_ASAN
1459
+ /* The old block is dead: a pointer kept across the push now reads poison.
1460
+ Only a heap block, as a literal's may be on the stack or a constant. */
1461
+ const char *kind = a->cap ? __asan_locate_address(a->data, 0, 0, 0, 0) : 0;
1462
+ if (kind && !strcmp(kind, "heap")) NISH_POISON(a->data, (g ? native : a->cap) * elem_size);
1463
+ if (g) nish_grown_drop(a->data);
1464
+ nish_grown_put(data, a, room);
1465
+ nish_grown_leave();
1466
+ #endif
1283
1467
  a->data = data;
1284
1468
  a->cap = cap;
1285
1469
  }
@@ -1301,7 +1485,7 @@ __attribute__((noinline)) void nish_wipe(nish_array *bytes) {
1301
1485
  void nish_panic_index(uint64_t idx, uint64_t len) {
1302
1486
  nish_panic_claim();
1303
1487
  dprintf(2, "index out of range: %" PRIu64 " >= %" PRIu64 "\n", idx, len);
1304
- _exit(1);
1488
+ nish_fatal_exit();
1305
1489
  }
1306
1490
 
1307
1491
  /* `s.slice(start, end)` (WP15 section 4) with a range the string does not
@@ -1315,7 +1499,7 @@ void nish_panic_index(uint64_t idx, uint64_t len) {
1315
1499
  void nish_panic_slice(int64_t start, int64_t end, int64_t len) {
1316
1500
  nish_panic_claim();
1317
1501
  dprintf(2, "slice out of range: [%" PRId64 ", %" PRId64 ") of length %" PRId64 "\n", start, end, len);
1318
- _exit(1);
1502
+ nish_fatal_exit();
1319
1503
  }
1320
1504
 
1321
1505
  /* ---- Checked arithmetic (Rust semantics): the failed-check paths. One table