@amritk/nish-x86_64-linux 0.15.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.
- package/INSTALL.md +15 -6
- package/bin/nish +0 -0
- package/package.json +1 -1
- package/runtime/nish.h +39 -5
- package/runtime/runtime-host.c +29 -2
- package/runtime/runtime-net.c +15 -9
- package/runtime/runtime-os.c +77 -37
- package/runtime/runtime-parallel.c +5 -1
- package/runtime/runtime-wasm.c +25 -2
- package/runtime/runtime.c +40 -6
- package/std/README.md +44 -19
- package/std/crypto/aes.ts +53 -13
- package/std/crypto/base64url.ts +18 -5
- package/std/crypto/chacha20poly1305.ts +9 -2
- package/std/crypto/ct.ts +8 -2
- package/std/crypto/hkdf.ts +21 -5
- package/std/crypto/hmac.ts +18 -2
- package/std/crypto/p256.ts +5 -0
- package/std/crypto/sha256.ts +12 -1
- package/std/crypto/sha512.ts +16 -2
- package/std/crypto/x25519.ts +4 -3
- package/std/crypto/x509.ts +143 -26
package/INSTALL.md
CHANGED
|
@@ -403,8 +403,15 @@ release is expected to move it. `--stages 1` stops one link sooner.
|
|
|
403
403
|
and `--profile` spellings as an installed `nish`, because it is the same
|
|
404
404
|
program, and makes every directory in the way of the IR, a sidecar or the
|
|
405
405
|
binary itself. It looks for `scripts/build.sh` and the two `runtime/*.c` files
|
|
406
|
-
one level up from wherever it was invoked
|
|
407
|
-
it wants a checkout or an installed package
|
|
406
|
+
one level up from wherever it was invoked (and one level up from the file a
|
|
407
|
+
symbolic link to it names), so it wants a checkout or an installed package
|
|
408
|
+
around it the way `nish` does. It falls back to the working directory only
|
|
409
|
+
when the compiler itself lies inside that directory, so a checkout you merely
|
|
410
|
+
`cd` into cannot hand it a `scripts/build.sh` to run. Keep `bin/nish` beside
|
|
411
|
+
`scripts/`, `runtime/` and `std/`, as every install above does, and keep that
|
|
412
|
+
directory's parent writable only by you: the package root is the parent of
|
|
413
|
+
the compiler's directory, so a compiler at `/tmp/x/nish` would trust
|
|
414
|
+
`/tmp/scripts/build.sh` (CLI-9 in [docs/security/cli.md](security/cli.md)).
|
|
408
415
|
|
|
409
416
|
Because the seed is the last release, `src/` may only *use* in its own
|
|
410
417
|
source the constructs that release compiles. A new construct is implemented
|
|
@@ -457,9 +464,11 @@ nish run hello.ts
|
|
|
457
464
|
`$XDG_CACHE_HOME/nish/run` (or `~/.cache/nish/run`) when that program has not
|
|
458
465
|
been linked before, and starts it with the arguments after the file. The first
|
|
459
466
|
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
|
-
|
|
462
|
-
directory.
|
|
467
|
+
It needs clang only for that link. It needs `HOME` or `XDG_CACHE_HOME` set
|
|
468
|
+
to an absolute path (a relative one is ignored, as if it were unset), and
|
|
469
|
+
refuses the run without one rather than keep a binary in a shared directory.
|
|
470
|
+
The first run that builds into the cache makes its root `0700`, so no other
|
|
471
|
+
user can plant a binary in it ([docs/security/cli.md](security/cli.md)).
|
|
463
472
|
|
|
464
473
|
## 4. Exit codes
|
|
465
474
|
|
|
@@ -469,7 +478,7 @@ directory.
|
|
|
469
478
|
| 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
479
|
| 2 | usage error: unknown flag, missing argument, no input files |
|
|
471
480
|
| 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 |
|
|
481
|
+
| | `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
482
|
| 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
483
|
|
|
475
484
|
## Troubleshooting
|
package/bin/nish
CHANGED
|
Binary file
|
package/package.json
CHANGED
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.
|
|
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,6 +214,11 @@ 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
|
|
@@ -254,12 +279,12 @@ nish_array *nish_read_file_bytes(const nish_str *path);
|
|
|
254
279
|
* `128 + n` when signal `n` killed it. -1 when `argv` is empty, when the
|
|
255
280
|
* child cannot be started or waited for, and always under WASI, which has no
|
|
256
281
|
* processes. The elements are `nish_str *`; the child receives their bytes,
|
|
257
|
-
*
|
|
282
|
+
* and an argument containing a NUL is refused (-1) rather than truncated. */
|
|
258
283
|
int32_t nish_spawn(const nish_array *argv);
|
|
259
284
|
/* `spawnSyncTo(argv, stdoutPath, stderrPath)`: exactly `nish_spawn` — the same
|
|
260
285
|
* `PATH` search, the same wait, the same status, `128 + n` and -1 — except that
|
|
261
286
|
* 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
|
|
287
|
+
* `nish_write_file` would leave it, a symbolic link at the path refused. An **empty** string leaves that stream
|
|
263
288
|
* inherited, so one call can capture stdout and let stderr through to the
|
|
264
289
|
* terminal. The child opens the files, so a failed open is a child that could
|
|
265
290
|
* not start and answers -1 rather than leaving this process redirected. The two
|
|
@@ -275,8 +300,8 @@ int32_t nish_spawn_to(const nish_array *argv, const nish_str *out, const nish_st
|
|
|
275
300
|
* arena (so a later `setenv` cannot change a string the program still holds),
|
|
276
301
|
* or NULL when it is unset — the language's `string | null`. An empty value
|
|
277
302
|
* is a set variable and answers a zero-length string, not NULL. `name` is
|
|
278
|
-
* read and never retained; a
|
|
279
|
-
*
|
|
303
|
+
* read and never retained; a name holding a NUL is unset, as it is for every
|
|
304
|
+
* other path-like argument here. */
|
|
280
305
|
nish_str *nish_getenv(const nish_str *name);
|
|
281
306
|
|
|
282
307
|
/* ---- Symlinks (WP19 §5a item 4) -----------------------------------------
|
|
@@ -336,6 +361,15 @@ double nish_stat_mtime(const nish_str *path);
|
|
|
336
361
|
* or a read that fails. */
|
|
337
362
|
int32_t nish_signal_fd(void);
|
|
338
363
|
int32_t nish_read_signal(int32_t fd);
|
|
364
|
+
/* Who owns a path, and whether it runs (docs/security/runtime.md, RT-9; no
|
|
365
|
+
* builtin reaches these yet). `nish_lstat_owner_mode(path)`: `lstat`, so a
|
|
366
|
+
* symbolic link answers for itself, as the owner's uid in the high 32 bits
|
|
367
|
+
* and `st_mode` in the low 32, or exactly -1 when the path does not resolve.
|
|
368
|
+
* `nish_euid()`: the effective uid. `nish_is_executable(path)`:
|
|
369
|
+
* `access(path, X_OK)`, for the real uid. */
|
|
370
|
+
int64_t nish_lstat_owner_mode(const nish_str *path);
|
|
371
|
+
int64_t nish_euid(void);
|
|
372
|
+
bool nish_is_executable(const nish_str *path);
|
|
339
373
|
|
|
340
374
|
/* ---- The network (WP34 N5), runtime/runtime-net.c, `nish:net` ------------
|
|
341
375
|
* Every call answers an `int32_t`: `>= 0` on success (a descriptor, a byte
|
package/runtime/runtime-host.c
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/* Nish runtime, the host half: the wall clock, entropy, a file's modification
|
|
2
2
|
* time and a descriptor that becomes readable when SIGTERM or SIGINT arrives
|
|
3
3
|
* (WP34 N3) — the four facts a program that runs for days asks of the machine
|
|
4
|
-
* it runs on
|
|
4
|
+
* it runs on — and who owns a path and whether it runs, which a driver asks
|
|
5
|
+
* before it trusts one.
|
|
5
6
|
*
|
|
6
7
|
* A translation unit of its own for the reason runtime-os.c is one: each file
|
|
7
8
|
* carries its own measured `.text*` ceiling in tests/run.js. runtime-os.c had
|
|
@@ -33,6 +34,7 @@ typedef int nish_host_unused;
|
|
|
33
34
|
#include <signal.h>
|
|
34
35
|
#include <stdint.h>
|
|
35
36
|
#include <stdio.h>
|
|
37
|
+
#include <string.h>
|
|
36
38
|
#include <sys/stat.h>
|
|
37
39
|
#include <time.h>
|
|
38
40
|
#include <unistd.h>
|
|
@@ -98,7 +100,7 @@ void nish_random_fill(nish_array *bytes) {
|
|
|
98
100
|
digits. `stat` follows a symbolic link, as `fs.statSync` does. */
|
|
99
101
|
double nish_stat_mtime(const nish_str *path) {
|
|
100
102
|
struct stat st;
|
|
101
|
-
if (stat(path
|
|
103
|
+
if (stat(nish_cpath(path), &st) != 0) return __builtin_nan("");
|
|
102
104
|
#if defined(__APPLE__)
|
|
103
105
|
struct timespec t = st.st_mtimespec;
|
|
104
106
|
#else
|
|
@@ -107,6 +109,31 @@ double nish_stat_mtime(const nish_str *path) {
|
|
|
107
109
|
return (double)t.tv_sec * 1e3 + (double)t.tv_nsec / 1e6;
|
|
108
110
|
}
|
|
109
111
|
|
|
112
|
+
/* ---- Who owns a path, and whether it runs (docs/security/runtime.md, RT-9)
|
|
113
|
+
*
|
|
114
|
+
* The primitives a driver needs before it trusts a directory or a program it
|
|
115
|
+
* found on its own. `nish_lstat_owner_mode` answers for the path itself, a
|
|
116
|
+
* symbolic link included, rather than for what it names: the owner's uid in
|
|
117
|
+
* the high 32 bits and `st_mode` in the low 32, or exactly -1 when the path
|
|
118
|
+
* does not resolve or holds a NUL. `nish_euid` is the user to compare the
|
|
119
|
+
* owner with, and `nish_is_executable` is `access(X_OK)`: whether the real
|
|
120
|
+
* user may run the file, which is what tells an executable `PATH` entry from
|
|
121
|
+
* one that is only readable. Nothing in the language reaches them yet: `src/` may call
|
|
122
|
+
* a runtime function only once the last release declares it, so they are here
|
|
123
|
+
* for the change after the next release (docs/security/cli.md, CLI-7 and
|
|
124
|
+
* CLI-9). */
|
|
125
|
+
int64_t nish_lstat_owner_mode(const nish_str *path) {
|
|
126
|
+
struct stat st;
|
|
127
|
+
if (lstat(nish_cpath(path), &st) != 0) return -1;
|
|
128
|
+
return (int64_t)((uint64_t)(uint32_t)st.st_uid << 32 | (uint32_t)st.st_mode);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
int64_t nish_euid(void) { return (int64_t)geteuid(); }
|
|
132
|
+
|
|
133
|
+
_Bool nish_is_executable(const nish_str *path) {
|
|
134
|
+
return access(nish_cpath(path), X_OK) == 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
110
137
|
/* ---- Signals: `signalFd()` and `readSignal(fd)`
|
|
111
138
|
*
|
|
112
139
|
* A descriptor rather than a handler in the language, because a handler would
|
package/runtime/runtime-net.c
CHANGED
|
@@ -229,18 +229,24 @@ int32_t nish_tcp_listen(const nish_str *host, int32_t port, int32_t backlog) {
|
|
|
229
229
|
return nish_net_bound(host, port, SOCK_STREAM, backlog);
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
/* A socket address into the caller's 18-byte form. An IPv4 peer
|
|
233
|
-
mapped, and the port is already in network order, which is the
|
|
234
|
-
big-endian.
|
|
235
|
-
|
|
232
|
+
/* A socket address of `n` bytes into the caller's 18-byte form. An IPv4 peer
|
|
233
|
+
is written mapped, and the port is already in network order, which is the
|
|
234
|
+
form's big-endian. Anything else -- no address at all, which is what
|
|
235
|
+
`recvmsg` reports on a connected or Unix socket, or a family that is not
|
|
236
|
+
IP -- is eighteen zeros: those bytes of `s` were never written, and copying
|
|
237
|
+
them handed the caller whatever the stack held (docs/security/runtime.md,
|
|
238
|
+
RT-6). */
|
|
239
|
+
static void nish_net_put(nish_array *out, const nish_sockaddr *s, socklen_t n) {
|
|
236
240
|
unsigned char *p = (unsigned char *)out->data;
|
|
237
|
-
uint16_t port =
|
|
238
|
-
|
|
241
|
+
uint16_t port = 0;
|
|
242
|
+
memset(p, 0, NISH_ADDR_BYTES);
|
|
243
|
+
if (n >= sizeof s->v4 && s->sa.sa_family == AF_INET) {
|
|
239
244
|
memcpy(p, nish_mapped_prefix, 12);
|
|
240
245
|
memcpy(p + 12, &s->v4.sin_addr, 4);
|
|
241
246
|
port = s->v4.sin_port;
|
|
242
|
-
} else {
|
|
247
|
+
} else if (n >= sizeof s->v6 && s->sa.sa_family == AF_INET6) {
|
|
243
248
|
memcpy(p, &s->v6.sin6_addr, 16);
|
|
249
|
+
port = s->v6.sin6_port;
|
|
244
250
|
}
|
|
245
251
|
memcpy(p + 16, &port, 2);
|
|
246
252
|
}
|
|
@@ -259,7 +265,7 @@ int32_t nish_tcp_accept(int32_t fd, nish_array *peer) {
|
|
|
259
265
|
int c = nish_net_setup(accept(fd, &s.sa, &n));
|
|
260
266
|
#endif
|
|
261
267
|
if (c < 0) return nish_net_fail();
|
|
262
|
-
nish_net_put(peer, &s);
|
|
268
|
+
nish_net_put(peer, &s, n);
|
|
263
269
|
return c;
|
|
264
270
|
}
|
|
265
271
|
|
|
@@ -399,7 +405,7 @@ int32_t nish_udp_recv_from(int32_t fd, nish_array *buf, int64_t off, int64_t len
|
|
|
399
405
|
}
|
|
400
406
|
#endif
|
|
401
407
|
memcpy(meta->data, info, sizeof info);
|
|
402
|
-
nish_net_put(from, &s);
|
|
408
|
+
nish_net_put(from, &s, m.msg_namelen);
|
|
403
409
|
return (int32_t)got;
|
|
404
410
|
}
|
|
405
411
|
|
package/runtime/runtime-os.c
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
/*
|
|
256
|
-
a
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
|
294
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/runtime/runtime-wasm.c
CHANGED
|
@@ -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 &&
|
|
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
|
-
|
|
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
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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;
|