From eb20204e732c90ad616e169dafda65d804fc9b6d Mon Sep 17 00:00:00 2001 From: xalestar Date: Sat, 24 Oct 2026 18:37:48 +0800 Subject: [PATCH 1/8] Check copy_file_range's ends before copying Linux runs generic_file_rw_checks before copy_file_range looks at offsets or lengths: EISDIR when either end is a directory, EINVAL when either is not a regular file, and EBADF when the input is not open for reading or the output is not open for writing or has O_APPEND. elfuse went straight to its pread/pwrite loop, so it appended through an O_APPEND output and copied out of a pipe, where Linux refuses both. Make the same checks on the host descriptors first. A size limit applied after the copy starts depends on this order: Linux refuses a bad end before it can raise SIGXFSZ for it. --- src/syscall/io.c | 28 +++++++++++++++++++++ src/syscall/linux-wire.h | 1 + tests/test-io-opt.c | 54 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+) diff --git a/src/syscall/io.c b/src/syscall/io.c index 02d757cdd..11a2ecd0b 100644 --- a/src/syscall/io.c +++ b/src/syscall/io.c @@ -3848,6 +3848,30 @@ int64_t sys_sendfile(guest_t *g, return (int64_t) total; } +/* generic_file_rw_checks (Linux fs/read_write.c), which copy_file_range runs + * before it looks at offsets, lengths or limits: both ends regular files, the + * input open for reading, and the output open for writing without O_APPEND. + */ +static int64_t copy_file_rw_checks(int in_hfd, int out_hfd) +{ + struct stat in_st, out_st; + if (fstat(in_hfd, &in_st) < 0 || fstat(out_hfd, &out_st) < 0) + return linux_errno(); + if (S_ISDIR(in_st.st_mode) || S_ISDIR(out_st.st_mode)) + return -LINUX_EISDIR; + if (!S_ISREG(in_st.st_mode) || !S_ISREG(out_st.st_mode)) + return -LINUX_EINVAL; + + int in_fl = fcntl(in_hfd, F_GETFL); + int out_fl = fcntl(out_hfd, F_GETFL); + if (in_fl < 0 || out_fl < 0) + return linux_errno(); + if ((in_fl & O_ACCMODE) == O_WRONLY || (out_fl & O_ACCMODE) == O_RDONLY || + (out_fl & O_APPEND)) + return -LINUX_EBADF; + return 0; +} + int64_t sys_copy_file_range(guest_t *g, int fd_in, uint64_t off_in_gva, @@ -3886,6 +3910,10 @@ int64_t sys_copy_file_range(guest_t *g, } } + err = copy_file_rw_checks(in_ref.fd, out_ref.fd); + if (err < 0) + goto out_copy_file_range; + /* Emulate with a pread/pwrite loop. */ int64_t moved = copy_fd_range(&(copy_ends_t) {.in_gfd = fd_in, .in_hfd = in_ref.fd, diff --git a/src/syscall/linux-wire.h b/src/syscall/linux-wire.h index 029772d1a..f445a7fc4 100644 --- a/src/syscall/linux-wire.h +++ b/src/syscall/linux-wire.h @@ -58,6 +58,7 @@ typedef struct { #define LINUX_EXDEV 18 #define LINUX_ENODEV 19 #define LINUX_ENOTDIR 20 +#define LINUX_EISDIR 21 #define LINUX_EINVAL 22 #define LINUX_EMFILE 24 #define LINUX_ENOTTY 25 diff --git a/tests/test-io-opt.c b/tests/test-io-opt.c index d00b96ae0..1e19261b8 100644 --- a/tests/test-io-opt.c +++ b/tests/test-io-opt.c @@ -8,6 +8,7 @@ * Tests: sendfile, copy_file_range, fsync, fallocate */ +#include #include #include #include @@ -185,6 +186,59 @@ int main(void) unlink(cfr_dst); } + /* copy_file_range refuses a directory, anything else not a regular file, an + * input not open for reading, and an output not open for writing or open + * O_APPEND, before it copies anything. + */ + TEST("copy_file_range refuses bad ends"); + { + int rd = open(src_path, O_RDONLY); + int wr = open(src_path, O_WRONLY); + int ap = open(src_path, O_WRONLY | O_APPEND); + int out = open(cfr_dst, O_CREAT | O_WRONLY | O_TRUNC, 0644); + int dir = open(tmpdir, O_RDONLY); + int pfd[2] = {-1, -1}; + int ok = rd >= 0 && wr >= 0 && ap >= 0 && out >= 0 && dir >= 0 && + pipe(pfd) == 0 && write(pfd[1], "x", 1) == 1; + const char *why = "setup failed"; + + if (ok) { + why = "write-only input not EBADF"; + ok = copy_file_range(wr, NULL, out, NULL, 4, 0) == -1 && + errno == EBADF; + } + if (ok) { + why = "read-only output not EBADF"; + ok = copy_file_range(rd, NULL, rd, NULL, 4, 0) == -1 && + errno == EBADF; + } + if (ok) { + why = "O_APPEND output not EBADF"; + ok = copy_file_range(rd, NULL, ap, NULL, 4, 0) == -1 && + errno == EBADF; + } + if (ok) { + why = "pipe input not EINVAL"; + ok = copy_file_range(pfd[0], NULL, out, NULL, 4, 0) == -1 && + errno == EINVAL; + } + if (ok) { + why = "directory input not EISDIR"; + ok = copy_file_range(dir, NULL, out, NULL, 4, 0) == -1 && + errno == EISDIR; + } + if (ok) + PASS(); + else + FAIL(why); + + int fds[] = {rd, wr, ap, out, dir, pfd[0], pfd[1]}; + for (size_t i = 0; i < sizeof(fds) / sizeof(fds[0]); i++) + if (fds[i] >= 0) + close(fds[i]); + unlink(cfr_dst); + } + /* Cleanup. The other three fixtures are unlinked by the blocks that make * them, so only these two are left for rmdir to trip over. */ From 7d0851fc2f80910932680d1369c788ce0a3374a6 Mon Sep 17 00:00:00 2001 From: xalestar Date: Sat, 24 Oct 2026 23:52:37 +0800 Subject: [PATCH 2/8] Refuse a copy_file_range range that wraps generic_copy_file_checks refuses a range whose end wraps with EOVERFLOW, testing pos + count < pos as an unsigned sum, before it shortens the copy or applies any limit. A negative offset that does not wrap goes on to rw_verify_area, which refuses it with EINVAL. elfuse had neither check, so copy_file_range with an offset of 1 and a length of UINT64_MAX copied to end of file, and a negative offset passed by pointer was taken as "use the descriptor position". Check both ends in the kernel's order, using the offset the guest passed or else the descriptor's position. Refs #467 --- src/syscall/io.c | 29 +++++++++++++++++++++++ tests/test-io-opt.c | 57 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/src/syscall/io.c b/src/syscall/io.c index 11a2ecd0b..443353f62 100644 --- a/src/syscall/io.c +++ b/src/syscall/io.c @@ -3872,6 +3872,22 @@ static int64_t copy_file_rw_checks(int in_hfd, int out_hfd) return 0; } +/* generic_copy_file_checks (Linux fs/read_write.c) refuses a range whose end + * wraps, testing pos + count < pos as an unsigned sum, so a negative offset + * passed by pointer wraps too. The position is that offset when the guest + * passed one, else the descriptor's own. + */ +static bool copy_range_wraps(int host_fd, + bool has_off, + int64_t off, + uint64_t len) +{ + int64_t pos = has_off ? off : lseek(host_fd, 0, SEEK_CUR); + if (!has_off && pos < 0) + return false; + return (uint64_t) pos + len < (uint64_t) pos; +} + int64_t sys_copy_file_range(guest_t *g, int fd_in, uint64_t off_in_gva, @@ -3913,6 +3929,19 @@ int64_t sys_copy_file_range(guest_t *g, err = copy_file_rw_checks(in_ref.fd, out_ref.fd); if (err < 0) goto out_copy_file_range; + if (copy_range_wraps(in_ref.fd, off_in_gva != 0, off_in, len) || + copy_range_wraps(out_ref.fd, off_out_gva != 0, off_out, len)) { + err = -LINUX_EOVERFLOW; + goto out_copy_file_range; + } + + /* A negative offset that does not wrap reaches rw_verify_area, which + * refuses it. Below, a negative offset means "none given". + */ + if ((off_in_gva != 0 && off_in < 0) || (off_out_gva != 0 && off_out < 0)) { + err = -LINUX_EINVAL; + goto out_copy_file_range; + } /* Emulate with a pread/pwrite loop. */ int64_t moved = copy_fd_range(&(copy_ends_t) {.in_gfd = fd_in, diff --git a/tests/test-io-opt.c b/tests/test-io-opt.c index 1e19261b8..f9f2beb75 100644 --- a/tests/test-io-opt.c +++ b/tests/test-io-opt.c @@ -9,6 +9,7 @@ */ #include +#include #include #include #include @@ -239,6 +240,62 @@ int main(void) unlink(cfr_dst); } + /* copy_file_range refuses a range whose end wraps as an unsigned sum, at + * either end, before it copies anything. A negative offset passed by + * pointer wraps too unless the length is short, and is then EINVAL. + */ + TEST("copy_file_range refuses a wrapping range"); + { + int rd = open(src_path, O_RDONLY); + int out = open(cfr_dst, O_CREAT | O_WRONLY | O_TRUNC, 0644); + int ok = rd >= 0 && out >= 0; + const char *why = "setup failed"; + loff_t one = 1, neg = -1, zero = 0; + struct stat st; + + if (ok) { + why = "off_in 1, len UINT64_MAX not EOVERFLOW"; + ok = copy_file_range(rd, &one, out, NULL, SIZE_MAX, 0) == -1 && + errno == EOVERFLOW; + } + if (ok) { + why = "off_out -1 not EOVERFLOW"; + ok = copy_file_range(rd, &zero, out, &neg, 4, 0) == -1 && + errno == EOVERFLOW; + } + if (ok) { + why = "off_in -1 not EOVERFLOW"; + ok = copy_file_range(rd, &neg, out, NULL, 4, 0) == -1 && + errno == EOVERFLOW; + } + if (ok) { + loff_t far = -4096; + why = "off_out -4096, len 10 not EINVAL"; + ok = copy_file_range(rd, &zero, out, &far, 10, 0) == -1 && + errno == EINVAL; + } + if (ok) { + loff_t far = -4096; + why = "off_in -4096, len 10 not EINVAL"; + ok = copy_file_range(rd, &far, out, NULL, 10, 0) == -1 && + errno == EINVAL; + } + if (ok) { + why = "output grew"; + ok = fstat(out, &st) == 0 && st.st_size == 0; + } + if (ok) + PASS(); + else + FAIL(why); + + if (rd >= 0) + close(rd); + if (out >= 0) + close(out); + unlink(cfr_dst); + } + /* Cleanup. The other three fixtures are unlinked by the blocks that make * them, so only these two are left for rmdir to trip over. */ From 34b6e68358e3b186918a651206e801452c382cfa Mon Sep 17 00:00:00 2001 From: xalestar Date: Sat, 24 Oct 2026 23:11:35 +0800 Subject: [PATCH 3/8] Refuse an O_APPEND output in sendfile and splice Linux refuses an output opened with O_APPEND before anything moves: do_splice_direct for sendfile, and the pipe-to-file branch of do_splice, both return EINVAL. elfuse appended the data instead. A pipe output is still accepted, as on Linux. The EBADF for an input not open for reading or an output not open for writing comes first, as in do_sendfile and do_splice, and a zero-length splice still returns 0 before either check. sendfile keeps refusing a negative input offset with EINVAL before it looks at the output, as do_sendfile does. --- src/syscall/io.c | 58 +++++++++++++++++++++++++++++++++++++++++---- tests/test-io-opt.c | 58 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 111 insertions(+), 5 deletions(-) diff --git a/src/syscall/io.c b/src/syscall/io.c index 443353f62..fe245d8f2 100644 --- a/src/syscall/io.c +++ b/src/syscall/io.c @@ -3784,6 +3784,35 @@ static int64_t copy_fd_range(const copy_ends_t *ends, return ret; } +/* do_sendfile and do_splice (Linux fs/read_write.c, fs/splice.c) refuse an + * input not open for reading with EBADF. do_sendfile then checks the input + * offset before it looks at the output, which is why the two ends are checked + * apart. + */ +static int64_t splice_in_check(int in_hfd) +{ + int fl = fcntl(in_hfd, F_GETFL); + return fl >= 0 && (fl & O_ACCMODE) == O_WRONLY ? -LINUX_EBADF : 0; +} + +/* An output not open for writing is EBADF. After that, do_splice_direct and the + * pipe-to-file branch of do_splice refuse an output opened O_APPEND with + * EINVAL, before anything moves. A pipe output is not checked for O_APPEND. + */ +static int64_t splice_out_check(int out_hfd) +{ + int fl = fcntl(out_hfd, F_GETFL); + if (fl < 0) + return 0; + if ((fl & O_ACCMODE) == O_RDONLY) + return -LINUX_EBADF; + + struct stat st; + if ((fl & O_APPEND) && fstat(out_hfd, &st) == 0 && !S_ISFIFO(st.st_mode)) + return -LINUX_EINVAL; + return 0; +} + int64_t sys_sendfile(guest_t *g, int out_fd, int in_fd, @@ -3810,11 +3839,17 @@ int64_t sys_sendfile(guest_t *g, err = -LINUX_EFAULT; goto out_sendfile; } - if (offset < 0) { - err = -LINUX_EINVAL; - goto out_sendfile; - } } + err = splice_in_check(in_ref.fd); + if (err < 0) + goto out_sendfile; + if (offset_gva != 0 && offset < 0) { + err = -LINUX_EINVAL; + goto out_sendfile; + } + err = splice_out_check(out_ref.fd); + if (err < 0) + goto out_sendfile; /* sendfile has no output offset, so out always uses write(). */ int64_t off_out = -1; @@ -4086,6 +4121,12 @@ int64_t sys_splice(guest_t *g, size_t len, unsigned int flags) { + /* The splice syscall returns 0 for a zero length before it looks at either + * descriptor. + */ + if (len == 0) + return 0; + (void) flags; host_fd_ref_t in_ref, out_ref; fd_block_state_t in_st, out_st; @@ -4115,6 +4156,15 @@ int64_t sys_splice(guest_t *g, } } + err = splice_in_check(in_ref.fd); + if (err == 0) + err = splice_out_check(out_ref.fd); + if (err < 0) { + host_fd_ref_close(&out_ref); + host_fd_ref_close(&in_ref); + return err; + } + /* Emulate with a read/write loop over a heap buffer. splice fully drains * each read chunk rather than stopping on a short write, which is why it * does not share copy_fd_range; splice_drain_chunk is that drain. diff --git a/tests/test-io-opt.c b/tests/test-io-opt.c index f9f2beb75..c37fba88a 100644 --- a/tests/test-io-opt.c +++ b/tests/test-io-opt.c @@ -5,7 +5,7 @@ * Copyright 2025 Moritz Angermann, zw3rk pte. ltd. * SPDX-License-Identifier: Apache-2.0 * - * Tests: sendfile, copy_file_range, fsync, fallocate + * Tests: sendfile, copy_file_range, splice, fsync, fallocate */ #include @@ -296,6 +296,62 @@ int main(void) unlink(cfr_dst); } + /* sendfile and a pipe-to-file splice refuse an O_APPEND output with EINVAL + * and move nothing, after the EBADF for an end opened the wrong way. A + * zero-length splice returns 0 before either check. sendfile checks a + * negative input offset before it looks at the output. + */ + TEST("sendfile and splice refuse an O_APPEND output"); + { + int rd = open(src_path, O_RDONLY); + int ap = open(cfr_dst, O_CREAT | O_WRONLY | O_APPEND | O_TRUNC, 0644); + int pfd[2] = {-1, -1}; + int ok = + rd >= 0 && ap >= 0 && pipe(pfd) == 0 && write(pfd[1], "xy", 2) == 2; + const char *why = "setup failed"; + struct stat st; + + if (ok) { + why = "sendfile not EINVAL"; + ok = sendfile(ap, rd, NULL, 2) == -1 && errno == EINVAL; + } + if (ok) { + why = "splice not EINVAL"; + ok = splice(pfd[0], NULL, ap, NULL, 2, 0) == -1 && errno == EINVAL; + } + if (ok) { + why = "zero-length splice not 0"; + ok = splice(pfd[0], NULL, ap, NULL, 0, 0) == 0; + } + if (ok) { + int wr = open(src_path, O_WRONLY); + why = "write-only input not EBADF ahead of O_APPEND"; + ok = wr >= 0 && sendfile(ap, wr, NULL, 2) == -1 && errno == EBADF && + splice(pfd[1], NULL, ap, NULL, 2, 0) == -1 && errno == EBADF; + if (wr >= 0) + close(wr); + } + if (ok) { + off_t neg = -1; + why = "negative offset not EINVAL ahead of a read-only output"; + ok = sendfile(rd, rd, &neg, 2) == -1 && errno == EINVAL; + } + if (ok) { + why = "output grew"; + ok = fstat(ap, &st) == 0 && st.st_size == 0; + } + if (ok) + PASS(); + else + FAIL(why); + + int fds[] = {rd, ap, pfd[0], pfd[1]}; + for (size_t i = 0; i < sizeof(fds) / sizeof(fds[0]); i++) + if (fds[i] >= 0) + close(fds[i]); + unlink(cfr_dst); + } + /* Cleanup. The other three fixtures are unlinked by the blocks that make * them, so only these two are left for rmdir to trip over. */ From f1a1150551b0f8aeee42ad6aa8b1513d39b5db0a Mon Sep 17 00:00:00 2001 From: xalestar Date: Sat, 24 Oct 2026 22:40:40 +0800 Subject: [PATCH 4/8] Honor SPLICE_F_NONBLOCK in splice sys_splice discarded its flags, so SPLICE_F_NONBLOCK had no effect: splice from an empty pipe whose writer is still open waited for data, where Linux returns EAGAIN (splice_from_pipe_next). The flag now reaches the input through a per-call field in the block state, which the transfer paths check before the ownership branch, so it also holds on a descriptor elfuse did not open, such as inherited stdin. It reaches a pipe and an AF_UNIX stream socket only, as on Linux (unix_stream_splice_read); a TCP input follows its own O_NONBLOCK (tcp_splice_read). The output is not made nonblocking: splice reads a chunk before it writes it, and a pipe or socket input cannot take back what a nonblocking write then failed to place. A pipe output is asked for room before each read instead, and the chunk is capped at the room it has, so a full output answers EAGAIN with the input untouched. As in do_splice, an O_NONBLOCK pipe end raises the flag too: the input for pipe to file, the output for file to pipe, either for pipe to pipe. A pipe output whose reader is gone fails with EPIPE and SIGPIPE before the input is read, as wait_for_space does; macOS polls such a pipe as POLLHUP without POLLOUT. --- src/syscall/internal.h | 4 + src/syscall/io.c | 108 +++++++++++++++- tests/test-io-opt.c | 274 ++++++++++++++++++++++++++++++++++++++++- tests/test-matrix.sh | 25 ++++ 4 files changed, 409 insertions(+), 2 deletions(-) diff --git a/src/syscall/internal.h b/src/syscall/internal.h index c575898be..66a0c4eb5 100644 --- a/src/syscall/internal.h +++ b/src/syscall/internal.h @@ -550,6 +550,9 @@ int fd_get_type(int guest_fd); * guest asked for, which on an owned fd is the only place it is recorded. seals * rides along so a write path can reject a sealed memfd from the state it * pinned, rather than from a second lookup that may describe another file. + * call_nonblock is never read from the table: a caller sets it on its own copy + * when the syscall itself asks for a nonblocking transfer (SPLICE_F_NONBLOCK), + * whatever mode the description carries. */ typedef struct { int type; @@ -558,6 +561,7 @@ typedef struct { bool can_block; bool nonblock_owned; bool guest_nonblock; + bool call_nonblock; } fd_block_state_t; fd_block_state_t fd_block_state(int guest_fd); diff --git a/src/syscall/io.c b/src/syscall/io.c index fe245d8f2..af76f87f2 100644 --- a/src/syscall/io.c +++ b/src/syscall/io.c @@ -407,6 +407,8 @@ static bool io_foreign_should_block(int host_fd, short events) */ static bool io_eagain_should_wait(const fd_block_state_t *st, int host_fd) { + if (st->call_nonblock) + return false; if (st->nonblock_owned) return !st->guest_nonblock; return sock_op_should_block(st, host_fd, 0); @@ -508,6 +510,20 @@ int64_t io_xfer(int fd, bool nb_transfer = st.nonblock_owned; bool wait_first = false; if (!nb_transfer) { + /* A per-call nonblocking request on a description elfuse does not own + * cannot reach the host transfer, which would block, so readiness is + * asked first instead. + */ + if (st.call_nonblock) { + struct pollfd pfd = {.fd = host_fd, .events = events}; + if (poll(&pfd, 1, 0) <= 0) { + errno = EAGAIN; + *out = -1; + return 0; + } + *out = io_xfer_once(host_fd, is_socket, is_read, iov, iovcnt); + return 0; + } if (!io_foreign_should_block(host_fd, events)) { *out = io_xfer_once(host_fd, is_socket, is_read, iov, iovcnt); return 0; @@ -4113,6 +4129,44 @@ static size_t splice_drain_chunk(splice_state_t *st, return written; } +#define LINUX_SPLICE_F_NONBLOCK 0x02 + +/* Whether SPLICE_F_NONBLOCK reaches this input on Linux: a pipe takes it in + * splice_from_pipe_next and an AF_UNIX stream socket in + * unix_stream_splice_read. tcp_splice_read and the other inputs follow the + * descriptor's own O_NONBLOCK. + */ +static bool splice_flag_reaches_input(int host_fd) +{ + struct stat sb; + if (fstat(host_fd, &sb) < 0) + return false; + if (S_ISFIFO(sb.st_mode)) + return true; + if (!S_ISSOCK(sb.st_mode)) + return false; + + struct sockaddr_storage ss; + socklen_t sl = sizeof(ss); + int type = 0; + socklen_t tl = sizeof(type); + return getsockname(host_fd, (struct sockaddr *) &ss, &sl) == 0 && + ss.ss_family == AF_UNIX && + getsockopt(host_fd, SOL_SOCKET, SO_TYPE, &type, &tl) == 0 && + type == SOCK_STREAM; +} + +/* The O_NONBLOCK the guest sees on a descriptor: the shadow on an owned fd, the + * host flag otherwise. + */ +static bool splice_end_nonblock(const fd_block_state_t *st, int host_fd) +{ + if (st->nonblock_owned) + return st->guest_nonblock; + int fl = fcntl(host_fd, F_GETFL); + return fl >= 0 && (fl & O_NONBLOCK); +} + int64_t sys_splice(guest_t *g, int fd_in, uint64_t off_in_gva, @@ -4127,7 +4181,6 @@ int64_t sys_splice(guest_t *g, if (len == 0) return 0; - (void) flags; host_fd_ref_t in_ref, out_ref; fd_block_state_t in_st, out_st; int64_t err = host_fd_ref_open_io_state(fd_in, &in_ref, NULL, &in_st); @@ -4185,11 +4238,64 @@ int64_t sys_splice(guest_t *g, .off_out = off_out, .in_st = in_st, .out_st = out_st}; + + /* SPLICE_F_NONBLOCK makes a pipe end nonblocking for this one call, + * whatever O_NONBLOCK it carries (splice_from_pipe_next and wait_for_space, + * Linux fs/splice.c), and an AF_UNIX stream input as well. do_splice also + * raises the flag itself when a pipe end is O_NONBLOCK: the input for + * pipe-to-file, the output for file-to-pipe, either for pipe-to-pipe. The + * output is never made nonblocking here: a chunk is read before it is + * written, and a pipe or socket input cannot take back what a nonblocking + * write then failed to place. A pipe output is asked for room before each + * read instead. + */ + struct stat in_sb, out_sb; + bool in_fifo = fstat(st.in_hfd, &in_sb) == 0 && S_ISFIFO(in_sb.st_mode); + bool out_fifo = fstat(st.out_hfd, &out_sb) == 0 && S_ISFIFO(out_sb.st_mode); + bool nonblock = (flags & LINUX_SPLICE_F_NONBLOCK) || + (in_fifo && splice_end_nonblock(&st.in_st, st.in_hfd)) || + (out_fifo && splice_end_nonblock(&st.out_st, st.out_hfd)); + st.in_st.call_nonblock = nonblock && splice_flag_reaches_input(st.in_hfd); + bool gate_out = nonblock && st.off_out < 0 && out_fifo; splice_fail_t f = {0}; size_t total = 0; int64_t ret; while (total < len) { size_t n = (len - total) > chunk ? chunk : (len - total); + + /* A chunk is read before it is written, so it is capped at the room the + * output pipe has now, and the write lands without blocking. On macOS a + * ready pipe has at least PIPE_BUF bytes free, and fstat reports the + * buffer size and the bytes queued, whose difference it takes whole + * (measured, the buffer growing on demand included). + */ + if (gate_out) { + /* A pipe whose reader is gone polls POLLHUP without POLLOUT on + * macOS; wait_for_space answers it with EPIPE and SIGPIPE before + * anything is read. + */ + struct pollfd pfd = {.fd = st.out_hfd, .events = POLLOUT}; + int pr = poll(&pfd, 1, 0); + if (pr > 0 && (pfd.revents & (POLLHUP | POLLERR))) { + if (total == 0) + signal_queue(LINUX_SIGPIPE); + f.rw_error = true; /* reported only if nothing moved */ + f.saved_errno = EPIPE; + break; + } + if (pr <= 0 || !(pfd.revents & POLLOUT)) { + f.rw_error = true; + f.saved_errno = EAGAIN; + break; + } + struct stat sb; + size_t room = PIPE_BUF; + if (fstat(st.out_hfd, &sb) == 0 && sb.st_blksize > sb.st_size && + (size_t) (sb.st_blksize - sb.st_size) > room) + room = (size_t) (sb.st_blksize - sb.st_size); + if (n > room) + n = room; + } ssize_t r; if (st.off_in >= 0) { r = pread(st.in_hfd, buf, n, st.off_in); diff --git a/tests/test-io-opt.c b/tests/test-io-opt.c index c37fba88a..72763e8f8 100644 --- a/tests/test-io-opt.c +++ b/tests/test-io-opt.c @@ -14,15 +14,71 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include #include #include #include "test-harness.h" -int main(void) +/* splice(@in, NULL, a pipe, NULL, 5, SPLICE_F_NONBLOCK) on a blocking socket + * whose peer @peer sends those 5 bytes from a child after 300 ms. + * + * Returns the splice result, with errno set when it is -1. + */ +static ssize_t splice_late_socket(int in, int peer) +{ + int pfd[2]; + if (pipe(pfd) != 0) + return -2; + pid_t pid = fork(); + if (pid == 0) { + usleep(300000); + _exit(send(peer, "hello", 5, 0) == 5 ? 0 : 1); + } + alarm(5); + ssize_t rc = splice(in, NULL, pfd[1], NULL, 5, SPLICE_F_NONBLOCK); + int saved = errno; + alarm(0); + if (pid > 0) + waitpid(pid, NULL, 0); + close(pfd[0]); + close(pfd[1]); + errno = saved; + return rc; +} + +/* SPLICE_F_NONBLOCK on fd 0, which tests/test-matrix.sh holds open as an empty + * pipe the guest did not create. + */ +static int splice_stdin_nonblock(void) +{ + char path[] = "/tmp/elfuse-io-opt-stdin-XXXXXX"; + int out = mkstemp(path); + if (out < 0) + return 1; + unlink(path); + alarm(5); + ssize_t rc = splice(0, NULL, out, NULL, 16, SPLICE_F_NONBLOCK); + alarm(0); + if (rc != -1 || errno != EAGAIN) + return 1; + printf("stdin splice: EAGAIN\n"); + return 0; +} + +int main(int argc, char **argv) { int passes = 0, fails = 0; + if (argc > 1 && strcmp(argv[1], "stdin") == 0) + return splice_stdin_nonblock(); + char tmpdir[] = "/tmp/elfuse-io-opt-XXXXXX"; if (!mkdtemp(tmpdir)) { perror("mkdtemp"); @@ -352,6 +408,222 @@ int main(void) unlink(cfr_dst); } + /* SPLICE_F_NONBLOCK answers EAGAIN on an empty pipe whose writer is still + * open, even when the pipe itself is blocking. + */ + TEST("splice SPLICE_F_NONBLOCK on empty pipe"); + { + int pfd[2] = {-1, -1}; + int out = open(cfr_dst, O_CREAT | O_WRONLY | O_TRUNC, 0644); + if (out < 0 || pipe(pfd) != 0) { + FAIL("setup failed"); + } else { + alarm(5); + ssize_t rc = splice(pfd[0], NULL, out, NULL, 16, SPLICE_F_NONBLOCK); + alarm(0); + EXPECT_TRUE(rc == -1 && errno == EAGAIN, "not EAGAIN"); + } + if (pfd[0] >= 0) { + close(pfd[0]); + close(pfd[1]); + } + if (out >= 0) + close(out); + unlink(cfr_dst); + } + + /* With the output pipe full, SPLICE_F_NONBLOCK answers EAGAIN before it + * reads anything; once a reader frees some room, it moves only what fits. + * Either way every byte is in one pipe or the other. + */ + TEST("splice SPLICE_F_NONBLOCK into a full pipe loses nothing"); + { + enum { IN_BYTES = 8192 }; + static char fill[4096]; + int a[2] = {-1, -1}, b[2] = {-1, -1}; + int ok = pipe(a) == 0 && pipe(b) == 0 && + write(a[1], fill, sizeof(fill)) == sizeof(fill) && + write(a[1], fill, sizeof(fill)) == sizeof(fill) && + fcntl(b[1], F_SETFL, O_NONBLOCK) == 0; + while (ok && write(b[1], fill, sizeof(fill)) > 0) + ; + while (ok && write(b[1], fill, 1) > 0) + ; + ok = ok && fcntl(b[1], F_SETFL, 0) == 0; + const char *why = "setup failed"; + ssize_t moved = 0; + int left = -1; + + if (ok) { + why = "full output not EAGAIN"; + ok = splice(a[0], NULL, b[1], NULL, IN_BYTES, SPLICE_F_NONBLOCK) == + -1 && + errno == EAGAIN; + } + if (ok) { + why = "input consumed on EAGAIN"; + ok = ioctl(a[0], FIONREAD, &left) == 0 && left == IN_BYTES; + } + if (ok) { + why = "nothing moved after the reader freed room"; + ok = read(b[0], fill, sizeof(fill)) == sizeof(fill); + moved = splice(a[0], NULL, b[1], NULL, IN_BYTES, SPLICE_F_NONBLOCK); + ok = ok && moved > 0; + } + if (ok) { + why = "bytes lost"; + ok = ioctl(a[0], FIONREAD, &left) == 0 && moved + left == IN_BYTES; + } + if (ok) + PASS(); + else + FAIL(why); + + int fds[] = {a[0], a[1], b[0], b[1]}; + for (size_t i = 0; i < sizeof(fds) / sizeof(fds[0]); i++) + if (fds[i] >= 0) + close(fds[i]); + } + + /* SPLICE_F_NONBLOCK reaches an AF_UNIX stream input + * (unix_stream_splice_read), but a TCP input follows its own O_NONBLOCK + * (tcp_splice_read) and waits for data. + */ + TEST("splice SPLICE_F_NONBLOCK on socket inputs"); + { + int sv[2] = {-1, -1}; + int ls = socket(AF_INET, SOCK_STREAM, 0); + int cli = socket(AF_INET, SOCK_STREAM, 0); + int srv = -1; + struct sockaddr_in sa = {.sin_family = AF_INET, + .sin_addr.s_addr = htonl(INADDR_LOOPBACK)}; + socklen_t sl = sizeof(sa); + int ok = socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0 && ls >= 0 && + cli >= 0 && bind(ls, (struct sockaddr *) &sa, sl) == 0 && + listen(ls, 1) == 0 && + getsockname(ls, (struct sockaddr *) &sa, &sl) == 0 && + connect(cli, (struct sockaddr *) &sa, sl) == 0 && + (srv = accept(ls, NULL, NULL)) >= 0; + const char *why = "setup failed"; + + if (ok) { + why = "AF_UNIX input not EAGAIN"; + ok = splice_late_socket(sv[0], sv[1]) == -1 && errno == EAGAIN; + } + if (ok) { + why = "TCP input did not wait for data"; + ok = splice_late_socket(srv, cli) == 5; + } + if (ok) + PASS(); + else + FAIL(why); + + int fds[] = {sv[0], sv[1], ls, cli, srv}; + for (size_t i = 0; i < sizeof(fds) / sizeof(fds[0]); i++) + if (fds[i] >= 0) + close(fds[i]); + } + + /* do_splice raises SPLICE_F_NONBLOCK itself when a pipe end is O_NONBLOCK, + * so a full O_NONBLOCK output answers EAGAIN before anything is read, and + * an empty AF_UNIX input does not wait. + */ + TEST("splice follows a pipe end's O_NONBLOCK"); + { + int sv[2] = {-1, -1}, a[2] = {-1, -1}, b[2] = {-1, -1}; + int ok = socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0 && pipe(a) == 0 && + pipe(b) == 0 && write(sv[1], "0123456789", 10) == 10 && + write(a[1], "xyz", 3) == 3 && + fcntl(b[1], F_SETFL, O_NONBLOCK) == 0; + static char fill[4096]; + while (ok && write(b[1], fill, sizeof(fill)) > 0) + ; + while (ok && write(b[1], fill, 1) > 0) + ; + const char *why = "setup failed"; + int left = -1; + + if (ok) { + why = "AF_UNIX input to a full pipe not EAGAIN, or bytes lost"; + ok = splice(sv[0], NULL, b[1], NULL, 10, 0) == -1 && + errno == EAGAIN && ioctl(sv[0], FIONREAD, &left) == 0 && + left == 10; + } + if (ok) { + why = "pipe input to a full pipe not EAGAIN, or bytes lost"; + ok = splice(a[0], NULL, b[1], NULL, 3, 0) == -1 && + errno == EAGAIN && ioctl(a[0], FIONREAD, &left) == 0 && + left == 3; + } + if (ok) { + char drain[4096]; + while (read(sv[0], drain, sizeof(drain)) == sizeof(drain)) + ; + ok = read(b[0], drain, sizeof(drain)) == sizeof(drain); + why = "empty AF_UNIX input waited"; + alarm(5); + ok = ok && splice(sv[0], NULL, b[1], NULL, 10, 0) == -1 && + errno == EAGAIN; + alarm(0); + } + if (ok) + PASS(); + else + FAIL(why); + + int fds[] = {sv[0], sv[1], a[0], a[1], b[0], b[1]}; + for (size_t i = 0; i < sizeof(fds) / sizeof(fds[0]); i++) + if (fds[i] >= 0) + close(fds[i]); + } + + /* An output pipe whose reader is gone fails with EPIPE and SIGPIPE before + * the input is read (wait_for_space). + */ + TEST("splice SPLICE_F_NONBLOCK into a pipe with no reader"); + { + int sv[2] = {-1, -1}, p[2] = {-1, -1}; + int ok = socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0 && pipe(p) == 0 && + write(sv[1], "0123456789", 10) == 10; + if (ok) { + close(p[0]); + p[0] = -1; + } + sigset_t pipe_set, old; + sigemptyset(&pipe_set); + sigaddset(&pipe_set, SIGPIPE); + sigprocmask(SIG_BLOCK, &pipe_set, &old); + int left = -1, sig = 0; + struct timespec now = {0}; + const char *why = "setup failed"; + + if (ok) { + why = "not EPIPE"; + ok = splice(sv[0], NULL, p[1], NULL, 10, SPLICE_F_NONBLOCK) == -1 && + errno == EPIPE; + } + if (ok) { + why = "input consumed"; + ok = ioctl(sv[0], FIONREAD, &left) == 0 && left == 10; + } + if (ok) { + why = "no SIGPIPE"; + sig = sigtimedwait(&pipe_set, NULL, &now); + ok = sig == SIGPIPE; + } + sigprocmask(SIG_SETMASK, &old, NULL); + if (ok) + PASS(); + else + FAIL(why); + + int fds[] = {sv[0], sv[1], p[0], p[1]}; + for (size_t i = 0; i < sizeof(fds) / sizeof(fds[0]); i++) + if (fds[i] >= 0) + close(fds[i]); + } + /* Cleanup. The other three fixtures are unlinked by the blocks that make * them, so only these two are left for rmdir to trip over. */ diff --git a/tests/test-matrix.sh b/tests/test-matrix.sh index 7722c17bb..a276766f7 100755 --- a/tests/test-matrix.sh +++ b/tests/test-matrix.sh @@ -630,6 +630,29 @@ test_pipe() local output rc if output=$(printf '%s' "$input" | $runner "$@"); then rc=0; else rc=$?; fi + pipe_report "$label" "$pattern" "$rc" "$output" +} + +# test_held_stdin