diff --git a/src/syscall/io.c b/src/syscall/io.c index ef2e34ef..819d8cab 100644 --- a/src/syscall/io.c +++ b/src/syscall/io.c @@ -46,6 +46,7 @@ #include "syscall/io.h" #include "syscall/net.h" #include "syscall/net-identity.h" +#include "syscall/net-sockopt.h" #include "syscall/poll.h" #include "syscall/proc.h" #include "syscall/signal.h" @@ -468,6 +469,7 @@ static int64_t rosetta_vz_ioctl(guest_t *g, uint64_t request, uint64_t arg) static const char fake_sock_path[] = ROSETTAD_SOCKET_PATH; memcpy(&caps[ROSETTA_CAPS_SOCKET_PATH], fake_sock_path, sizeof(fake_sock_path)); + /* Snapshot the caps binary path under the rosetta path lock so a * concurrent execve cannot tear the string between length probe and * copy. Inline buffer matches the cap exactly; the snapshot helper @@ -615,6 +617,7 @@ static uint32_t mac_oflag_to_linux(tcflag_t mf) #define LINUX_PARODD 0x0200 #define LINUX_HUPCL 0x0400 #define LINUX_CLOCAL 0x0800 + /* LINUX_CBAUD 0x0000100f and LINUX_CBAUDEX 0x00001000 encode baud in c_cflag; * macOS uses dedicated speed fields, so termios translation ignores CBAUD on * translation. TCGETS2/TCSETS2 use BOTHER to signal numeric c_ispeed/c_ospeed. @@ -650,6 +653,7 @@ static speed_t linux_cbaud_to_speed(uint32_t cbaud) static tcflag_t linux_cflag_to_mac(uint32_t lf) { tcflag_t mf = 0; + /* CSIZE: Linux CS5=0x00, CS6=0x10, CS7=0x20, CS8=0x30 * macOS CS5=0x00, CS6=0x100, CS7=0x200, CS8=0x300 */ @@ -1088,8 +1092,9 @@ int64_t sys_read(guest_t *g, int fd, uint64_t buf_gva, uint64_t count) } ssize_t ret = read(host_ref.fd, buf, count); + int64_t result = ret < 0 ? recv_eof_or_errno(host_ref.fd, fd) : ret; host_fd_ref_close(&host_ref); - return ret < 0 ? linux_errno() : ret; + return result; } int64_t sys_pread64(guest_t *g, @@ -1212,6 +1217,7 @@ static int64_t build_host_iov(guest_t *g, free(heap); return -LINUX_EFAULT; } + /* Cap to contiguous permitted bytes. When the guest iov entry spans a * non-contiguous boundary (different mapping or permission), zero every * subsequent host iov length so the host readv/writev returns a @@ -1386,6 +1392,7 @@ int64_t sys_readv(guest_t *g, int fd, uint64_t iov_gva, int iovcnt) return err; int64_t ret = urandom_fill_iov(fd, host_iov.iov, iovcnt); host_iov_free(&host_iov); + /* Mirror sys_read's slow-path refill so a readv consumer that drains * the shim ring leaves it ready for the next call, instead of forcing * every subsequent EL1 fast-path attempt back through HVC until some @@ -1398,6 +1405,7 @@ int64_t sys_readv(guest_t *g, int fd, uint64_t iov_gva, int iovcnt) type == FD_INOTIFY) { if (iovcnt <= 0) return -LINUX_EINVAL; + /* Use guest_read for the iov array since guest_ptr alone is unsafe if * the array spans a 2MiB block boundary. */ @@ -1444,7 +1452,7 @@ int64_t sys_readv(guest_t *g, int fd, uint64_t iov_gva, int iovcnt) } ssize_t ret = readv(host_ref.fd, host_iov.iov, iovcnt); - int64_t result = ret < 0 ? linux_errno() : ret; + int64_t result = ret < 0 ? recv_eof_or_errno(host_ref.fd, fd) : ret; host_iov_free(&host_iov); host_fd_ref_close(&host_ref); return result; @@ -2127,6 +2135,7 @@ int64_t sys_ioctl(guest_t *g, int fd, uint64_t request, uint64_t arg) t.c_cflag = linux_cflag_to_mac(lt2.c_cflag); t.c_lflag = linux_lflag_to_mac(lt2.c_lflag); termios_copy_cc_to_mac(t.c_cc, lt2.c_cc); + /* Resolve baud rate: BOTHER means use numeric c_ispeed/c_ospeed; * otherwise decode the standard CBAUD index to a numeric rate. */ @@ -2283,6 +2292,7 @@ int64_t sys_ioctl(guest_t *g, int fd, uint64_t request, uint64_t arg) close(host_slave_fd); return -LINUX_EMFILE; } + /* Track CLOEXEC + accmode in the guest table so exec honors them; the * host fd's own FD_CLOEXEC is per-descriptor and would be lost on the * dup that host_fd_ref hands multi-threaded callers. @@ -2359,6 +2369,7 @@ int64_t sys_fallocate(int fd, int mode, int64_t offset, int64_t len) host_fd_ref_close(&host_ref); return 0; } + /* EINVAL: misaligned, sub-block, or non-regular file. pwrite zeros only * through the current EOF so KEEP_SIZE remains guest-visible. Any other * host errno propagates verbatim. diff --git a/src/syscall/net-msg.c b/src/syscall/net-msg.c index 9478ae44..54ff01c7 100644 --- a/src/syscall/net-msg.c +++ b/src/syscall/net-msg.c @@ -462,8 +462,17 @@ int64_t sys_recvmsg(guest_t *g, int fd, uint64_t msg_gva, int flags) ssize_t ret = recvmsg(host_ref.fd, &msg, mac_flags); if (ret < 0) { - host_fd_ref_close(&host_ref); - return linux_errno(); + int64_t r = recv_eof_or_errno(host_ref.fd, fd); + if (r != 0) { + host_fd_ref_close(&host_ref); + return r; + } + + /* SEQPACKET-over-DGRAM EOF: fall through with an empty result so + * the guest sees msg_controllen 0 and msg_flags 0 like a real EOF. + */ + ret = 0; + msg.msg_flags = 0; } uint64_t zero64 = 0; int32_t mflags = mac_to_linux_msg_flags(msg.msg_flags); @@ -545,10 +554,33 @@ int64_t sys_recvmsg(guest_t *g, int fd, uint64_t msg_gva, int flags) ssize_t ret = recvmsg(host_ref.fd, &msg, mac_flags); if (ret < 0) { + int64_t r = recv_eof_or_errno(host_ref.fd, fd); free(mac_ctrl_heap); host_iov_free(&host_iov); + if (r == 0) { + /* SEQPACKET-over-DGRAM EOF: report an empty control buffer and + * flags, matching a real 0-length recvmsg. Do not fall through -- + * recvmsg left msg unmodified, so the control loop would walk the + * stale (uninitialized) mac_ctrl buffer. Surface EFAULT on an + * unwritable msghdr like the success path. + */ + uint64_t zero64 = 0; + uint32_t zero32 = 0; + int32_t zflags = 0; + if ((lmsg.msg_name && + guest_write_small( + g, msg_gva + offsetof(linux_msghdr_t, msg_namelen), + &zero32, sizeof(zero32)) < 0) || + guest_write_small( + g, msg_gva + offsetof(linux_msghdr_t, msg_controllen), + &zero64, sizeof(zero64)) < 0 || + guest_write_small(g, + msg_gva + offsetof(linux_msghdr_t, msg_flags), + &zflags, sizeof(zflags)) < 0) + r = -LINUX_EFAULT; + } host_fd_ref_close(&host_ref); - return linux_errno(); + return r; } if (lmsg.msg_name) { @@ -960,8 +992,17 @@ int64_t sys_recvmmsg(guest_t *g, } ssize_t ret = recvmsg(host_ref.fd, &host_msg, mac_flags); if (ret < 0) { - host_fd_ref_close(&host_ref); - return linux_errno(); + int64_t r = recv_eof_or_errno(host_ref.fd, fd); + if (r != 0) { + host_fd_ref_close(&host_ref); + return r; + } + + /* SEQPACKET-over-DGRAM EOF: record one empty message, matching + * how the general loop path counts a 0-length recvmsg. + */ + ret = 0; + host_msg.msg_flags = 0; } uint32_t msg_len = (uint32_t) ret; int32_t out_flags = @@ -1024,6 +1065,15 @@ int64_t sys_recvmmsg(guest_t *g, if (write_linux_mmsghdr_len(g, hdr_gva, msg_len) < 0) return received > 0 ? (int64_t) received : -LINUX_EFAULT; received++; + /* A zero-length message ends the batch. On a connection-oriented + * socket it is EOF; the macOS SEQPACKET-over-DGRAM substitute reports + * that EOF only once (then EAGAIN), so a further blocking iteration + * would hang waiting for a persistent EOF that never arrives. Stop + * here, matching the vlen==1 fast path -- recvmmsg may return fewer + * than vlen messages and callers loop for more. + */ + if (ret == 0) + break; } return (int64_t) received; } diff --git a/src/syscall/net-sockopt.c b/src/syscall/net-sockopt.c index 16ceffaf..cbd4ca2e 100644 --- a/src/syscall/net-sockopt.c +++ b/src/syscall/net-sockopt.c @@ -5,6 +5,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +#include #include #include #include @@ -159,6 +160,58 @@ int net_socket_cached_int_get(int guest_fd, int level, int optname, int *value) return net_sock_cache_get(guest_fd, idx, value); } +/* True when the host fd is an AF_UNIX datagram socket. Used together with the + * guest's cached SEQPACKET type to pin down the sys_socketpair substitute. + */ +static bool host_fd_is_unix_dgram(int host_fd) +{ + int st = 0; + socklen_t sl = sizeof(st); + if (getsockopt(host_fd, SOL_SOCKET, SO_TYPE, &st, &sl) < 0 || + st != SOCK_DGRAM) + return false; + struct sockaddr_storage ss; + socklen_t al = sizeof(ss); + return getsockname(host_fd, (struct sockaddr *) &ss, &al) == 0 && + ss.ss_family == AF_UNIX; +} + +/* Fold a failed host recv/read into Linux semantics: the SEQPACKET-over-DGRAM + * substitute reports ECONNRESET on peer close where Linux SEQPACKET returns a + * clean EOF, so report EOF (0) for exactly that case and the translated + * negative errno otherwise. + * + * The fold fires only when the guest asked for SOCK_SEQPACKET (cached SO_TYPE) + * and the host fd is an AF_UNIX datagram socket -- the two facts that together + * identify the socketpair substitute. Deliberately left alone: + * - genuine AF_UNIX SOCK_DGRAM sockets (cached type DGRAM): Linux never + * returns EOF there; after draining it blocks or reports EAGAIN, so a + * macOS peer-close ECONNRESET must stay an error, not a fake end-of-stream; + * - the AF_UNIX SOCK_STREAM substitute used for socket()/accept SEQPACKET + * (host type STREAM): a genuine peer abort there is a real reset; + * - INET UDP (host type DGRAM but not AF_UNIX): recv can report a real + * network error, which must stay an error. + * + * Call only when the host call returned < 0, with the pinned host fd still + * open. Probing host_fd is race-free against guest fd reuse; the cached-type + * read keys on guest_fd, but a stale hit is bounded by the AF_UNIX-datagram + * host check and can only mis-map a peer close between two AF_UNIX sockets -- + * the same racy-but-benign window the read fast path documents. Both probes run + * only on the rare ECONNRESET tail, off the hot path. errno is preserved. + */ +int64_t recv_eof_or_errno(int host_fd, int guest_fd) +{ + int e = errno; + int cached_type = 0; + bool fold = e == ECONNRESET && + net_socket_cached_int_get(guest_fd, LINUX_SOL_SOCKET, + LINUX_SO_TYPE, &cached_type) && + cached_type == LINUX_SOCK_SEQPACKET && + host_fd_is_unix_dgram(host_fd); + errno = e; /* undo the getsockopt/getsockname clobber on both paths */ + return fold ? 0 : linux_errno(); +} + int net_socket_cached_int_get_if_generation(int guest_fd, uint64_t generation, int level, diff --git a/src/syscall/net-sockopt.h b/src/syscall/net-sockopt.h index 8aa08c68..e92376a6 100644 --- a/src/syscall/net-sockopt.h +++ b/src/syscall/net-sockopt.h @@ -7,9 +7,20 @@ #pragma once +#include #include int net_socket_fd_is_valid(int guest_fd); + +/* Fold a failed host recv/read into Linux semantics: returns 0 (EOF) only for + * the SEQPACKET-over-DGRAM socketpair substitute's peer-close ECONNRESET (guest + * cached SO_TYPE SEQPACKET and host fd AF_UNIX SOCK_DGRAM), which Linux reports + * as a clean end-of-stream; otherwise the translated negative Linux errno. + * Genuine AF_UNIX datagram sockets are left untouched. Call only when the host + * call returned < 0, on the pinned host fd; errno is preserved across the + * internal socket probes. + */ +int64_t recv_eof_or_errno(int host_fd, int guest_fd); int net_socket_cached_int_get(int guest_fd, int level, int optname, int *value); int net_socket_cached_int_get_if_generation(int guest_fd, uint64_t generation, diff --git a/src/syscall/net.c b/src/syscall/net.c index f0488476..9154e1a1 100644 --- a/src/syscall/net.c +++ b/src/syscall/net.c @@ -72,8 +72,8 @@ int64_t net_wait_or_interrupted(int host_fd, short events, int msg_flags) * the macOS host call does. (read() is the exception: sock_read_iter returns 0 * for a zero count, so sys_read stays untouched.) Gate the host call on * readability: an interruptible wait for blocking callers, a zero-timeout - * readiness probe for nonblocking ones. EOF counts as readable in both, and - * the host call then returns 0 like Linux. + * readiness probe for nonblocking ones. EOF counts as readable in both, and the + * host call then returns 0 like Linux. * * Returns 0 to proceed or a negative Linux errno (EINTR/EAGAIN). */ @@ -831,6 +831,7 @@ int64_t sys_sendto(guest_t *g, len = avail; int mac_flags = translate_msg_flags(linux_flags); + /* MSG_NOSIGNAL (0x4000): suppress SIGPIPE on EPIPE. macOS has no * MSG_NOSIGNAL; elfuse handles it by not queuing SIGPIPE. */ @@ -911,9 +912,9 @@ int64_t sys_recvfrom(guest_t *g, int mac_flags = translate_msg_flags(flags); /* A single interruptible wait (not a MSG_DONTWAIT probe loop) preserves - * MSG_WAITALL semantics; the tiny ready-then-stolen window can still - * block, matching sys_read. A zero-length recv takes the readiness gate - * instead: unlike read(), Linux blocks it on an empty socket. + * MSG_WAITALL semantics; the tiny ready-then-stolen window can still block, + * matching sys_read. A zero-length recv takes the readiness gate instead: + * unlike read(), Linux blocks it on an empty socket. */ int64_t waited = len > 0 ? net_wait_or_interrupted(host_ref.fd, POLLIN, flags) @@ -934,8 +935,13 @@ int64_t sys_recvfrom(guest_t *g, ret = recv(host_ref.fd, buf, len, mac_flags); } if (ret < 0) { - host_fd_ref_close(&host_ref); - return linux_errno(); + int64_t result = recv_eof_or_errno(host_ref.fd, fd); + if (result < 0) { + host_fd_ref_close(&host_ref); + return result; + } + ret = 0; + mac_len = 0; } /* Write back source address if requested. */ @@ -954,8 +960,8 @@ int64_t sys_recvfrom(guest_t *g, int out_len = mac_to_linux_sockaddr((struct sockaddr *) &mac_sa, mac_len, linux_sa, (uint32_t) sizeof(linux_sa)); - if (out_len > 0) { - uint32_t actual_len = (uint32_t) out_len; + if (out_len > 0 || mac_len == 0) { + uint32_t actual_len = out_len > 0 ? (uint32_t) out_len : 0; uint32_t write_len = actual_len; if (write_len > guest_addrlen) write_len = guest_addrlen; @@ -963,6 +969,7 @@ int64_t sys_recvfrom(guest_t *g, host_fd_ref_close(&host_ref); return -LINUX_EFAULT; } + /* Write back actual length (Linux returns full size even if the * address was truncated to fit the buffer). */ diff --git a/src/syscall/syscall.c b/src/syscall/syscall.c index 8f8c341f..ca5e833f 100644 --- a/src/syscall/syscall.c +++ b/src/syscall/syscall.c @@ -56,6 +56,7 @@ #include "syscall/io.h" #include "syscall/mem.h" #include "syscall/net.h" +#include "syscall/net-sockopt.h" #include "syscall/poll.h" #include "syscall/path.h" #include "syscall/proc.h" @@ -92,6 +93,7 @@ void syscall_init(void) { fdtable_init(); signal_init(); + /* Mirror signal_init's attention_guest reset for the fd/urandom bitmap * singleton in shim-globals. Defends against a stale parent-process pointer * surviving across posix_spawn re-init. @@ -226,6 +228,7 @@ SC_FORWARD(sc_pwrite64, sys_pwrite64(g, (int) x0, x1, x2, (int64_t) x3)) SC_FORWARD(sc_ioctl, sys_ioctl(g, (int) x0, x1, x2)) SC_FORWARD(sc_preadv, sys_preadv(g, (int) x0, x1, (int) x2, (int64_t) x3)) SC_FORWARD(sc_pwritev, sys_pwritev(g, (int) x0, x1, (int) x2, (int64_t) x3)) + /* aarch64 LP64 raw ABI: x3=pos_l (full 64-bit offset), x4=pos_h (0), x5=flags */ SC_FORWARD(sc_preadv2, sys_preadv2(g, (int) x0, x1, (int) x2, (int64_t) x3, (int) x5)) @@ -265,6 +268,7 @@ SC_FORWARD(sc_fchmodat2, sys_fchmodat(g, (int) x0, x1, (uint32_t) x2, (int) x3 SC_FORWARD(sc_fchownat, sys_fchownat(g, (int) x0, x1, (uint32_t) x2, (uint32_t) x3, (int) x4)) SC_FORWARD(sc_fchown, sys_fchown((int) x0, (uint32_t) x1, (uint32_t) x2)) SC_FORWARD(sc_utimensat, sys_utimensat(g, (int) x0, x1, x2, (int) x3)) + /* Linux faccessat (SYS 48) is 3-arg: dirfd, path, mode. The flags parameter was * added in faccessat2 (SYS 439). x3 contains garbage from the caller's register * state. @@ -383,6 +387,7 @@ SC_FORWARD(sc_sched_rr_get_interval, sys_sched_rr_get_interval(g, (int) x0, x1) SC_FORWARD(sc_exit, SC_EXIT_SENTINEL | ((int) x0 & 0xFF)) SC_FORWARD(sc_getpid, proc_get_pid()) SC_FORWARD(sc_getppid, proc_get_ppid()) + /* getpgid(0) is served inline by the shim's pgid cache and never reaches here, * so a registry sync in this path would be dead for the common form; the group * signal path in sc_kill does the sync where it can actually run. getpgid may @@ -410,6 +415,7 @@ static int64_t sc_setsid(guest_t *g, (void) x5; (void) verbose; proc_registry_sync_self_pgid(g); + /* setsid moves the caller into a new group; refresh the registry so the * group signal path sees the new pgid without a per-query republish. */ @@ -609,10 +615,12 @@ static int64_t sc_setpgid(guest_t *g, (void) x4; (void) x5; (void) verbose; + /* setpgid takes pid_t (32-bit) args; cast as int like sc_kill so a negative * pid/pgid is seen as negative, not a large positive. */ int pid = (int) x0, pgid = (int) x1; + /* Kernel order: default pid/pgid before validation, so setpgid(-9, 0) turns * the pgid negative and fails EINVAL, not ESRCH. */ @@ -623,6 +631,7 @@ static int64_t sc_setpgid(guest_t *g, return -LINUX_EINVAL; if (rpid < 0) return -LINUX_ESRCH; + /* setpgid on a direct child records the group so kill(0) and kill(-pgid) * reach it. Kept in the syscall layer so proc-identity stays free of the * process-table dependency. Only POSIX-plausible targets are recorded: the @@ -635,6 +644,7 @@ static int64_t sc_setpgid(guest_t *g, */ if (rpid != self) { proc_signal_target_t peer; + /* The registry is the single source of truth for group membership: * every child publishes on fork, setpgid, and setsid. The old * process-table enumerator was redundant and could report a phantom @@ -796,6 +806,7 @@ static int64_t sc_sched_setaffinity(guest_t *g, return -LINUX_ESRCH; if (cpusetsize == 0) return -LINUX_EINVAL; + /* Linux accepts short masks as long as at least one supplied bit is set. * elfuse only supports CPU 0, so require bit 0 in the first byte. */ @@ -817,6 +828,7 @@ static void thread_force_exit_cb(thread_entry_t *t, void *ctx) (void) ctx; if (t == current_thread) return; + /* Skip a slot that is active but has not yet published its vCPU (a sibling * still in thread_create_and_run bring-up): it is not in hv_vcpu_run, and * handing hv_vcpus_exit a zero handle is invalid. Runs under thread_lock @@ -904,21 +916,23 @@ static int64_t sc_exit_group(guest_t *g, (void) x4; (void) x5; (void) verbose; + /* Request + interrupt only; do NOT join here. This handler runs on * whichever thread issued exit_group, so a join from here would snapshot - * the main thread (slot 0, never deactivates) and detach it after the - * poll cap, and would race the main thread's own join over the same - * siblings (double pthread_join is undefined). The kicked workers wind - * down on their own; the single authoritative join is in main() after - * vcpu_run_loop returns, before guest teardown. + * the main thread (slot 0, never deactivates) and detach it after the poll + * cap, and would race the main thread's own join over the same siblings + * (double pthread_join is undefined). The kicked workers wind down on their + * own; the single authoritative join is in main() after vcpu_run_loop + * returns, before guest teardown. */ proc_request_exit_group((int) x0); wakeup_pipe_signal(); thread_for_each(thread_force_exit_cb, NULL); - /* Workers parked on internal condvars (fork barrier, ptrace stop/wait) - * see neither the pipe nor the vCPU kick; broadcast so they re-check the - * exit-group flag and terminate before the authoritative join in main() - * (or guest_destroy, for the forked-child path) gives up on them. + + /* Workers parked on internal condvars (fork barrier, ptrace stop/wait) see + * neither the pipe nor the vCPU kick; broadcast so they re-check the + * exit-group flag and terminate before the authoritative join in main() (or + * guest_destroy, for the forked-child path) gives up on them. */ thread_wake_exit_waiters(); return SC_EXIT_SENTINEL | ((int) x0 & 0xFF); @@ -1037,6 +1051,7 @@ static int64_t sc_kill(guest_t *g, ? 0 : -LINUX_ESRCH; } + /* Process-group probe: the caller always exists in its own group; * pid<-1 needs the caller or a tracked child in group -pid. */ @@ -1081,6 +1096,7 @@ static int64_t sc_kill(guest_t *g, return 0; if (count == 0) return -LINUX_ESRCH; + /* Targets existed but every send failed: report the first transport * error rather than a misleading ESRCH. */ @@ -1173,17 +1189,20 @@ static int64_t sc_tgkill(guest_t *g, return -LINUX_EINVAL; if (tgid <= 0 || tid <= 0) return -LINUX_EINVAL; + /* All guest threads share the single guest tgid; a mismatch names a thread * that is not in this group (or a foreign process elfuse cannot reach), * which Linux reports as -ESRCH. */ if (tgid != (int) proc_get_pid()) return -LINUX_ESRCH; + /* sig == 0 is the existence/permission probe: report whether the thread is * live without queueing anything. */ if (sig == 0) return thread_tid_alive((int64_t) tid) ? 0 : -LINUX_ESRCH; + /* Thread-directed: only the target thread consumes it (Linux * task->pending). The enqueue resolves and validates the tid atomically. */ @@ -1206,6 +1225,7 @@ static int64_t rt_sigqueueinfo_impl(guest_t *g, return -LINUX_EINVAL; if (!thread_tid_alive((int64_t) tid)) return -LINUX_ESRCH; + /* sig == 0 is the existence/permission probe: the target is live, queue * nothing. */ @@ -1234,6 +1254,7 @@ static int64_t rt_sigqueueinfo_impl(guest_t *g, log_debug("rt_sigqueueinfo(tid=%d, sig=%d, si_code=%d)", tid, sig, info.si_code); } + /* Queued signals carry sigval in si_value for both standard and RT signals; * standard signals still coalesce to one pending instance. */ @@ -1249,11 +1270,13 @@ static int64_t rt_sigqueueinfo_impl(guest_t *g, (int64_t) tid, sig, info.si_code, info.si_pid, (uint32_t) info.si_uid, si_int, si_ptr) : signal_queue_thread((int64_t) tid, sig); + /* The target may have exited between the liveness check and the * enqueue; report -ESRCH as Linux would for a vanished thread. */ return queued ? 0 : -LINUX_ESRCH; } + /* Process-directed: queue into the shared set. Existence was checked * lock-free above; if the named tid was a worker that exits in the gap, the * signal still lands in the surviving thread group and this returns 0 where @@ -2434,6 +2457,7 @@ int syscall_dispatch(hv_vcpu_t vcpu, guest_t *g, int *exit_code, bool verbose) if (tp != FD_REGULAR && tp != FD_STDIO && tp != FD_PIPE && tp != FD_SOCKET) goto slow_path; + /* Same racy-but-benign read as tp above, and no worse than the * shipped tp-based divert: a concurrent close+reopen (only possible * with a live sibling thread; a single active thread has no @@ -2490,6 +2514,7 @@ int syscall_dispatch(hv_vcpu_t vcpu, guest_t *g, int *exit_code, bool verbose) if (can_block) { short ev = (nr == SYS_read) ? POLLIN : POLLOUT; struct pollfd pfd = {.fd = host_ref.fd, .events = ev}; + /* Divert on not-ready (0) or probe error (< 0, e.g. EINTR): a * blocking call here cannot be preempted, so let the * interruptible slow path handle both. @@ -2502,13 +2527,26 @@ int syscall_dispatch(hv_vcpu_t vcpu, guest_t *g, int *exit_code, bool verbose) ssize_t ret = (nr == SYS_read) ? read(host_ref.fd, buf, count) : write(host_ref.fd, buf, count); - host_fd_ref_close(&host_ref); if (ret >= 0) { + host_fd_ref_close(&host_ref); result = ret; goto fast_done; } - if (nr == SYS_write && errno == EPIPE) + + /* SEQPACKET-over-DGRAM peer close is ECONNRESET on the host + * datagram socket; Linux reports a clean EOF. recv_eof_or_errno + * probes the pinned host fd, so keep it open until after the check. + */ + if (nr == SYS_read) { + result = recv_eof_or_errno(host_ref.fd, fd); + host_fd_ref_close(&host_ref); + goto fast_done; + } + int fast_errno = errno; + host_fd_ref_close(&host_ref); + if (fast_errno == EPIPE) signal_queue(LINUX_SIGPIPE); + errno = fast_errno; result = linux_errno(); goto fast_done; } diff --git a/tests/test-socket.c b/tests/test-socket.c index e3e24194..411962de 100644 --- a/tests/test-socket.c +++ b/tests/test-socket.c @@ -26,6 +26,9 @@ * nonblocking and empty, blocks when empty, 0 without consuming when * data is pending) * 15. invalid recvmsg iov returns EFAULT immediately + * 16. SEQPACKET peer close reads as EOF (0), not ECONNRESET (Rust spawn) + * 17. genuine AF_UNIX datagram peer close is not folded to EOF + * 18. recvmmsg(vlen>1) on a peer-closed SEQPACKET substitute does not hang */ #include @@ -245,6 +248,7 @@ int main(void) } else { n = recvfrom(dsv[1], recv_buf, sizeof(recv_buf), 0, (struct sockaddr *) &sa, &salen); + /* Linux returns salen=0 for unnamed AF_UNIX socketpair * endpoints (no source address). Accept both salen==0 and salen * with AF_UNIX family filled in. @@ -398,11 +402,11 @@ int main(void) } /* Test 14: zero-length recvmsg follows receive-readiness semantics. Linux - * clamps the receive low-water target to one byte (sock_rcvlowat returns - * v ?: 1), so a zero-length recvmsg behaves like a one-byte receive for + * clamps the receive low-water target to one byte (sock_rcvlowat returns v + * ?: 1), so a zero-length recvmsg behaves like a one-byte receive for * readiness: EAGAIN on an empty nonblocking socket, blocks on an empty - * blocking socket (observed here as EINTR via alarm), and returns 0 - * without consuming anything once data is pending. + * blocking socket (observed here as EINTR via alarm), and returns 0 without + * consuming anything once data is pending. */ printf("test-socket: 14. zero-length recvmsg readiness semantics... "); { @@ -429,8 +433,8 @@ int main(void) /* Empty socket, blocking: parks until the timer interrupts. A * repeating interval (not a one-shot alarm) closes the race where - * the first SIGALRM lands before recvmsg enters the kernel and - * the call then blocks with no interrupt left. + * the first SIGALRM lands before recvmsg enters the kernel and the + * call then blocks with no interrupt left. */ struct sigaction zsa, old_zsa; memset(&zsa, 0, sizeof(zsa)); @@ -580,6 +584,164 @@ int main(void) close(seq_fd); } + /* Test 16: SEQPACKET peer close reads as EOF, not ECONNRESET. Rust's + * process spawn uses an AF_UNIX SOCK_SEQPACKET socketpair as its CLOEXEC + * status channel and treats a recv error there as unreachable (it aborts). + * macOS has no AF_UNIX SEQPACKET, so elfuse substitutes SOCK_DGRAM, whose + * recv reports ECONNRESET on peer close where Linux SEQPACKET returns EOF. + * Verify the clean-EOF contract holds. + */ + printf("test-socket: 16. SEQPACKET peer close -> EOF... "); + { + /* Every receive syscall must report the peer close as EOF (0), not + * ECONNRESET, on the SEQPACKET-over-DGRAM substitute. recv covers + * Rust's spawn channel; read, readv, recvmsg, and recvmmsg are the + * sibling paths that forward the same host error. + */ + int ok = 1; + for (int which = 0; which < 6 && ok; which++) { + int eof_sv[2]; + if (socketpair(AF_UNIX, SOCK_SEQPACKET, 0, eof_sv) < 0) { + printf("FAIL (socketpair: %m)\n"); + ok = 0; + break; + } + close(eof_sv[0]); /* drop the peer, as an exec'd child would */ + char eof_buf[8]; + char eof_buf2[8]; + ssize_t eof_n; + const char *op; + int meta_ok = 1; + if (which == 0) { + op = "recv"; + eof_n = recv(eof_sv[1], eof_buf, sizeof(eof_buf), 0); + } else if (which == 1) { + op = "read"; + eof_n = read(eof_sv[1], eof_buf, sizeof(eof_buf)); + } else if (which == 2) { + /* Two iovecs forces sys_readv's multi-iovec host readv(). */ + op = "readv"; + struct iovec iov[2] = { + {.iov_base = eof_buf, .iov_len = sizeof(eof_buf)}, + {.iov_base = eof_buf2, .iov_len = sizeof(eof_buf2)}}; + eof_n = readv(eof_sv[1], iov, 2); + } else if (which == 3) { + op = "recvfrom"; + struct sockaddr_storage ss; + socklen_t slen = sizeof(ss); + eof_n = recvfrom(eof_sv[1], eof_buf, sizeof(eof_buf), 0, + (struct sockaddr *) &ss, &slen); + meta_ok = slen == 0; + } else if (which == 4) { + op = "recvmsg"; + struct iovec iov = {.iov_base = eof_buf, + .iov_len = sizeof(eof_buf)}; + struct sockaddr_storage ss; + struct msghdr mh = {.msg_name = &ss, + .msg_namelen = sizeof(ss), + .msg_iov = &iov, + .msg_iovlen = 1}; + eof_n = recvmsg(eof_sv[1], &mh, 0); + meta_ok = mh.msg_namelen == 0; + } else { + /* recvmmsg with vlen==1 records one empty message on EOF. */ + op = "recvmmsg"; + struct iovec iov = {.iov_base = eof_buf, + .iov_len = sizeof(eof_buf)}; + struct mmsghdr mm = {0}; + mm.msg_hdr.msg_iov = &iov; + mm.msg_hdr.msg_iovlen = 1; + int r = recvmmsg(eof_sv[1], &mm, 1, 0, NULL); + /* Expect 1 message received, of length 0. */ + eof_n = (r == 1 && mm.msg_len == 0) ? 0 : (r < 0 ? -1 : 1); + } + if (eof_n != 0 || !meta_ok) { + printf("FAIL (%s=%zd errno=%d %s)\n", op, eof_n, + eof_n < 0 ? errno : 0, + eof_n < 0 ? strerror(errno) + : (meta_ok ? "data" : "stale address")); + ok = 0; + } + close(eof_sv[1]); + } + if (ok) + printf("PASS\n"); + else + failures++; + } + + /* Test 17: a genuine AF_UNIX datagram socket must NOT fold a peer-close + * ECONNRESET into EOF -- only the SEQPACKET-over-DGRAM substitute does. + * Linux never reports EOF on a datagram receive (it blocks, or returns + * EAGAIN when nonblocking), so a clean 0 here would misreport a live peer + * as end-of-stream. Use a nonblocking receiver so the check cannot hang on + * a real Linux host where the datagram recv would otherwise block. + */ + printf("test-socket: 17. datagram peer close is not EOF... "); + { + int dsv[2]; + if (socketpair(AF_UNIX, SOCK_DGRAM, 0, dsv) < 0) { + printf("FAIL (socketpair: %m)\n"); + failures++; + } else { + fcntl(dsv[1], F_SETFL, O_NONBLOCK); + close(dsv[0]); + char b[8]; + ssize_t dn = recv(dsv[1], b, sizeof(b), 0); + /* Any error is acceptable (ECONNRESET on macOS/elfuse, EAGAIN on + * Linux); a clean EOF(0) is the regression this guards. + */ + if (dn != 0) { + printf("PASS (recv=%zd errno=%d)\n", dn, dn < 0 ? errno : 0); + } else { + printf("FAIL (datagram peer close reported EOF)\n"); + failures++; + } + close(dsv[1]); + } + } + + /* Test 18: a blocking recvmmsg(vlen>1) on a peer-closed SEQPACKET + * substitute must terminate the batch at the folded EOF, not hang. The + * macOS datagram substitute reports the peer close only once (then EAGAIN), + * so a naive loop would block forever on the second iteration's readiness + * gate. Expect at least one zero-length message and a prompt return; the + * test-runner timeout catches a regression to the hang. + */ + printf("test-socket: 18. recvmmsg(vlen>1) EOF does not hang... "); + { + int msv[2]; + if (socketpair(AF_UNIX, SOCK_SEQPACKET, 0, msv) < 0) { + printf("FAIL (socketpair: %m)\n"); + failures++; + } else { + close(msv[0]); + char mb[4][8]; + struct mmsghdr mm[4]; + struct iovec miov[4]; + memset(mm, 0, sizeof(mm)); + for (int i = 0; i < 4; i++) { + miov[i].iov_base = mb[i]; + miov[i].iov_len = sizeof(mb[i]); + mm[i].msg_hdr.msg_iov = &miov[i]; + mm[i].msg_hdr.msg_iovlen = 1; + } + /* Blocking (no MSG_DONTWAIT): this is the path that would hang. */ + int mr = recvmmsg(msv[1], mm, 4, 0, NULL); + /* elfuse stops at the first EOF (mr==1); Linux keeps reading the + * persistent EOF and returns 4. Accept any mr>=1 with the first + * message empty; the point is that it returned at all. + */ + if (mr >= 1 && mm[0].msg_len == 0) { + printf("PASS (mr=%d)\n", mr); + } else { + printf("FAIL (mr=%d len0=%u)\n", mr, mm[0].msg_len); + failures++; + } + close(msv[1]); + } + } + if (failures == 0) { printf("test-socket: all tests passed -- PASS\n"); return 0;