@amritk/nish-x86_64-linux 0.28.0 → 0.30.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/runtime/nish.mjs CHANGED
@@ -104,9 +104,11 @@ provide("realpathSync", shim.realpathSync);
104
104
  provide("spawnSync", shim.spawnSync);
105
105
  provide("spawnSyncTo", shim.spawnSyncTo);
106
106
  provide("spawnPiped", shim.spawnPiped);
107
- // WP34 N12. `spawn` and `waitPid` throw, naming themselves: a child is reaped
108
- // by Node's event loop. `kill`, `availableParallelism` and `totalMemory` answer.
107
+ // WP34 N12. `spawn`, `spawnWithFds` and `waitPid` throw, naming themselves: a
108
+ // child is reaped by Node's event loop. `kill`, `availableParallelism` and
109
+ // `totalMemory` answer.
109
110
  provide("spawn", shim.spawn);
111
+ provide("spawnWithFds", shim.spawnWithFds);
110
112
  provide("kill", shim.kill);
111
113
  provide("waitPid", shim.waitPid);
112
114
  provide("availableParallelism", shim.availableParallelism);
@@ -147,16 +149,28 @@ for (const name of [
147
149
  "pollRemove",
148
150
  "pollWait",
149
151
  "netSetOption",
152
+ "netMss",
153
+ "unixListen",
154
+ "unixListenMode",
155
+ "unixConnect",
156
+ "unixAccept",
157
+ "unixSocketPair",
158
+ "sendFd",
159
+ "recvFd",
160
+ "peerCredentials",
150
161
  ]) {
151
162
  provide(name, () => shim.noNetReading(name));
152
163
  }
153
- // `netSetOption`'s option ids are plain numbers, so they read the same here
154
- // as natively: only the call that takes one throws.
164
+ // `netSetOption`'s option ids and the `NET_UNIX_` flag and kinds are plain
165
+ // numbers, so they read the same here as natively: only a call throws.
155
166
  provide("NET_TCP_NODELAY", 1);
156
167
  provide("NET_TCP_QUICKACK", 2);
157
168
  provide("NET_SO_KEEPALIVE", 3);
158
169
  provide("NET_SO_SNDBUF", 4);
159
170
  provide("NET_SO_RCVBUF", 5);
171
+ provide("NET_UNIX_UNLINK_STALE", 1);
172
+ provide("NET_UNIX_STREAM", 1);
173
+ provide("NET_UNIX_SEQPACKET", 2);
160
174
 
161
175
  // `nish:term`. Whether a descriptor is a terminal and how big stdout's is have
162
176
  // Node's answers; reading stdin, waiting for it and raw mode throw, each naming
@@ -18,7 +18,7 @@
18
18
  * here answers the failure's errno, negated. The codes a loop branches on are Linux's numbers on every platform (-11
19
19
  * would block, -95 unsupported, -32 the peer is gone, -104 reset, -98 the
20
20
  * address is in use, -111 refused, -110 timed out, -22 a bad argument, -88
21
- * not a socket), so
21
+ * not a socket, -107 not connected), so
22
22
  * `nish_net_err` translates Darwin's; any other failure is the host's own
23
23
  * `-errno`. Nothing here allocates: the addresses a call reads or writes are
24
24
  * the caller's `u8[]`.
@@ -30,20 +30,26 @@
30
30
  * (`EAFNOSUPPORT`, as in many containers) `::` falls back to IPv4's
31
31
  * `0.0.0.0`.
32
32
  *
33
- * Linux and Darwin differ in six places: `SOCK_NONBLOCK | SOCK_CLOEXEC` and
33
+ * Linux and Darwin differ in nine places: `SOCK_NONBLOCK | SOCK_CLOEXEC` and
34
34
  * `accept4` against a `socket` or `accept` followed by `fcntl`, `MSG_NOSIGNAL`
35
35
  * against the `SO_NOSIGPIPE` socket option (a write to a gone peer is -32 on
36
36
  * both, never SIGPIPE), the errno numbers, UDP's segmentation offload
37
37
  * (`UDP_SEGMENT`, `UDP_GRO`) and ECN marks, which Linux has and for which
38
38
  * Darwin answers -95, and the readiness loop, which is epoll on Linux and
