@amritk/nish-x86_64-linux 0.14.0 → 0.16.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.
@@ -52,6 +52,14 @@ extern char **environ;
52
52
 
53
53
  #include "nish.h"
54
54
 
55
+ /* Darwin's <fcntl.h> declares `O_NOFOLLOW` only at its full C level, and the
56
+ strict level the two macros above select is the one that binds the right
57
+ `realpath`, so the flag is spelled here with Darwin's own value
58
+ (<sys/fcntl.h>: 0x00000100). Linux and WASI declare it at POSIX 2008. */
59
+ #if defined(__APPLE__) && !defined(O_NOFOLLOW)
60
+ #define O_NOFOLLOW 0x00000100
61
+ #endif
62
+
55
63
  /* The same spelling runtime.c uses for its own cold paths: a function that
56
64
  reports and exits is never on a path worth optimising for. */
57
65
  #define NISH_COLD __attribute__((noreturn, cold, noinline))
@@ -63,6 +71,12 @@ static NISH_COLD void nish_io_fail(const char *what, const nish_str *path) {
63
71
  _exit(1);
64
72
  }
65
73
 
74
+ /* The longest string or array a read hands back: 2^31 - 1, the same limit
75
+ runtime.c's `NISH_ARRAY_MAX` puts on a push. Under --number-mode i32
76
+ `length` is an `i32`, so one byte more reads back negative and bounds-check
77
+ elimination trusts it (docs/security/runtime.md, RT-1). */
78
+ #define NISH_LENGTH_MAX 2147483647
79
+
66
80
  /* `readFileSyncOrNull(path)`: null rather than a message, so a program can
67
81
  turn a missing file into its own diagnostic and carry on.
68
82
  *
@@ -84,11 +98,16 @@ static NISH_COLD void nish_io_fail(const char *what, const nish_str *path) {
84
98
  * rather than `stat` on the path, so the answer is about the file that was
85
99
  * opened and not about whatever the name means a moment later. */