39
- * kqueue on Darwin, and `TCP_QUICKACK`, which only Linux has (-95 on
40
- * Darwin). Only the sending side and the GRO bind say -95 there: a
39
+ * kqueue on Darwin, `TCP_QUICKACK`, which only Linux has (-95 on
40
+ * Darwin), and the MSS, which `netMss` reads from Linux's `TCP_INFO` and from
41
+ * `TCP_MAXSEG` on Darwin, a descriptor `recvFd` keeps, which Linux's
42
+ * `MSG_CMSG_CLOEXEC` makes close-on-exec as it arrives and Darwin marks with
43
+ * `fcntl` after `recvmsg` returns (the window RT-38 records), and
44
+ * `peerCredentials`, which reads Linux's `SO_PEERCRED` and Darwin's
45
+ * `getpeereid` with `LOCAL_PEERPID` (pid -1 where that refuses). Of the UDP calls, only the sending side and
46
+ * the GRO bind say -95 on Darwin: a
41
47
  * receive leaves `meta` at 0, 0, and a `meta[1]` of 0 is also what a real
42
48
  * Not-ECT datagram reads as, so a program learns that Darwin has no ECN from
43
49
  * the send, never from the receive. The Darwin branch is compiled by CI's Darwin bootstrap
44
50
  * rows and run by its `runtime-net (macos-latest)` job: tests/runtime-test.c and
45
51
  * the `net_` sections of tests/run.js, on a mac. A WASI build has none of this (the checker refuses
46
- * every `nish:net` call under a wasm target), so there the file is empty.
52
+ * every `nish:net` call on wasm but `netMss`, folded to -1), so the file is empty.
47
53
  */
48
54
  #if defined(__wasi__) || defined(__wasm__)
49
55
  /* ISO C wants at least one declaration in a translation unit. */
@@ -58,9 +64,13 @@ typedef int nish_net_unused;
58
64
  #include <fcntl.h>
59
65
  #include <netinet/in.h>
60
66
  #include <netinet/tcp.h>
67
+ #include <stddef.h>
61
68
  #include <stdint.h>
69
+ #include <stddef.h>
62
70
  #include <string.h>
63
71
  #include <sys/socket.h>
72
+ #include <sys/stat.h>
73
+ #include <sys/un.h>
64
74
  #include <unistd.h>
65
75
  #if defined(__linux__)
66
76
  #include <netinet/udp.h>
@@ -103,6 +113,9 @@ static int32_t nish_net_err(int e) {
103
113
  if (e == ECONNREFUSED) return -111;
104
114
  if (e == ETIMEDOUT) return -110;
105
115
  if (e == ENOTSOCK) return -88;
116
+ if (e == ENAMETOOLONG) return -36;
117
+ if (e == ELOOP) return -40;
118
+ if (e == ENOTCONN) return -107;
106
119
  #endif
107
120
  return -e;
108
121
  }
@@ -424,6 +437,35 @@ int32_t nish_net_set_option(int32_t fd, int32_t option, int32_t value) {
424
437
  return setsockopt(fd, level, name, &value, sizeof value) != 0 ? nish_net_fail() : 0;
425
438
  }
426
439
 
440
+ /* `netMss(fd)`: the segment size a connected TCP socket sends, the bytes of
441
+ payload one packet carries, or a plain -1 when there is none to read: not
442
+ an errno, because "no MSS" is one answer whatever the reason. A socket
443
+ with no peer is -1 first, as a fresh one answers the default 536 to both
444
+ options below. Linux's `TCP_INFO` answers the current `tcpi_snd_mss`; the
445
+ kernel copies at most the struct it has and says how much, so an answer
446
+ too short to reach the field is not read. `TCP_MAXSEG` is the fallback,
447
+ and Darwin's only reading. */
448
+ int32_t nish_net_mss(int32_t fd) {
449
+ struct sockaddr_storage peer;
450
+ socklen_t len = sizeof peer;
451
+ if (getpeername(fd, (struct sockaddr *)&peer, &len) != 0) return -1;
452
+ #if defined(TCP_INFO) && defined(__linux__)
453
+ struct tcp_info info;
454
+ memset(&info, 0, sizeof info);
455
+ len = sizeof info;
456
+ if (getsockopt(fd, IPPROTO_TCP, TCP_INFO, &info, &len) == 0 &&
457
+ len >= offsetof(struct tcp_info, tcpi_snd_mss) + sizeof info.tcpi_snd_mss &&
458
+ info.tcpi_snd_mss > 0 && info.tcpi_snd_mss <= INT32_MAX)
459
+ return (int32_t)info.tcpi_snd_mss;
460
+ #endif
461
+ #if defined(TCP_MAXSEG)
462
+ int mss = 0;
463
+ len = sizeof mss;
464
+ if (getsockopt(fd, IPPROTO_TCP, TCP_MAXSEG, &mss, &len) == 0 && len == sizeof mss && mss > 0) return mss;
465
+ #endif
466
+ return -1;
467
+ }
468
+
427
469
  /* `udpBind(host, port, flags)`: a datagram socket, non-blocking and
428
470
  close-on-exec, bound like `tcpListen`'s, with `SO_REUSEPORT` for flag 1
429
471
  and `UDP_GRO` for flag 2; any other bit is -22. */
@@ -689,4 +731,343 @@ int32_t nish_poll_wait(int32_t loop, nish_array *ready, int32_t timeout_ms) {
689
731
  return n;
690
732
  #endif
691
733
  }
734
+
735
+ /* The Unix-domain address `path` names into `s`, its length into `n`, and 1
736
+ or 0 for whether it is abstract; or the failure, before any socket exists.
737
+ An empty path or one with a NUL inside is -22, and so is a bare `@`. A path
738
+ too long for `sun_path` with its NUL is -36: the kernel would cut it short
739
+ or refuse it, and a cut one binds a file the program never named
740
+ (docs/security/runtime.md, RT-33). A leading `@` is Linux's abstract
741
+ namespace: the rest, at most `sizeof sun_path - 1` bytes, follows a NUL,
742
+ and the length is exact, because the name is every byte the length covers.
743
+ Darwin has no abstract namespace, so there `@` is -95. */
744
+ static int32_t nish_unix_addr(const nish_str *path, struct sockaddr_un *s, socklen_t *n) {
745
+ const char *p = path->data;
746
+ uint64_t len = path->len;
747
+ memset(s, 0, sizeof *s);
748
+ s->sun_family = AF_UNIX;
749
+ if (len == 0 || strlen(p) != len || (len == 1 && p[0] == '@')) return -22;
750
+ int abstract = p[0] == '@';
751
+ #if !defined(__linux__)
752
+ if (abstract) return -95;
753
+ #endif
754
+ if (len + !abstract > sizeof s->sun_path) return -36;
755
+ memcpy(s->sun_path, p, len);
756
+ if (abstract) s->sun_path[0] = 0;
757
+ *n = (socklen_t)(offsetof(struct sockaddr_un, sun_path) + len + !abstract);
758
+ #if !defined(__linux__)
759
+ s->sun_len = (unsigned char)*n;
760
+ #endif
761
+ return abstract;
762
+ }
763
+
764
+ /* Whether the socket file at `s` (`n` bytes) is stale, for `unixListen` flag
765
+ 1 after a bind found the path taken: 0 once it is unlinked, else the
766
+ failure. Anything but a socket is -17 and left alone. A probe connects to
767
+ it: a refusal means nothing listens, and only then is the file unlinked; a
768
+ connect that succeeds or would wait (a full backlog) is a live listener,
769
+ -98. The probe is a connection the live listener sees and that closes at
770
+ once. Between the probe and the unlink another process may bind the path,
771
+ and that file is the one unlinked: the socket's directory is the trust
772
+ boundary (RT-34). Darwin refuses a connect to a listener whose backlog is
773
+ full with the same `ECONNREFUSED` as one to a dead file, so the probe
774
+ cannot tell them apart there, and a socket file in the way is -95 and left
775
+ alone: the flag never removes a live listener's file. */
776
+ static int32_t nish_unix_stale(const struct sockaddr_un *s, socklen_t n) {
777
+ struct stat st;
778
+ if (lstat(s->sun_path, &st) != 0) return errno == ENOENT ? 0 : nish_net_fail();
779
+ if (!S_ISSOCK(st.st_mode)) return -17;
780
+ #if !defined(__linux__)
781
+ (void)n;
782
+ return -95;
783
+ #else
784
+ int probe = nish_net_socket(AF_UNIX, SOCK_STREAM);
785
+ if (probe < 0) return nish_net_fail();
786
+ int e = connect(probe, (const struct sockaddr *)s, n) == 0 ? 0 : errno;
787
+ close(probe);
788
+ if (e != ECONNREFUSED) return e == 0 || e == EAGAIN || e == EINPROGRESS ? -98 : nish_net_err(e);
789
+ return unlink(s->sun_path) != 0 && errno != ENOENT ? nish_net_fail() : 0;
790
+ #endif
791
+ }
792
+
793
+ /* A listening Unix stream socket at `path`, or the failure. A path that
794
+ exists is -98, unless flag 1 asks `nish_unix_stale` to clear a socket file
795
+ nothing listens on, after which the bind is tried once more; any other bit
796
+ is -22, and an abstract name ignores the flag, since no file is left
797
+ behind. A pathname socket's file gets `mode` between the bind and the
798
+ listen: a connect needs write permission on it, and until the listen every
799
+ connect is refused, so the umask's mode the bind gave it never admits
800
+ anyone (RT-35; `tests/cases/net_unix_paths` checks the order under umask
801
+ 0). A failure after the bind closes the socket and unlinks the
802
+ file it made, so no failure leaves a descriptor or a file behind
803
+ (RT-36). */
804
+ static int32_t nish_unix_bound(const nish_str *path, int32_t backlog, int32_t flags, int32_t mode) {
805
+ if ((uint32_t)flags > 1) return -22;
806
+ struct sockaddr_un s;
807
+ socklen_t n;
808
+ int32_t abstract = nish_unix_addr(path, &s, &n);
809
+ if (abstract < 0) return abstract;
810
+ int fd = nish_net_socket(AF_UNIX, SOCK_STREAM);
811
+ if (fd < 0) return nish_net_fail();
812
+ int bound = bind(fd, (struct sockaddr *)&s, n);
813
+ if (bound != 0 && errno == EADDRINUSE && (flags & 1) && !abstract) {
814
+ int32_t stale = nish_unix_stale(&s, n);
815
+ if (stale != 0) {
816
+ close(fd);
817
+ return stale;
818
+ }
819
+ bound = bind(fd, (struct sockaddr *)&s, n);
820
+ }
821
+ if (bound != 0) {
822
+ int e = errno;
823
+ close(fd);
824
+ return nish_net_err(e);
825
+ }
826
+ if ((!abstract && chmod(s.sun_path, (mode_t)mode) != 0) || listen(fd, backlog) != 0) {
827
+ int e = errno;
828
+ close(fd);
829
+ if (!abstract) unlink(s.sun_path);
830
+ return nish_net_err(e);
831
+ }
832
+ return fd;
833
+ }
834
+
835
+ /* `unixListen(path, backlog, flags)`: a listening Unix stream socket whose
836
+ file only its owner may connect to (0600). */
837
+ int32_t nish_unix_listen(const nish_str *path, int32_t backlog, int32_t flags) {
838
+ return nish_unix_bound(path, backlog, flags, 0600);
839
+ }
840
+
841
+ /* `unixListenMode(path, backlog, flags, mode)`: the same with the file's mode
842
+ chosen, 0 to 0o777 and no other bits; an abstract name has no file to give
843
+ one, so it is -22, as a mode outside the range is. */
844
+ int32_t nish_unix_listen_mode(const nish_str *path, int32_t backlog, int32_t flags, int32_t mode) {
845
+ if ((uint32_t)mode > 0777) return -22;
846
+ if (path->len > 0 && path->data[0] == '@') return -22;
847
+ return nish_unix_bound(path, backlog, flags, mode);
848
+ }
849
+
850
+ /* `unixConnect(path)`: a new socket, non-blocking and close-on-exec,
851
+ connecting to the listener at `path`. A Unix connect finishes at once or
852
+ fails; a full backlog is -11 on Linux (`EAGAIN`) and -111 on Darwin, which
853
+ answers it as it answers a dead file, and either closes the socket, so the
854
+ caller tries again later. `EINPROGRESS` is answered as `tcpConnect`
855
+ answers it: the descriptor, with `connectResult` to say how it went once it
856
+ is writable. Any other failure closes the socket and is answered. */
857
+ int32_t nish_unix_connect(const nish_str *path) {
858
+ struct sockaddr_un s;
859
+ socklen_t n;
860
+ int32_t abstract = nish_unix_addr(path, &s, &n);
861
+ if (abstract < 0) return abstract;
862
+ int fd = nish_net_socket(AF_UNIX, SOCK_STREAM);
863
+ if (fd < 0) return nish_net_fail();
864
+ if (connect(fd, (struct sockaddr *)&s, n) != 0 && errno != EINPROGRESS) {
865
+ int e = errno;
866
+ close(fd);
867
+ return nish_net_err(e);
868
+ }
869
+ return fd;
870
+ }
871
+
872
+ /* `unixAccept(fd)`: the next connection, non-blocking and close-on-exec; -11
873
+ when none is waiting. A Unix peer has no address worth the name (a client
874
+ is seldom bound), so none is written. */
875
+ int32_t nish_unix_accept(int32_t fd) {
876
+ #if defined(__linux__)
877
+ int c = accept4(fd, NULL, NULL, SOCK_NONBLOCK | SOCK_CLOEXEC);
878
+ #else
879
+ int c = nish_net_setup(accept(fd, NULL, NULL));
880
+ #endif
881
+ return c < 0 ? nish_net_fail() : c;
882
+ }
883
+
884
+ /* `unixSocketPair(kind, fds)`: two connected sockets into `fds[0]` and
885
+ `fds[1]`, non-blocking and close-on-exec, and 0. Kind 1 is a byte stream,
886
+ kind 2 keeps each write one message (`SOCK_SEQPACKET`), and any other kind,
887
+ or an `fds` shorter than two, is -22 before any socket exists. Darwin has
888
+ no Unix `SOCK_SEQPACKET`, so kind 2 there is -95. */
889
+ int32_t nish_unix_socket_pair(int32_t kind, nish_array *fds) {
890
+ if ((kind != 1 && kind != 2) || fds->len < 2) return -22;
891
+ int sv[2];
892
+ #if defined(__linux__)
893
+ if (socketpair(AF_UNIX, (kind == 1 ? SOCK_STREAM : SOCK_SEQPACKET) | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, sv) != 0)
894
+ return nish_net_fail();
895
+ #else
896
+ if (socketpair(AF_UNIX, kind == 1 ? SOCK_STREAM : SOCK_SEQPACKET, 0, sv) != 0)
897
+ return errno == EPROTONOSUPPORT || errno == EPROTOTYPE ? -95 : nish_net_fail();
898
+ nish_net_setup(sv[0]);
899
+ nish_net_setup(sv[1]);
900
+ #endif
901
+ int32_t *out = (int32_t *)fds->data;
902
+ out[0] = sv[0];
903
+ out[1] = sv[1];
904
+ return 0;
905
+ }
906
+
907
+ /* 0 when `fd` is a Unix-domain socket, the one kind a descriptor or a
908
+ credential passes over; else the refusal: -88 for a descriptor that is not
909
+ a socket, -95 for a socket of another family, and the host's failure for a
910
+ descriptor that is not open (-9). Asked before the call rather than read
911
+ from it, because a TCP socket does not refuse `SCM_RIGHTS` as such: Linux
912
+ answers -22 for the control message, and `SO_PEERCRED` on one succeeds with
913
+ no peer behind it. */
914
+ static int32_t nish_unix_only(int32_t fd) {
915
+ struct sockaddr_un s;
916
+ socklen_t n = sizeof s;
917
+ if (getsockname(fd, (struct sockaddr *)&s, &n) != 0) return nish_net_fail();
918
+ return s.sun_family == AF_UNIX ? 0 : -95;
919
+ }
920
+
921
+ /* The most descriptors one `recvFd` makes room for: one is kept and every
922
+ other is closed, so the room only decides how many the kernel installs to
923
+ be closed rather than drops itself (docs/security/runtime.md, RT-37). */
924
+ #define NISH_RECV_FDS 8
925
+
926
+ /* Linux marks a received descriptor close-on-exec as it arrives; Darwin has
927
+ no such flag, and `nish_recv_fd` marks it after (RT-38). */
928
+ #if defined(__linux__)
929
+ #define NISH_RECV_CLOEXEC MSG_CMSG_CLOEXEC
930
+ #else
931
+ #define NISH_RECV_CLOEXEC 0
932
+ #endif
933
+
934
+ /* `sendFd(sock, fd, buf, off, len)`: `len` bytes of `buf` from `off`, with
935
+ one `SCM_RIGHTS` copy of `fd` riding on them, and the bytes sent or the
936
+ failure. A descriptor travels with data, so `len` 0 is -22, and so is a
937
+ negative `fd`; one that is not open is the kernel's -9. Only a Unix socket
938
+ carries one (`nish_unix_only`). A gone peer is -32, as `netWrite`'s is.
939
+ The descriptor stays open here: the receiver gets its own. */
940
+ int32_t nish_send_fd(int32_t sock, int32_t fd, const nish_array *buf, int64_t off, int64_t len) {
941
+ if (len == 0 || fd < 0) return -22;
942
+ int32_t kind = nish_unix_only(sock);
943
+ if (kind != 0) return kind;
944
+ union {
945
+ struct cmsghdr h;
946
+ char room[CMSG_SPACE(sizeof(int))];
947
+ } c;
948
+ memset(&c, 0, sizeof c);
949
+ struct iovec v = {buf->data + off, (size_t)len};
950
+ struct msghdr m;
951
+ memset(&m, 0, sizeof m);
952
+ m.msg_iov = &v;
953
+ m.msg_iovlen = 1;
954
+ m.msg_control = c.room;
955
+ m.msg_controllen = sizeof c.room;
956
+ struct cmsghdr *h = CMSG_FIRSTHDR(&m);
957
+ h->cmsg_level = SOL_SOCKET;
958
+ h->cmsg_type = SCM_RIGHTS;
959
+ h->cmsg_len = CMSG_LEN(sizeof(int));
960
+ int one = fd;
961
+ memcpy(CMSG_DATA(h), &one, sizeof one);
962
+ ssize_t n = sendmsg(sock, &m, NISH_NOSIGNAL);
963
+ return n < 0 ? nish_net_fail() : (int32_t)n;
964
+ }
965
+
966
+ /* `recvFd(sock, buf, off, len, fd)`: at most `len` bytes into `buf` from
967
+ `off`, and the bytes read (0 at end of stream) or the failure. `fd[0]` is
968
+ the first descriptor the message carried, or -1 when it carried none; every
969
+ further one is closed before the answer, so a peer cannot make this process
970
+ hold descriptors it never asked for (RT-37). A message carrying more than
971
+ NISH_RECV_FDS has the rest dropped by the kernel (`MSG_CTRUNC`), and the
972
+ bytes and the first descriptor are answered under the same rule. `len` 0 is
973
+ -22, as `sendFd`'s is, and so is an empty `fd`, both before anything is
974
+ read. The descriptor kept is close-on-exec: from the receive on Linux
975
+ (`MSG_CMSG_CLOEXEC`), and from the `fcntl` after it on Darwin, where a child
976
+ another thread spawns in between inherits it (RT-38); a Darwin `fcntl`
977
+ that fails closes it and answers the failure, so no descriptor is handed
978
+ out without the flag. It is not made
979
+ non-blocking: its status flags are the open file's, which the sender
980
+ shares. */
981
+ int32_t nish_recv_fd(int32_t sock, nish_array *buf, int64_t off, int64_t len, nish_array *fd) {
982
+ if (len == 0 || fd->len < 1) return -22;
983
+ int32_t *out = (int32_t *)fd->data;
984
+ out[0] = -1;
985
+ int32_t kind = nish_unix_only(sock);
986
+ if (kind != 0) return kind;
987
+ union {
988
+ struct cmsghdr h;
989
+ char room[CMSG_SPACE(NISH_RECV_FDS * sizeof(int))];
990
+ } c;
991
+ struct iovec v = {buf->data + off, (size_t)len};
992
+ struct msghdr m;
993
+ memset(&m, 0, sizeof m);
994
+ m.msg_iov = &v;
995
+ m.msg_iovlen = 1;
996
+ m.msg_control = c.room;
997
+ m.msg_controllen = sizeof c.room;
998
+ ssize_t n = recvmsg(sock, &m, NISH_RECV_CLOEXEC);
999
+ if (n < 0) return nish_net_fail();
1000
+ int32_t err = 0;
1001
+ int kept = 0;
1002
+ for (struct cmsghdr *h = CMSG_FIRSTHDR(&m); h; h = CMSG_NXTHDR(&m, h)) {
1003
+ if (h->cmsg_level != SOL_SOCKET || h->cmsg_type != SCM_RIGHTS) continue;
1004
+ /* Counted from the header, but never past the room the kernel filled:
1005
+ a truncated message's last header may claim more than arrived. */
1006
+ size_t room = (size_t)((char *)m.msg_control + m.msg_controllen - (char *)CMSG_DATA(h));
1007
+ size_t claim = h->cmsg_len > CMSG_LEN(0) ? h->cmsg_len - CMSG_LEN(0) : 0;
1008
+ size_t count = (claim < room ? claim : room) / sizeof(int);
1009
+ for (size_t k = 0; k < count; k++) {
1010
+ int got;
1011
+ memcpy(&got, CMSG_DATA(h) + k * sizeof(int), sizeof got);
1012
+ if (!kept) {
1013
+ kept = 1;
1014
+ #if !defined(__linux__)
1015
+ if (fcntl(got, F_SETFD, FD_CLOEXEC) != 0) {
1016
+ err = nish_net_fail();
1017
+ close(got);
1018
+ continue;
1019
+ }
1020
+ #endif
1021
+ out[0] = got;
1022
+ } else {
1023
+ close(got);
1024
+ }
1025
+ }
1026
+ }
1027
+ return err != 0 ? err : (int32_t)n;
1028
+ }
1029
+
1030
+ /* `peerCredentials(fd, out)`: the process at the other end of a connected
1031
+ Unix socket, as `out[0..2]` = pid, uid, gid, and 0; an `out` shorter than
1032
+ three is -22 before anything is asked. The kernel recorded them when the
1033
+ connection was made (`connect`, or `socketpair` for a pair), so a peer that
1034
+ has since changed its uid, or passed the socket to another process, still
1035
+ reads as it was then (RT-39). A socket with no peer, listening or never
1036
+ connected, is -107 (ENOTCONN) on both: Linux's `SO_PEERCRED` would answer
1037
+ the listener's own credentials, or none, so `getpeername` is asked first;
1038
+ a peer that has since closed keeps its connect-time answer. Linux reads
1039
+ `SO_PEERCRED`. Darwin reads uid
1040
+ and gid from `getpeereid` and the pid from `LOCAL_PEERPID` where the SDK
1041
+ names it, else -1. */
1042
+ int32_t nish_peer_credentials(int32_t fd, nish_array *out) {
1043
+ if (out->len < 3) return -22;
1044
+ int32_t kind = nish_unix_only(fd);
1045
+ if (kind != 0) return kind;
1046
+ int32_t *o = (int32_t *)out->data;
1047
+ #if defined(__linux__)
1048
+ struct sockaddr_un a;
1049
+ socklen_t an = sizeof a;
1050
+ if (getpeername(fd, (struct sockaddr *)&a, &an) != 0) return nish_net_fail();
1051
+ struct ucred c;
1052
+ socklen_t n = sizeof c;
1053
+ if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &c, &n) != 0) return nish_net_fail();
1054
+ o[0] = (int32_t)c.pid;
1055
+ o[1] = (int32_t)c.uid;
1056
+ o[2] = (int32_t)c.gid;
1057
+ #else
1058
+ uid_t uid;
1059
+ gid_t gid;
1060
+ if (getpeereid(fd, &uid, &gid) != 0) return nish_net_fail();
1061
+ int32_t pid = -1;
1062
+ #if defined(LOCAL_PEERPID)
1063
+ pid_t p;
1064
+ socklen_t n = sizeof p;
1065
+ if (getsockopt(fd, SOL_LOCAL, LOCAL_PEERPID, &p, &n) == 0) pid = (int32_t)p;
1066
+ #endif
1067
+ o[0] = pid;
1068
+ o[1] = (int32_t)uid;
1069
+ o[2] = (int32_t)gid;
1070
+ #endif
1071
+ return 0;
1072
+ }
692
1073
  #endif