86
100
  nish_str *nish_read_file_or_null(const nish_str *path) {
87
- int fd = open(path->data, O_RDONLY);
101
+ int fd = open(nish_cpath(path), O_RDONLY);
88
102
  if (fd < 0) return 0;
89
103
  struct stat st;
90
104
  off_t len = fstat(fd, &st) == 0 && !S_ISDIR(st.st_mode) ? lseek(fd, 0, SEEK_END) : -1;
91
- if (len < 0) {
105
+ /* A file longer than a length can say is refused before anything is
106
+ allocated, as an unreadable one is: null here, null from
107
+ `readFileBytesSync`, and `cannot read` from `readFileSync`. The bytes read
108
+ below are never more than `len`, so a file that grows meanwhile cannot
109
+ pass the limit either. */
110
+ if (len < 0 || len > NISH_LENGTH_MAX) {
92
111
  close(fd);
93
112
  return 0;
94
113
  }
@@ -123,8 +142,14 @@ nish_str *nish_read_file(const nish_str *path) {
123
142
  return s;
124
143
  }
125
144
 
145
+ /* `O_NOFOLLOW` (docs/security/runtime.md, RT-4): a symbolic link as the last
146
+ component of the path is refused, as an unwritable file is, rather than
147
+ followed to whatever file it names. `nish file.ts` writes `file.ll` beside
148
+ its source, so a link planted at that name in a checkout would otherwise
149
+ truncate any file the user can write. A link among the directories on the
150
+ way is still followed. */
126
151
  static void nish_put_file(const nish_str *path, const nish_str *data, int flags) {
127
- int fd = open(path->data, O_WRONLY | O_CREAT | flags, 0644);
152
+ int fd = open(nish_cpath(path), O_WRONLY | O_CREAT | O_NOFOLLOW | flags, 0644);
128
153
  if (fd < 0) nish_io_fail("write ", path);
129
154
  for (uint64_t done = 0; done < data->len;) {
130
155
  ssize_t n = write(fd, data->data + done, data->len - done);
@@ -149,14 +174,14 @@ void nish_append_file(const nish_str *path, const nish_str *data) { nish_put_fil
149
174
  answer to that question. */
150
175
  _Bool nish_is_dir(const nish_str *path) {
151
176
  struct stat st;
152
- return stat(path->data, &st) == 0 && S_ISDIR(st.st_mode);
177
+ return stat(nish_cpath(path), &st) == 0 && S_ISDIR(st.st_mode);
153
178
  }
154
179
 
155
180
  /* One directory, not recursive. The retry is the `stat` above rather than
156
181
  `errno == EEXIST` so that a plain file at the path answers false, which is
157
182
  what the promise "a directory is there afterwards" means. */
158
183
  _Bool nish_mkdir(const nish_str *path) {
159
- return mkdir(path->data, 0777) == 0 || nish_is_dir(path);
184
+ return mkdir(nish_cpath(path), 0777) == 0 || nish_is_dir(path);
160
185
  }
161
186
 
162
187
  /* `posix_spawnp` is one libc call where fork/execvp/waitpid would be three,
@@ -168,7 +193,7 @@ _Bool nish_mkdir(const nish_str *path) {
168
193
  two arguments answered differently, so the argv vector, the wait and the
169
194
  signal convention are written once; it is `static`, so a program that spawns
170
195
  nothing still loses all three to `--gc-sections`. */
171
- static int32_t nish_spawn_impl(const nish_array *argv, const char *out, const char *err) {
196
+ static int32_t nish_spawn_impl(const nish_array *argv, const nish_str *out, const nish_str *err) {
172
197
  #ifdef __wasi__
173
198
  (void)argv;
174
199
  (void)out;
@@ -181,18 +206,27 @@ static int32_t nish_spawn_impl(const nish_array *argv, const char *out, const ch
181
206
  path out of here has anything to free. */
182
207
  char **v = nish_alloc_struct((argv->len + 1) * sizeof *v);
183
208
  uint64_t i = 0;
184
- for (; i < argv->len; i++) v[i] = s[i]->data;
209
+ /* An argument holding a NUL is refused rather than cut short at it, which
210
+ would run a different command line from the one the program built: it is
211
+ the one whose C string is not its own bytes. */
212
+ for (; i < argv->len; i++) {
213
+ v[i] = (char *)nish_cpath(s[i]);
214
+ if (v[i] != s[i]->data) return -1;
215
+ }
185
216
  v[i] = 0;
186
217
  posix_spawn_file_actions_t fa;
187
218
  posix_spawn_file_actions_t *fap = 0;
188
- if (out || err) {
189
- if (posix_spawn_file_actions_init(&fa)) return -1;
190
- fap = &fa;
219
+ const nish_str *to[2] = { out, err };
220
+ for (int k = 0; k < 2; k++) {
221
+ if (!to[k]) continue;
222
+ if (!fap) {
223
+ if (posix_spawn_file_actions_init(&fa)) return -1;
224
+ fap = &fa;
225
+ }
191
226
  /* The child opens the file, not this process: a redirect that this process
192
227
  performed would have to be undone afterwards, and a failed open would
193
228
  leave its own stdout pointing at the file. */
194
- if (out) posix_spawn_file_actions_addopen(fap, 1, out, O_WRONLY | O_CREAT | O_TRUNC, 0644);
195
- if (err) posix_spawn_file_actions_addopen(fap, 2, err, O_WRONLY | O_CREAT | O_TRUNC, 0644);
229
+ posix_spawn_file_actions_addopen(fap, k + 1, nish_cpath(to[k]), O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, 0644);
196
230
  }
197
231
  pid_t pid;
198
232
  int status;
@@ -210,14 +244,15 @@ int32_t nish_spawn(const nish_array *argv) { return nish_spawn_impl(argv, 0, 0);
210
244
  to a file, which is what comparing a program's output against a golden needs
211
245
  — `nish_spawn` answers a status and the output is gone. An **empty** path
212
246
  leaves that stream inherited, so one call can capture stdout and let stderr
213
- through to the terminal. Each file is created or truncated at 0644, as
214
- `writeFileSync` does.
247
+ through to the terminal. Each file is created or truncated at 0644 and a
248
+ symbolic link at its name is refused, as `writeFileSync` does; the child
249
+ then fails to start, and the answer is -1.
215
250
 
216
251
  Two streams must not name one path: each would be opened separately, with
217
252
  its own offset, and the two would overwrite each other rather than
218
253
  interleave. Capturing them apart and concatenating is the way to merge. */
219
254
  int32_t nish_spawn_to(const nish_array *argv, const nish_str *out, const nish_str *err) {
220
- return nish_spawn_impl(argv, out->len ? out->data : 0, err->len ? err->data : 0);
255
+ return nish_spawn_impl(argv, out->len ? out : 0, err->len ? err : 0);
221
256
  }
222
257
 
223
258
  /* `readdirSync(path)`: the listing a driver needs to discover its own inputs.
@@ -240,7 +275,7 @@ nish_array *nish_readdir(const nish_str *path) {
240
275
  (void)path;
241
276
  return 0;
242
277
  #else
243
- DIR *d = opendir(path->data);
278
+ DIR *d = opendir(nish_cpath(path));
244
279
  if (!d) return 0;
245
280
  nish_array *a = nish_alloc_struct(sizeof *a);
246
281
  *a = (nish_array){ 0, 0, 0 };
@@ -252,15 +287,30 @@ nish_array *nish_readdir(const nish_str *path) {
252
287
  ((nish_str **)a->data)[a->len++] = nish_str_new(n, strlen(n));
253
288
  }
254
289
  closedir(d);
255
- /* Insertion sort over pointers: a directory is short, and `qsort` would cost
256
- a comparator symbol and its unwind entry for a call that is never hot. */
290
+ /* Heapsort over pointers, in place. It was an insertion sort, on the theory
291
+ that a directory is short, but whoever fills the directory decides that:
292
+ 100,000 names took 2.5 billion compares and 12.5 s, where this takes 3.0
293
+ million and 0.07 s (docs/security/runtime.md, RT-5). It needs no scratch
294
+ memory, and unlike `qsort` costs no comparator symbol or unwind entry. */
257
295
  nish_str **v = (nish_str **)a->data;
258
- for (uint64_t i = 1; i < a->len; i++) {
259
- nish_str *s = v[i];
260
- uint64_t j = i;
261
- while (j && strcmp(v[j - 1]->data, s->data) > 0) {
262
- v[j] = v[j - 1];
263
- j--;
296
+ /* Each pass sifts one name down the max-heap `v[0, end)`: first every
297
+ parent from the middle up, which builds the heap, then the name the
298
+ largest is swapped out for, which shrinks the heap by one. */
299
+ uint64_t i = a->len / 2, end = a->len;
300
+ while (end > 1) {
301
+ nish_str *s;
302
+ if (i) {
303
+ s = v[--i];
304
+ } else {
305
+ s = v[--end];
306
+ v[end] = v[0];
307
+ }
308
+ uint64_t j = i, c;
309
+ while ((c = 2 * j + 1) < end) {
310
+ if (c + 1 < end && strcmp(v[c]->data, v[c + 1]->data) < 0) c++;
311
+ if (strcmp(s->data, v[c]->data) >= 0) break;
312
+ v[j] = v[c];
313
+ j = c;
264
314
  }
265
315
  v[j] = s;
266
316
  }
@@ -290,13 +340,8 @@ int64_t nish_monotonic_nanos(void) {
290
340
  string with the lifetime every other one has. NULL for an unset variable,
291
341
  which is the language's `string | null`. Contract: nish.h. */
292
342
  nish_str *nish_getenv(const nish_str *name) {
293
- const char *v = getenv(name->data);
294
- if (!v) return 0;
295
- uint64_t len = strlen(v);
296
- nish_str *s = nish_alloc_struct(8 + len + 1);
297
- s->len = len;
298
- memcpy(s->data, v, len + 1);
299
- return s;
343
+ const char *v = getenv(nish_cpath(name));
344
+ return v ? nish_str_new(v, strlen(v)) : 0;
300
345
  }
301
346
 
302
347
  /* ---- Symlinks (WP19 §5a item 4): `realpathSync(path)`, the one path
@@ -321,12 +366,7 @@ nish_str *nish_getenv(const nish_str *name) {
321
366
  it resolves. Contract: nish.h. */
322
367
  nish_str *nish_realpath(const nish_str *path) {
323
368
  char buf[PATH_MAX];
324
- if (!realpath(path->data, buf)) return 0;
325
- uint64_t len = strlen(buf);
326
- nish_str *s = nish_alloc_struct(8 + len + 1);
327
- s->len = len;
328
- memcpy(s->data, buf, len + 1);
329
- return s;
369
+ return realpath(nish_cpath(path), buf) ? nish_str_new(buf, strlen(buf)) : 0;
330
370
  }
331
371
 
332
372
  /* ---- What machine this is (WP14 §7a): `process.platform` and `process.arch`,
@@ -109,7 +109,11 @@ void nish_parallel_range(nish_par_body body, void *ctx, int64_t len, int64_t gra
109
109
  if (grain < 1) grain = 1;
110
110
  /* Never more threads than there is work for at the requested granularity,
111
111
  * and never more than the machine has. */
112
- int64_t chunks = (len + grain - 1) / grain;
112
+ /* Rounded up without `len + grain - 1`, which overflows -- undefined
113
+ behaviour, and in practice a negative count that ran the whole range on
114
+ one thread -- once `len` is within `grain` of 2^63
115
+ (docs/security/runtime.md, RT-8). */
116
+ int64_t chunks = len / grain + (len % grain != 0);
113
117
  int64_t n = nish_cpu_count();
114
118
  if (n > NISH_PAR_MAX) n = NISH_PAR_MAX;
115
119
  if (chunks < n) n = chunks;
@@ -40,7 +40,14 @@ _Static_assert(offsetof(struct nish_arena, chunks) == 24, "arena layout is ABI")
40
40
  extern unsigned char __heap_base;
41
41
  #define NISH_PAGE ((uint64_t)65536)
42
42
 
43
- /* Slow path of the inlined allocator (`size` is already 8-byte rounded). */
43
+ /* Slow path of the inlined allocator (`size` is already 8-byte rounded).
44
+ *
45
+ * `need` has to fit the 32-bit address space, and is checked before anything
46
+ * is grown (docs/security/runtime.md, RT-7). Without that, an `off + size` that
47
+ * wrapped 2^64 looked small and moved the offset backwards, and a page count
48
+ * past 2^32 truncated to the `uintptr_t` `memory.grow` takes: a request of
49
+ * 2^48 bytes grew nothing, claimed 2^48 bytes of capacity, and the next block
50
+ * landed at `buf + off` reduced mod 2^32, on top of live data. */
44
51
  void *nish_arena_grow(uint64_t size) {
45
52
  if (!nish_arena.buf) {
46
53
  uintptr_t base = ((uintptr_t)&__heap_base + 7) & ~(uintptr_t)7;
@@ -48,6 +55,7 @@ void *nish_arena_grow(uint64_t size) {
48
55
  nish_arena.cap = (uint64_t)__builtin_wasm_memory_size(0) * NISH_PAGE - base;
49
56
  }
50
57
  uint64_t need = nish_arena.off + size;
58
+ if (need < size || need > (uintptr_t)-1 - (uintptr_t)nish_arena.buf) __builtin_trap();
51
59
  if (need > nish_arena.cap) {
52
60
  uint64_t pages = (need - nish_arena.cap + NISH_PAGE - 1) / NISH_PAGE;
53
61
  if (__builtin_wasm_memory_grow(0, (uintptr_t)pages) == (uintptr_t)-1) __builtin_trap();
@@ -58,9 +66,12 @@ void *nish_arena_grow(uint64_t size) {
58
66
  return p;
59
67
  }
60
68
 
69
+ /* `size <= cap - off` rather than `off + size <= cap`, which a size near 2^64
70
+ wraps into a yes; a size that rounding wraps to 0 is trapped the same way. */
61
71
  void *nish_alloc_struct(uint64_t size) {
72
+ if (size > (uint64_t)-8) __builtin_trap();
62
73
  size = (size + 7) & ~(uint64_t)7;
63
- if (nish_arena.buf && nish_arena.off + size <= nish_arena.cap) {
74
+ if (nish_arena.buf && size <= nish_arena.cap - nish_arena.off) {
64
75
  void *p = nish_arena.buf + nish_arena.off;
65
76
  nish_arena.off += size;
66
77
  return p;
@@ -77,7 +88,15 @@ void nish_arena_release(uint64_t mark) { nish_arena.off = mark ? mark - (uintptr
77
88
  /* ---- Arrays: %struct.nish_array = type { i64, i64, i8* } */
78
89
  typedef struct nish_array { uint64_t len; uint64_t cap; char *data; } nish_array;
79
90
 
91
+ /* The length limit runtime.c's `nish_array_grow` and `nish_alloc_array` keep:
92
+ 2^31 - 1 elements, so an i32-mode `length` is never negative. Past it the
93
+ wasm runtime traps, as it does for every other failure. */
94
+ #define NISH_ARRAY_MAX 2147483647u
95
+
80
96
  nish_array *nish_alloc_array(uint64_t elem_size, uint64_t len) {
97
+ /* With `len` in range, `len * elem_size` cannot wrap for any element
98
+ smaller than 8 GiB, and a product past 4 GiB traps in the grow. */
99
+ if (len > NISH_ARRAY_MAX) __builtin_trap();
81
100
  nish_array *a = (nish_array *)nish_alloc_struct(sizeof *a);
82
101
  a->len = a->cap = len;
83
102
  a->data = (char *)nish_alloc_struct(len * elem_size);
@@ -86,6 +105,10 @@ nish_array *nish_alloc_array(uint64_t elem_size, uint64_t len) {
86
105
 
87
106
  void nish_array_grow(nish_array *a, uint64_t elem_size) {
88
107
  uint64_t cap = a->cap ? a->cap * 2 : 4;
108
+ if (cap > NISH_ARRAY_MAX) {
109
+ if (a->len >= NISH_ARRAY_MAX) __builtin_trap();
110
+ cap = NISH_ARRAY_MAX;
111
+ }
89
112
  char *data = (char *)nish_alloc_struct(cap * elem_size);
90
113
  if (a->len) __builtin_memcpy(data, a->data, (uintptr_t)(a->len * elem_size)); /* memory.copy (-mbulk-memory) */
91
114
  a->data = data;
package/runtime/runtime.c CHANGED
@@ -83,7 +83,13 @@ static NISH_COLD void nish_die(const char *msg) {
83
83
  (void)!write(2, msg, strlen(msg));
84
84
  _exit(1);
85
85
  }
86
- #define nish_oom() nish_die("nish: out of memory\n")
86
+ /* A function rather than a macro around `nish_die`: seven call sites each
87
+ loading the message's address cost more than one more symbol does. */
88
+ static NISH_COLD void nish_oom(void) { nish_die("nish: out of memory\n"); }
89
+
90
+ /* The most elements an array and the most bytes a string may hold: see
91
+ `nish_array_grow` and `nish_str_concat`. */
92
+ #define NISH_ARRAY_MAX 2147483647u
87
93
 
88
94
  /* Slow path (size 8-byte rounded): push a chunk (>= 64 KB), bump from it. */
89
95
  void *nish_arena_grow(uint64_t size) {
@@ -196,9 +202,16 @@ nish_str *nish_str_new(const char *bytes, uint64_t len) {
196
202
  return s;
197
203
  }
198
204
 
205
+ /* A string never passes 2^31 - 1 bytes, for the reason an array never passes
206
+ that many elements (`NISH_ARRAY_MAX`, below): under --number-mode i32 a
207
+ longer one's `length` reads back negative and bounds-check elimination
208
+ trusts it (docs/security/runtime.md, RT-2). `a + b` past it fails as an
209
+ allocation does. */
199
210
  nish_str *nish_str_concat(const nish_str *a, const nish_str *b) {
200
- nish_str *s = nish_alloc_struct(8 + a->len + b->len + 1);
201
- s->len = a->len + b->len;
211
+ uint64_t len = a->len + b->len;
212
+ if (len > NISH_ARRAY_MAX) nish_oom();
213
+ nish_str *s = nish_alloc_struct(8 + len + 1);
214
+ s->len = len;
202
215
  memcpy(s->data, a->data, a->len);
203
216
  memcpy(s->data + a->len, b->data, b->len);
204
217
  s->data[s->len] = 0;
@@ -1106,8 +1119,12 @@ double nish_random(void) {
1106
1119
  /* ---- Arrays: %struct.nish_array = type { i64, i64, i8* }, cold paths */
1107
1120
  typedef struct nish_array { uint64_t len; uint64_t cap; char *data; } nish_array;
1108
1121
 
1109
- /* Host entry (WP8): `len` uninitialised elements, len == cap */
1122
+ /* Host entry (WP8): `len` uninitialised elements, len == cap. A `len` past
1123
+ `NISH_ARRAY_MAX` fails as an allocation does, which also keeps
1124
+ `len * elem_size` from wrapping into a short block behind a long header
1125
+ (docs/security/runtime.md, RT-7). */
1110
1126
  nish_array *nish_alloc_array(uint64_t elem_size, uint64_t len) {
1127
+ if (len > NISH_ARRAY_MAX) nish_oom();
1111
1128
  nish_array *a = nish_alloc_struct(sizeof *a);
1112
1129
  *a = (nish_array){ len, len, nish_alloc_struct(len * elem_size) };
1113
1130
  return a;
@@ -1160,9 +1177,26 @@ double nish_parse_number(const nish_str *s, int32_t mode) {
1160
1177
  * `sizeof` one element, which for a record element type (WP15 section 2a) is
1161
1178
  * the whole struct, so this relocates the elements themselves rather than a
1162
1179
  * block of pointers to them. Compiled code may therefore hold no pointer into
1163
- * `data` across a push, and the checker refuses the programs that would. */
1164
- void nish_array_grow(nish_array *a, uint64_t elem_size) {
1180
+ * `data` across a push, and the checker refuses the programs that would.
1181
+ *
1182
+ * A push never takes an array past 2^31 - 1 elements (docs/security/codegen.md,
1183
+ * K1-6). Under --number-mode i32 `length` is an `i32`, and one element more
1184
+ * read back as a negative or a short length everywhere, a hash or a tag
1185
+ * compare included. This file cannot tell which mode compiled its caller, so
1186
+ * the limit holds in both, and the push past it fails as an allocation does,
1187
+ * through `nish_oom`. `new Array(n)` checks its own `n` in the IR.
1188
+ *
1189
+ * `noinline`: under LTO the grown body was inlined into every push site, and
1190
+ * the extra compare moved register allocation in the hot loops around them
1191
+ * (+0.2% to +2% instructions over bench/instructions.json's map rows). Out of
1192
+ * line, every one of those rows runs fewer instructions than it did before, and
1193
+ * the call costs nothing on a path that runs once per doubling. */
1194
+ __attribute__((noinline)) void nish_array_grow(nish_array *a, uint64_t elem_size) {
1165
1195
  uint64_t cap = a->cap ? a->cap * 2 : 4;
1196
+ if (cap > NISH_ARRAY_MAX) {
1197
+ if (a->len >= NISH_ARRAY_MAX) nish_oom();
1198
+ cap = NISH_ARRAY_MAX;
1199
+ }
1166
1200
  char *data = nish_alloc_struct(cap * elem_size);
1167
1201
  if (a->len) memcpy(data, a->data, a->len * elem_size);
1168
1202
  a->data = data;
package/runtime/shim.mjs CHANGED
@@ -740,6 +740,21 @@ export function readSignal() {
740
740
  return signalFd();
741
741
  }
742
742
 
743
+ /**
744
+ * The `nish:net` functions (WP34 N5) have no faithful reading under Node
745
+ * either, for `signalFd`'s reason: Node's sockets are asynchronous only, and a
746
+ * socket becomes readable, writable or connected only to the event loop, which
747
+ * a program that owns its loop and spins or blocks in it never returns to. So
748
+ * each throws, naming itself, rather than answer -11 forever; `nish.mjs`
749
+ * installs one per name.
750
+ * docs/wp33-round-trip.md is the note that asks for one stated answer.
751
+ */
752
+ export function noNetReading(name) {
753
+ throw new Error(
754
+ `\`${name}\` has no synchronous reading under Node: a socket is ready only to the event loop, which a program that owns its loop never returns to (docs/wp33-round-trip.md)`
755
+ );
756
+ }
757
+
743
758
  /** `process.argv`: index 0 is the program (the script here, the executable natively), then the arguments. */
744
759
  export function argv() {
745
760
  return process.argv.slice(1);
package/scripts/build.sh CHANGED
@@ -3,12 +3,13 @@
3
3
  #
4
4
  # scripts/build.sh <module.ll> [more .ll/.c files...] -o <out> [--profile debug|speed|size|wasm]
5
5
  #
6
- # The C runtime is four translation units and is named as one: an input
6
+ # The C runtime is five translation units and is named as one: an input
7
7
  # <dir>/runtime.c also compiles <dir>/runtime-os.c, the half that wraps the
8
8
  # system calls (files, directories, subprocesses, the environment, the clock),
9
9
  # <dir>/runtime-parallel.c, the half that divides a range of work across
10
- # threads, and <dir>/runtime-host.c, the wall clock, entropy, file times and
11
- # signals. Each of those files says why they are compiled and measured apart.
10
+ # threads, <dir>/runtime-host.c, the wall clock, entropy, file times and
11
+ # signals, and <dir>/runtime-net.c, the sockets of `nish:net`. Each of those
12
+ # files says why they are compiled and measured apart.
12
13
  #
13
14
  # Profiles:
14
15
  # debug clang defaults: no optimisation, symbols kept. The "before" number.
@@ -84,9 +85,9 @@ done
84
85
  [ ${#inputs[@]} -gt 0 ] || { echo "error: no input files" >&2; exit 2; }
85
86
  [ -n "$out" ] || { echo "error: -o <out> is required" >&2; exit 2; }
86
87
 
87
- # The runtime is four translation units, and a caller names one: whoever passes
88
- # <dir>/runtime.c gets <dir>/runtime-os.c, <dir>/runtime-parallel.c and
89
- # <dir>/runtime-host.c compiled beside it. They were one file until the operating-system half was split out
88
+ # The runtime is five translation units, and a caller names one: whoever passes
89
+ # <dir>/runtime.c gets <dir>/runtime-os.c, <dir>/runtime-parallel.c,
90
+ # <dir>/runtime-host.c and <dir>/runtime-net.c compiled beside it. They were one file until the operating-system half was split out
90
91
  # for its own size budget, and the parallel half followed for the same reason
91
92
  # (each file's header comment says why), and a link line is where those splits
92
93
  # would otherwise leak: `nish --link` builds its command line in
@@ -98,7 +99,7 @@ done
98
99
  for i in ${inputs[@]+"${inputs[@]}"}; do
99
100
  case "$i" in
100
101
  */runtime.c|runtime.c)
101
- for half in runtime-os.c runtime-parallel.c runtime-host.c; do
102
+ for half in runtime-os.c runtime-parallel.c runtime-host.c runtime-net.c; do
102
103
  side="${i%runtime.c}$half"
103
104
  have=0
104
105
  for j in "${inputs[@]}"; do
package/std/README.md CHANGED
@@ -19,22 +19,46 @@ whatever program imports it, and subject to the same rules as `examples/` or
19
19
  ## `nish/crypto` — the primitives under TLS 1.3
20
20
 
21
21
  The first lanes of [WP34](../docs/wp34-hosting-cs.md) §5: K1's hashes, MACs and
22
- key derivation, and K4's key exchange, in pure Nish (decision S1), each module
23
- imported by its own specifier. Every one is written from its specification
24
- rather than ported, and reproduces that specification's published vectors in
25
- its `tests/link/crypto_*` programs. The performance gate compiles every module
26
- with no diagnostics under both `--number-mode i32` and `f64`, and the hashes,
27
- HMAC, HKDF and X25519 also run their vectors in `f64` (`crypto_*_f64`).
22
+ key derivation, K2's and K3's AEADs, K4's key exchange, K5's signatures and
23
+ K6's certificates, in
24
+ pure Nish (decision S1), each module imported by its own specifier. Every one
25
+ is written from its specification, with two exceptions that keep their
26
+ upstream notice: `crypto/aes.ts`'s `ghashMul32` is adapted from BearSSL, and
27
+ `crypto/p256.ts`'s field and scalar arithmetic is ported from fiat-crypto
28
+ ([`THIRD_PARTY_NOTICES.md`](../THIRD_PARTY_NOTICES.md)). Each reproduces its
29
+ specification's published vectors in its `tests/link/crypto_*` programs. The
30
+ performance gate compiles every module with no diagnostics under both
31
+ `--number-mode i32` and `f64`, and the hashes, HMAC, HKDF, X25519,
32
+ ChaCha20-Poly1305, AES-GCM, P-256 and X.509 also run their vectors in `f64`
33
+ (`crypto_*_f64`).
34
+
35
+ Every module here was security-audited (#363); the records are in
36
+ [`docs/security/`](../docs/security/README.md), and how to report a
37
+ vulnerability is in [`SECURITY.md`](../SECURITY.md). Two limits hold across the
38
+ modules. **Lengths stop at 2^31 − 1.** No array built by `push` or `new Array`,
39
+ and no array or string the runtime makes (a file read, a concatenation, a
40
+ template), can pass 2^31 − 1 elements or bytes, and the one-shot functions
41
+ below refuse a longer input as each row says. A string built by `join` still
42
+ can, and under `--number-mode i32` its `length` is then wrong, which nothing in
43
+ `std` can tell (CG-3, open): a program built in that mode must not hand these
44
+ functions anything built from such a string. **Secrets are not wiped.** Private keys,
45
+ nonces and intermediate state stay in arena memory after a call returns, until
46
+ that memory is reused, because the language has no store the optimiser is
47
+ barred from removing (ECC-2, X509-7, open).
28
48
 
29
49
  | Module | What it is | Reproduces |
30
50
  | --- | --- | --- |
31
- | [`crypto/sha256.ts`](./crypto/sha256.ts) | `sha256(data)`, and `Sha256`, a streaming hasher: `update(buf, off, len)` over a window of a `u8[]`, `copy()` for the hash of a prefix while the original keeps going, and `digest()`, a fresh 32-byte array. `SHA256_SIZE` and `SHA256_BLOCK` | FIPS 180-4 §6.2 |
32
- | [`crypto/sha512.ts`](./crypto/sha512.ts) | SHA-512 and SHA-384 on one compression function: `sha512` and `sha384`, and the streaming `Sha512` and `Sha384` with `Sha256`'s three methods; digests of 64 and 48 bytes. `SHA512_SIZE`, `SHA384_SIZE` and `SHA512_BLOCK` | FIPS 180-4 §6.4, §6.5 |
33
- | [`crypto/hmac.ts`](./crypto/hmac.ts) | `hmacSha256` and `hmacSha384`, the streaming `HmacSha256` and `HmacSha384` (keyed in the constructor, then `update` and `digest`), and `hmacSha256Verify` / `hmacSha384Verify`, which compare a received tag with `timingSafeEqual` | RFC 2104, RFC 4231 §4 |
34
- | [`crypto/hkdf.ts`](./crypto/hkdf.ts) | `hkdfExtractSha256` / `hkdfExtractSha384` (an empty salt is HashLen zeros) and `hkdfExpandSha256` / `hkdfExpandSha384`, which answer `null` for a length below zero or above 255 × HashLen. TLS 1.3's HKDF-Expand-Label is not here; it belongs with TLS | RFC 5869 §2, Appendix A |
35
- | [`crypto/ct.ts`](./crypto/ct.ts) | `timingSafeEqual(a, b)`, which reads every byte whatever it holds, and `timingSafeEqualAt(a, aOff, b, bOff, len)` over two windows, which answers `false` for a window outside its array. Two lengths that differ answer `false` at once, because a length is public | — |
36
- | [`crypto/base64url.ts`](./crypto/base64url.ts) | `base64urlEncode(data)` and `base64urlDecode(text)`, unpadded. Decoding is strict, so every byte string has one spelling: a `=`, a character outside the alphabet, a length of 1 mod 4 or nonzero unused low bits answer `null` | RFC 4648 §5, §10 |
37
- | [`crypto/x25519.ts`](./crypto/x25519.ts) | `x25519(scalar, u)` and `x25519Base(scalar)`, on ten 25.5-bit limbs in `i64`. Either answers `null` unless its arguments are `X25519_SIZE` (32) bytes; the scalar is clamped on a copy | RFC 7748 §5.2, §6.1 |
51
+ | [`crypto/sha256.ts`](./crypto/sha256.ts) | `sha256(data)`, and `Sha256`, a streaming hasher: `update(buf, off, len)` over a window of a `u8[]`, `copy()` for the hash of a prefix while the original keeps going, and `digest()`, a fresh 32-byte array. `SHA256_SIZE` and `SHA256_BLOCK`. `sha256` panics on an array longer than 2^31 − 1 bytes | FIPS 180-4 §6.2 |
52
+ | [`crypto/sha512.ts`](./crypto/sha512.ts) | SHA-512 and SHA-384 on one compression function: `sha512` and `sha384`, and the streaming `Sha512` and `Sha384` with `Sha256`'s three methods; digests of 64 and 48 bytes. `SHA512_SIZE`, `SHA384_SIZE` and `SHA512_BLOCK`. The one-shot `sha512` and `sha384` panic on an array longer than 2^31 − 1 bytes | FIPS 180-4 §6.4, §6.5 |
53
+ | [`crypto/hmac.ts`](./crypto/hmac.ts) | `hmacSha256` and `hmacSha384`, the streaming `HmacSha256` and `HmacSha384` (keyed in the constructor, then `update` and `digest`), and `hmacSha256Verify` / `hmacSha384Verify`, which compare a received tag with `timingSafeEqual`. The one-shot functions panic on a message longer than 2^31 − 1 bytes | RFC 2104, RFC 4231 §4 |
54
+ | [`crypto/hkdf.ts`](./crypto/hkdf.ts) | `hkdfExtractSha256` / `hkdfExtractSha384` (an empty salt is HashLen zeros) and `hkdfExpandSha256` / `hkdfExpandSha384`, which answer `null` for a length below zero or above 255 × HashLen, for an `info` longer than 2^31 − 1 bytes, and for a PRK shorter than HashLen. TLS 1.3's HKDF-Expand-Label is not here; it belongs with TLS | RFC 5869 §2, Appendix A |
55
+ | [`crypto/ct.ts`](./crypto/ct.ts) | `timingSafeEqual(a, b)`, which reads every byte whatever it holds, and `timingSafeEqualAt(a, aOff, b, bOff, len)` over two windows, which answers `false` for a window outside its array. Two lengths that differ answer `false` at once, because a length is public, and so does an array longer than 2^31 − 1 bytes | — |
56
+ | [`crypto/base64url.ts`](./crypto/base64url.ts) | `base64urlEncode(data)` and `base64urlDecode(text)`, unpadded. Decoding is strict, so every byte string has one spelling: a `=`, a character outside the alphabet, a length of 1 mod 4 or nonzero unused low bits answer `null`, as does a text longer than 2^31 − 1 bytes; `base64urlEncode` panics on such an array | RFC 4648 §5, §10 |
57
+ | [`crypto/x25519.ts`](./crypto/x25519.ts) | `x25519(scalar, u)` and `x25519Base(scalar)`, on ten 25.5-bit limbs in `i64`. Either answers `null` unless its arguments are `X25519_SIZE` (32) bytes; the scalar is clamped on a copy | RFC 7748 §5.2, §6.1, Wycheproof `x25519_test` |
58
+ | [`crypto/chacha20poly1305.ts`](./crypto/chacha20poly1305.ts) | `chacha20Poly1305Seal(key, nonce, aad, plaintext)`, which answers the ciphertext followed by the 16-byte tag, and `chacha20Poly1305Open`, which checks the whole tag before it decrypts anything and answers `null` for a message that does not authenticate. Beneath them `chacha20Block`, `chacha20`, `chacha20QuarterRound`, `poly1305` and `poly1305KeyGen`, and `chacha20HeaderMask`, QUIC's 5-byte header-protection mask. Every input of the wrong length answers `null` | RFC 8439 §2, RFC 9001 §5.4.4, Wycheproof `chacha20_poly1305` (all 325 cases) |
59
+ | [`crypto/aes.ts`](./crypto/aes.ts) | AES-128 and AES-256, bitsliced four blocks at a time with the Boyar–Peralta S-box circuit, so no table is read: `aesKey(key)` expands a 16- or 32-byte key into an `AesKey`, then `aesEncryptBlock`, `aesGcmSeal` / `aesGcmOpen` (tag last, checked in full before anything is decrypted; any non-empty IV) and `aesHeaderMask`. AES-192 is out of scope, and a wrong length answers `null`, as does an `AesKey` not made by `aesKey`; one whose H is zero opens nothing | FIPS 197, SP 800-38D, RFC 9001 §5.4.3, Wycheproof `aes_gcm` (all 316 cases; the 103 AES-192 ones check that the key is refused) |
60
+ | [`crypto/p256.ts`](./crypto/p256.ts) | ECDSA over P-256 with RFC 6979's deterministic nonces: `p256PublicKey(priv)` (65-byte uncompressed SEC1), `p256Sign` / `p256Verify` over a digest and `p256SignSha256` / `p256VerifySha256` over a message; a signature is `r` and `s`, 32 big-endian bytes each, not DER (`crypto/x509.ts`'s `x509DerSignatureRS` converts one). A malformed key, a point off the curve or an `r` or `s` out of range answers `null` or `false`, never a panic. ECDSA is malleable, and `p256Verify` accepts a high `s`: when `(r, s)` verifies, so does `(r, n − s)` | RFC 6979 A.2.5, Wycheproof `ecdsa_secp256r1_sha256`, `_sha256_p1363`, `_sha512` and `_sha512_p1363` |
61
+ | [`crypto/x509.ts`](./crypto/x509.ts) | DER, PEM and X.509 over P-256: `pemToDer` / `derToPem` (RFC 7468, padded standard base64, strict labels), `x509ParseP256PrivateKey` (SEC1 or PKCS#8), `x509ParseCertificate` / `x509ParseChain` into an `X509Certificate`, `x509VerifySignature` (ecdsa-with-SHA256 under an issuer's key), `x509DerSignatureRS` (a DER ECDSA-Sig-Value as the `r || s` `crypto/p256.ts` takes), `x509MintSelfSigned`, the 1-to-14-day self-signed P-256 certificate WebTransport's `serverCertificateHashes` accepts, and `x509CertificateHash`, its SHA-256. DER is read strictly — non-minimal lengths and integers, indefinite lengths and trailing bytes answer `null`. `x509ParseCertificate` and `x509ParseChain` parse and do not validate: there is no path validation, no extension is read or exposed, and no clock is consulted. `pemToDer` refuses a file in which any block, of any label, is malformed. `x509MintSelfSigned` refuses a common name that is not UTF-8 or holds a NUL, and its key and serial must come from `crypto.getRandomValues`, which the module cannot call itself without refusing wasm32 (X509-6) | X.690, RFC 5280, RFC 7468, RFC 5915, RFC 5208 / 5958; certificates checked against OpenSSL |
38
62
 
39
63
  Three rules hold across the modules:
40
64
 
@@ -45,16 +69,57 @@ Three rules hold across the modules:
45
69
  that is itself spent, so any `update` or `digest` on it panics. Either way,
46
70
  copy *before* `digest` when the computation has to go on.
47
71
  - **An all-zero X25519 result is returned, not refused.** It is what a
48
- low-order `u` gives, and RFC 7748 §6.1 leaves the check to the protocol; TLS
49
- 1.3 (WP34 T1) makes it. A key exchange outside TLS has to make it itself.
50
- - **Constant time by construction, not yet by proof.** No module branches on,
51
- or indexes by, a secret: comparisons OR the differences into one word and
52
- test it once, the ladder swaps with a mask and always runs 255 steps, and
53
- base64url maps characters by arithmetic on range masks rather than a table.
54
- Every branch is on a length, a loop counter or a bit position. What checks
55
- that the machine code kept that shape is WP34 N6 — the `ctSelect` / `ctEq`
56
- builtins behind an optimisation barrier, and a disassembly check — and it is
57
- not built yet, so this is the discipline and not a verified property.
72
+ low-order `u` gives, and RFC 7748 §6.1 leaves the check to the protocol.
73
+ Nothing in this tree makes that check yet (TLS 1.3, WP34 T1, is planned to),
74
+ so every caller doing a key exchange must refuse an all-zero answer itself,
75
+ with `timingSafeEqual` against 32 zero bytes, since the answer is a secret.
76
+ - **Constant time by construction, and verified where the check reaches.** No
77
+ module branches on, or indexes by, a secret: comparisons OR the differences
78
+ into one word and test it once, the ladder swaps with a mask and always runs
79
+ 255 steps, AES reads no table, P-256's window reads all sixteen entries and
80
+ keeps one by mask, and base64url maps characters by arithmetic on range masks
81
+ rather than a table. Every branch is on a length, a loop counter or a bit
82
+ position. WP34 N6 checks that the machine code kept that shape:
83
+ `tests/ct-asm.js` disassembles golden fixtures on x86-64 and aarch64 and
84
+ refuses a branch, a call, a secret-addressed load, or a divide or square
85
+ root of a secret (whose latency follows its operands) in them. It reads what
86
+ `clang -O2` emits for each target's baseline CPU, and only the functions a
87
+ fixture names: the fixtures are copies of each module's straight-line cores,
88
+ so the module functions themselves, and every loop, rest on review.
89
+ [`docs/security/ct-verification.md`](../docs/security/ct-verification.md)
90
+ lists every secret-handling function no fixture reads. What it holds, per
91
+ module, is what that module's header says:
92
+ - `tests/cases/ct_asm_mac` pins the two loop shapes the others are built
93
+ from, a full tag compare and a masked table read; `crypto/hmac.ts`'s
94
+ verifiers compare in the first shape.
95
+ - `tests/cases/ct_asm_k1_ct`: copies of `timingSafeEqual`'s loop at 32 and
96
+ 48 bytes and `timingSafeEqualAt`'s at a 16-byte window.
97
+ - `tests/cases/ct_asm_k1_base64url`: the two character maps verbatim, and
98
+ one encode group and one decode group. The length-driven loops, the
99
+ decoder's store guard and the encoder's string building remain discipline.
100
+ - `tests/cases/ct_asm_x25519`: X25519's field multiply, square, add,
101
+ subtract and multiply by a24, its conditional swap, and one step of the
102
+ Montgomery ladder, the check following each call into the field functions.
103
+ The 255-step loop that drives the ladder, the inversion and the encodings
104
+ remain discipline.
105
+ - `tests/cases/ct_asm_chacha20poly1305`: Poly1305's key clamp, one block,
106
+ the final reduction with `s`, and the tag compare. The ChaCha20 rounds and
107
+ the loops that drive both halves remain discipline.
108
+ - `tests/cases/ct_asm_aes`: one full bitsliced round, one GHASH multiply and
109
+ the tag compare. Packing, the key schedule and the loops over blocks
110
+ remain discipline.
111
+ - `tests/cases/ct_asm_p256`: fiat's field multiply, square, add and
112
+ subtract, its scalar multiply and its conditional move; the sixteen-entry
113
+ table read by a secret digit; the complete doubling and addition; and one
114
+ window step of the scalar multiplication (four doublings, the table read
115
+ and an addition), the check following each call into the field functions.
116
+ The 64-window loop, the table build, the inversions, the encodings and
117
+ RFC 6979's nonce derivation remain discipline.
118
+
119
+ The SHA-2 hashes and HKDF are additions, rotations and xors that branch
120
+ only on lengths, and are not in a fixture either; nor is `crypto/x509.ts`,
121
+ whose one secret, a private key, is base64-decoded by range masks and
122
+ otherwise only copied and handed to `crypto/p256.ts`.
58
123
 
59
124
  ## How a program imports it
60
125
 
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2016 Thomas Pornin <pornin@bolet.org>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
18
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
19
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015-2020 the fiat-crypto authors (see the AUTHORS file).
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.