@@ -358,19 +358,23 @@ _Bool nish_mkdir(const nish_str *path) {
358
358
  value rather than through a child that has already run.
359
359
 
360
360
  `out` and `err` are paths for the child's stdout and stderr, or NULL to let
361
- it inherit this process's; `dup`, when not NULL, is three descriptors the
362
- child takes as its stdin, stdout and stderr instead (`spawnPiped`'s pipes). Every spawn builtin starts its child here, so the
363
- argv vector and the redirects are written once; it is `static`, so a program
361
+ it inherit this process's; `dup`, when not NULL, is `n` descriptors the
362
+ child takes as `base`, `base + 1`, ...: its stdin, stdout and stderr for
363
+ `spawnPiped`'s pipes, and 3 onward for `spawnWithFds`. Every spawn builtin
364
+ starts its child here, so the argv vector and the redirects are written once; it is `static`, so a program
364
365
  that spawns nothing loses it to `--gc-sections`. The answer is the child's
365
366
  pid, or a negative errno: -EINVAL for an empty argv or an argument holding a
366
367
  NUL, and `posix_spawnp`'s own error otherwise (-ENOENT for a command that is
367
368
  not on PATH, on glibc). */
368
- static int32_t nish_spawn_start(const nish_array *argv, const nish_str *out, const nish_str *err, const int *dup) {
369
+ static int32_t nish_spawn_start(const nish_array *argv, const nish_str *out, const nish_str *err, const int *dup,
370
+ int base, int n) {
369
371
  #ifdef __wasi__
370
372
  (void)argv;
371
373
  (void)out;
372
374
  (void)err;
373
375
  (void)dup;
376
+ (void)base;
377
+ (void)n;
374
378
  return -ENOSYS;
375
379
  #else
376
380
  if (!argv->len) return -EINVAL; /* argv[0] would read past an empty array */
@@ -395,7 +399,7 @@ static int32_t nish_spawn_start(const nish_array *argv, const nish_str *out, con
395
399
  if (failed) return -failed;
396
400
  /* A `dup2` action that could not be recorded would start the child on this
397
401
  process's own stream in its place, so it fails the spawn. */
398
- for (int k = 0; dup && k < 3 && !failed; k++) failed = posix_spawn_file_actions_adddup2(&fa, dup[k], k);
402
+ for (int k = 0; dup && k < n && !failed; k++) failed = posix_spawn_file_actions_adddup2(&fa, dup[k], base + k);
399
403
  if (failed) {
400
404
  posix_spawn_file_actions_destroy(&fa);
401
405
  return -failed;
@@ -425,7 +429,7 @@ static int32_t nish_exit_code(int status) {
425
429
  /* The two blocking builtins: start, then wait for that one child. A failed
426
430
  start is -1, the answer they have always given. */
427
431
  static int32_t nish_spawn_impl(const nish_array *argv, const nish_str *out, const nish_str *err) {
428
- int32_t pid = nish_spawn_start(argv, out, err, 0);
432
+ int32_t pid = nish_spawn_start(argv, out, err, 0, 0, 0);
429
433
  if (pid < 0) return -1;
430
434
  #ifdef __wasi__
431
435
  return -1;
@@ -456,7 +460,54 @@ int32_t nish_spawn_to(const nish_array *argv, const nish_str *out, const nish_st
456
460
  /* `spawn(argv)`: the child's pid, or a negative errno, without waiting. The
457
461
  child is this process's until a `waitPid` reaps it; one never reaped stays
458
462
  a zombie until the program exits, as it would in C. */
459
- int32_t nish_spawn_child(const nish_array *argv) { return nish_spawn_start(argv, 0, 0, 0); }
463
+ int32_t nish_spawn_child(const nish_array *argv) { return nish_spawn_start(argv, 0, 0, 0, 0, 0); }
464
+
465
+ /* The most descriptors `spawnWithFds` hands on in one call. */
466
+ #define NISH_SPAWN_FDS 64
467
+
468
+ /* `spawnWithFds(argv, fds)`: `spawn`, with the child holding `fds[i]` as
469
+ descriptor `3 + i`, close-on-exec cleared there, and its pid or a negative
470
+ errno back. More than NISH_SPAWN_FDS descriptors, or a negative one, is
471
+ -EINVAL before anything is opened or started. Each source is first
472
+ duplicated, close-on-exec, to a scratch descriptor at or above `3 + n`, and
473
+ the child's `dup2` actions read only the scratch ones: a source that is
474
+ already its own target (`fds[0] == 3`) is then a `dup2` between two
475
+ different descriptors, which clears close-on-exec on every libc where a
476
+ `dup2` onto itself leaves it set, and a source that is an earlier entry's
477
+ target (`fds = [4, 3]`) is not overwritten before it is read. A source that
478
+ is not open is the `F_DUPFD_CLOEXEC`'s -EBADF. The scratch descriptors are
479
+ closed on every path out, so this process holds what it held before; every
480
+ descriptor of its own stays as it was, so the child also inherits whatever
481
+ this process holds without close-on-exec, which nothing the runtime opens
482
+ is (docs/security/runtime.md, RT-17, and RT-40 for what this call hands
483
+ on). The child shares each descriptor's open file: its offset and its
484
+ status flags, `O_NONBLOCK` among them. */
485
+ int32_t nish_spawn_with_fds(const nish_array *argv, const nish_array *fds) {
486
+ #ifdef __wasi__
487
+ (void)argv;
488
+ (void)fds;
489
+ return -ENOSYS;
490
+ #else
491
+ const int32_t *src = (const int32_t *)fds->data;
492
+ int n = (int)fds->len;
493
+ if (fds->len > NISH_SPAWN_FDS) return -EINVAL;
494
+ for (int k = 0; k < n; k++)
495
+ if (src[k] < 0) return -EINVAL;
496
+ int scratch[NISH_SPAWN_FDS];
497
+ int32_t rc = 0;
498
+ int made = 0;
499
+ for (; made < n; made++) {
500
+ scratch[made] = fcntl(src[made], F_DUPFD_CLOEXEC, 3 + n);
501
+ if (scratch[made] < 0) {
502
+ rc = -errno;
503
+ break;
504
+ }
505
+ }
506
+ if (!rc) rc = nish_spawn_start(argv, 0, 0, scratch, 3, n);
507
+ while (made-- > 0) close(scratch[made]);
508
+ return rc;
509
+ #endif
510
+ }
460
511
 
461
512
  /* `spawnPiped(argv, stdin, stdout, stderr)`: run the child as `nish_spawn`
462
513
  does, with `stdin`'s bytes on its standard input and its standard output
@@ -525,7 +576,7 @@ int32_t nish_spawn_piped(const nish_array *argv, const nish_array *in, nish_arra
525
576
  int32_t rc = -1;
526
577
  if (nish_pipe(p) || nish_pipe(p + 2) || nish_pipe(p + 4)) goto done;
527
578
  int child[3] = { p[0], p[3], p[5] };
528
- int32_t pid = nish_spawn_start(argv, 0, 0, child);
579
+ int32_t pid = nish_spawn_start(argv, 0, 0, child, 0, 3);
529
580
  /* The child holds its ends now; this process keeps only its own. */
530
581
  for (int k = 0; k < 3; k++) {
531
582
  close(child[k]);