mirror of
https://github.com/RsyncProject/rsync.git
synced 2026-09-12 21:28:25 -04:00
io/socket: address review of the poll() conversion
Follow-up to the FD_SETSIZE fix, covering the points raised in review. Negative/overflowing I/O timeouts. set_io_timeout() could produce a negative select_timeout (a peer-supplied MSG_IO_TIMEOUT value was applied unchecked), and every wait now passes select_timeout * 1000 to poll(), where a negative millisecond count means "wait forever" -- so a hostile or buggy peer could stall the other side and bypass keepalives entirely. select() used to reject that with EINVAL, which kept the loop and check_timeout() running. Clamp a negative argument to 0, compute allowed_lull without overflowing near INT_MAX (secs / 2 + secs % 2), ignore a non-positive MSG_IO_TIMEOUT value, and funnel all three waits through poll_timeout_ms(), which keeps the count positive and bounded. The daemon accept loop had the same fd_set overflow. start_accept_loop() still stored listening sockets in an fd_set, so a daemon started with enough descriptors already open got listener fds >= FD_SETSIZE and hit the same undefined behaviour at startup -- verified: with the old code a transfer through such a daemon yields nothing, with this change it succeeds. Converted it to poll() as well. Readiness testing. Treating any non-zero revents as ordinary readiness was wrong: poll() reports POLLERR/POLLHUP/POLLNVAL unrequested, and an invalid fd shows up as POLLNVAL on a successful poll() rather than -1/EBADF, which left the EBADF branches dead and let an invalid ff_forward_fd reach forward_filesfrom_data() (where EBADF reads as EOF). Use role-specific masks (POLL_RD_BITS / POLL_WR_BITS), handle POLLNVAL explicitly in all three loops, and request POLLPRI so select()'s old exception set is not silently dropped. A bidirectional fd is no longer entered twice. A direct daemon connection uses one fd for both directions; it now occupies a single pollfd row with OR-ed events instead of two rows carrying different masks, which also avoids the Cygwin < 3.3.6 duplicate-entry readiness bug. poll() is now a declared requirement: configure.ac checks for poll.h and poll(), failing with a clear message rather than leaving it implicit. The test no longer hardcodes FD_SETSIZE (1024 on glibc but 65536 on 64-bit Solaris, where it would have opened too few fds and passed vacuously); it asks the C library for the real value via a small compiled probe and skips if that is unavailable. Its description now also covers the fortified-libc case, where the pre-fix result is an abort rather than a hang.
This commit is contained in:
1 parent
4a751a2ceb
commit
7ef165dd45
4 files changed
+174
-73
No files matched your search
+7
-1
@@ -5,7 +5,7 @@ AC_INIT([rsync],[ ],[https://rsync.samba.org/bug-tracking.html])
|
||||
AC_C_BIGENDIAN
|
||||
AC_HEADER_DIRENT
|
||||
AC_HEADER_SYS_WAIT
|
||||
AC_CHECK_HEADERS(sys/fcntl.h sys/select.h fcntl.h sys/time.h sys/unistd.h \
|
||||
AC_CHECK_HEADERS(poll.h sys/fcntl.h sys/select.h fcntl.h sys/time.h sys/unistd.h \
|
||||
unistd.h utime.h compat.h sys/param.h ctype.h sys/wait.h sys/stat.h \
|
||||
sys/ioctl.h sys/filio.h string.h stdlib.h sys/socket.h sys/mode.h grp.h \
|
||||
sys/un.h sys/attr.h arpa/inet.h arpa/nameser.h locale.h sys/types.h \
|
||||
@@ -909,6 +909,12 @@ AC_HAVE_TYPE([struct stat64], [#include <stdio.h>
|
||||
|
||||
# if we can't find strcasecmp, look in -lresolv (for Unixware at least)
|
||||
#
|
||||
dnl rsync's I/O readiness loops use poll() rather than select() so that a
|
||||
dnl file descriptor at or above FD_SETSIZE cannot overflow an fd_set (which
|
||||
dnl is undefined behaviour and could hang the transfer). poll() is in
|
||||
dnl POSIX.1-2001; fail early and clearly if this target lacks it.
|
||||
AC_CHECK_FUNCS([poll], , [AC_MSG_ERROR([rsync requires poll(); please report the platform to the rsync developers])])
|
||||
|
||||
AC_CHECK_FUNCS(strcasecmp)
|
||||
if test x"$ac_cv_func_strcasecmp" = x"no"; then
|
||||
AC_CHECK_LIB(resolv, strcasecmp)
|
||||
|
||||
@@ -33,6 +33,12 @@
|
||||
|
||||
#include <poll.h>
|
||||
|
||||
/* Readiness bits we act on. poll() can report POLLERR/POLLHUP/POLLNVAL even
|
||||
* when they were not requested, and POLLPRI stands in for select()'s old
|
||||
* exception set. */
|
||||
#define POLL_RD_BITS (POLLIN | POLLPRI | POLLERR | POLLHUP)
|
||||
#define POLL_WR_BITS (POLLOUT | POLLERR | POLLHUP)
|
||||
|
||||
/** If no timeout is specified then use a 60 second I/O timeout */
|
||||
#define SELECT_TIMEOUT 60
|
||||
|
||||
@@ -115,6 +121,19 @@ static xbuf ff_xb = EMPTY_XBUF;
|
||||
static xbuf iconv_buf = EMPTY_XBUF;
|
||||
#endif
|
||||
static int select_timeout = SELECT_TIMEOUT;
|
||||
|
||||
/* Turn select_timeout (in seconds) into a poll() millisecond count, keeping it
|
||||
* positive and bounded. A negative count means "wait forever" to poll(), which
|
||||
* would bypass our keepalives and timeout enforcement entirely. */
|
||||
static int poll_timeout_ms(void)
|
||||
{
|
||||
int secs = select_timeout;
|
||||
|
||||
if (secs <= 0 || secs > SELECT_TIMEOUT)
|
||||
secs = SELECT_TIMEOUT;
|
||||
return secs * 1000;
|
||||
}
|
||||
|
||||
static int active_filecnt = 0;
|
||||
static OFF_T active_bytecnt = 0;
|
||||
static int first_message = 1;
|
||||
@@ -251,12 +270,12 @@ static size_t safe_read(int fd, char *buf, size_t len)
|
||||
/* We use poll() rather than select() so that a high-numbered fd
|
||||
* (>= FD_SETSIZE) cannot overflow an fd_set bitmap. */
|
||||
pfd.fd = fd;
|
||||
pfd.events = POLLIN;
|
||||
pfd.events = POLLIN | POLLPRI;
|
||||
pfd.revents = 0;
|
||||
|
||||
cnt = poll(&pfd, 1, select_timeout * 1000);
|
||||
cnt = poll(&pfd, 1, poll_timeout_ms());
|
||||
if (cnt <= 0) {
|
||||
if (cnt < 0 && errno == EBADF) {
|
||||
if (cnt < 0 && errno != EINTR && errno != EAGAIN) {
|
||||
rsyserr(FERROR, errno, "safe_read poll failed");
|
||||
exit_cleanup(RERR_FILEIO);
|
||||
}
|
||||
@@ -264,7 +283,13 @@ static size_t safe_read(int fd, char *buf, size_t len)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pfd.revents) {
|
||||
/* An invalid fd is reported here rather than via poll()'s return. */
|
||||
if (pfd.revents & POLLNVAL) {
|
||||
rsyserr(FERROR, EBADF, "safe_read poll failed");
|
||||
exit_cleanup(RERR_FILEIO);
|
||||
}
|
||||
|
||||
if (pfd.revents & POLL_RD_BITS) {
|
||||
ssize_t n = read(fd, buf + got, len - got);
|
||||
if (DEBUG_GTE(IO, 2)) {
|
||||
rprintf(FINFO, "[%s] safe_read(%d)=%" SIZE_T_FMT_MOD "d\n",
|
||||
@@ -337,9 +362,9 @@ static void safe_write(int fd, const char *buf, size_t len)
|
||||
pfd.events = POLLOUT;
|
||||
pfd.revents = 0;
|
||||
|
||||
cnt = poll(&pfd, 1, select_timeout * 1000);
|
||||
cnt = poll(&pfd, 1, poll_timeout_ms());
|
||||
if (cnt <= 0) {
|
||||
if (cnt < 0 && errno == EBADF) {
|
||||
if (cnt < 0 && errno != EINTR && errno != EAGAIN) {
|
||||
rsyserr(FERROR, errno, "safe_write poll failed on %s", what_fd_is(fd));
|
||||
exit_cleanup(RERR_FILEIO);
|
||||
}
|
||||
@@ -348,7 +373,12 @@ static void safe_write(int fd, const char *buf, size_t len)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pfd.revents) {
|
||||
if (pfd.revents & POLLNVAL) {
|
||||
rsyserr(FERROR, EBADF, "safe_write poll failed on %s", what_fd_is(fd));
|
||||
exit_cleanup(RERR_FILEIO);
|
||||
}
|
||||
|
||||
if (pfd.revents & POLL_WR_BITS) {
|
||||
n = write(fd, buf, len);
|
||||
if (n < 0) {
|
||||
if (errno == EINTR)
|
||||
@@ -657,7 +687,7 @@ static char *perform_io(size_t needed, int flags)
|
||||
if (iobuf.in_fd >= 0 && iobuf.in.size - iobuf.in.len) {
|
||||
if (!read_batch || batch_fd >= 0) {
|
||||
pfds[npfds].fd = iobuf.in_fd;
|
||||
pfds[npfds].events = POLLIN;
|
||||
pfds[npfds].events = POLLIN | POLLPRI;
|
||||
pfds[npfds].revents = 0;
|
||||
in_pollpos = npfds++;
|
||||
}
|
||||
@@ -714,10 +744,18 @@ static char *perform_io(size_t needed, int flags)
|
||||
} else
|
||||
out = NULL;
|
||||
if (out) {
|
||||
pfds[npfds].fd = iobuf.out_fd;
|
||||
pfds[npfds].events = POLLOUT;
|
||||
pfds[npfds].revents = 0;
|
||||
out_pollpos = npfds++;
|
||||
/* A direct daemon connection uses one fd for both
|
||||
* directions; give it a single row with both events
|
||||
* rather than two rows carrying different masks. */
|
||||
if (in_pollpos >= 0 && iobuf.out_fd == iobuf.in_fd) {
|
||||
pfds[in_pollpos].events |= POLLOUT;
|
||||
out_pollpos = in_pollpos;
|
||||
} else {
|
||||
pfds[npfds].fd = iobuf.out_fd;
|
||||
pfds[npfds].events = POLLOUT;
|
||||
pfds[npfds].revents = 0;
|
||||
out_pollpos = npfds++;
|
||||
}
|
||||
if (iobuf.out_fd > max_fd)
|
||||
max_fd = iobuf.out_fd;
|
||||
}
|
||||
@@ -757,10 +795,10 @@ static char *perform_io(size_t needed, int flags)
|
||||
poll_timeout = 0;
|
||||
else {
|
||||
extra_flist_sending_enabled = False;
|
||||
poll_timeout = select_timeout * 1000;
|
||||
poll_timeout = poll_timeout_ms();
|
||||
}
|
||||
} else
|
||||
poll_timeout = select_timeout * 1000;
|
||||
poll_timeout = poll_timeout_ms();
|
||||
|
||||
cnt = poll(pfds, npfds, poll_timeout);
|
||||
|
||||
@@ -784,7 +822,20 @@ static char *perform_io(size_t needed, int flags)
|
||||
pfds[out_pollpos].revents = 0;
|
||||
}
|
||||
|
||||
if (iobuf.in_fd >= 0 && in_pollpos >= 0 && pfds[in_pollpos].revents) {
|
||||
if (cnt > 0) {
|
||||
/* poll() reports a bad fd here, not via its return value. */
|
||||
int p;
|
||||
for (p = 0; p < npfds; p++) {
|
||||
if (pfds[p].revents & POLLNVAL) {
|
||||
msgs2stderr = 1;
|
||||
rsyserr(FERROR, EBADF, "perform_io: poll reported an invalid fd");
|
||||
exit_cleanup(RERR_SOCKETIO);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (iobuf.in_fd >= 0 && in_pollpos >= 0
|
||||
&& pfds[in_pollpos].revents & POLL_RD_BITS) {
|
||||
size_t len, pos = iobuf.in.pos + iobuf.in.len;
|
||||
ssize_t n;
|
||||
if (pos >= iobuf.in.size) {
|
||||
@@ -833,7 +884,7 @@ static char *perform_io(size_t needed, int flags)
|
||||
exit_cleanup(RERR_TIMEOUT);
|
||||
}
|
||||
|
||||
if (out && out_pollpos >= 0 && pfds[out_pollpos].revents) {
|
||||
if (out && out_pollpos >= 0 && pfds[out_pollpos].revents & POLL_WR_BITS) {
|
||||
size_t len = iobuf.raw_flushing_ends_before ? iobuf.raw_flushing_ends_before - out->pos : out->len;
|
||||
ssize_t n;
|
||||
|
||||
@@ -894,7 +945,8 @@ static char *perform_io(size_t needed, int flags)
|
||||
wait_for_receiver(); /* generator only */
|
||||
}
|
||||
|
||||
if (ff_forward_fd >= 0 && ff_pollpos >= 0 && pfds[ff_pollpos].revents) {
|
||||
if (ff_forward_fd >= 0 && ff_pollpos >= 0
|
||||
&& pfds[ff_pollpos].revents & POLL_RD_BITS) {
|
||||
/* This can potentially flush all output and enable
|
||||
* multiplexed output, so keep this last in the loop
|
||||
* and be sure to not cache anything that would break
|
||||
@@ -1153,8 +1205,11 @@ void io_set_sock_fds(int f_in, int f_out)
|
||||
|
||||
void set_io_timeout(int secs)
|
||||
{
|
||||
if (secs < 0)
|
||||
secs = 0;
|
||||
io_timeout = secs;
|
||||
allowed_lull = (io_timeout + 1) / 2;
|
||||
/* Round up without overflowing when secs is near INT_MAX. */
|
||||
allowed_lull = secs / 2 + secs % 2;
|
||||
|
||||
if (!io_timeout || allowed_lull > SELECT_TIMEOUT)
|
||||
select_timeout = SELECT_TIMEOUT;
|
||||
@@ -1559,7 +1614,10 @@ static void read_a_msg(void)
|
||||
goto invalid_msg;
|
||||
val = raw_read_int();
|
||||
iobuf.in_multiplexed = 1;
|
||||
if (!io_timeout || io_timeout > val) {
|
||||
/* Ignore a non-positive value: it would otherwise disable our
|
||||
* timeout entirely (and a negative one used to be rejected by
|
||||
* select(), but would make poll() wait forever). */
|
||||
if (val > 0 && (!io_timeout || io_timeout > val)) {
|
||||
if (INFO_GTE(MISC, 2))
|
||||
rprintf(FINFO, "Setting --timeout=%d to match server\n", val);
|
||||
set_io_timeout(val);
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
* emulate it using the KAME implementation. */
|
||||
|
||||
#include "rsync.h"
|
||||
#include <poll.h>
|
||||
#include "itypes.h"
|
||||
#include "ifuncs.h"
|
||||
#ifdef HAVE_NETINET_IN_SYSTM_H
|
||||
@@ -536,8 +537,8 @@ static void sigchld_handler(UNUSED(int val))
|
||||
|
||||
void start_accept_loop(int port, int (*fn)(int, int))
|
||||
{
|
||||
fd_set deffds;
|
||||
int *sp, maxfd, i;
|
||||
struct pollfd *pfds;
|
||||
int *sp, nsp, i;
|
||||
|
||||
#ifdef HAVE_SIGACTION
|
||||
sigact.sa_flags = SA_NOCLDSTOP;
|
||||
@@ -549,8 +550,12 @@ void start_accept_loop(int port, int (*fn)(int, int))
|
||||
exit_cleanup(RERR_SOCKETIO);
|
||||
|
||||
/* ready to listen */
|
||||
FD_ZERO(&deffds);
|
||||
for (i = 0, maxfd = -1; sp[i] >= 0; i++) {
|
||||
for (nsp = 0; sp[nsp] >= 0; nsp++) {}
|
||||
/* poll() rather than select(): a listening fd at or above FD_SETSIZE
|
||||
* would overflow an fd_set, which is undefined behaviour (issue #231). */
|
||||
if (!(pfds = new_array(struct pollfd, nsp ? nsp : 1)))
|
||||
out_of_memory("start_accept_loop");
|
||||
for (i = 0; sp[i] >= 0; i++) {
|
||||
if (listen(sp[i], lp_listen_backlog()) < 0) {
|
||||
rsyserr(FERROR, errno, "listen() on socket failed");
|
||||
#ifdef INET6
|
||||
@@ -560,36 +565,32 @@ void start_accept_loop(int port, int (*fn)(int, int))
|
||||
#endif
|
||||
exit_cleanup(RERR_SOCKETIO);
|
||||
}
|
||||
FD_SET(sp[i], &deffds);
|
||||
if (maxfd < sp[i])
|
||||
maxfd = sp[i];
|
||||
pfds[i].fd = sp[i];
|
||||
pfds[i].events = POLLIN;
|
||||
pfds[i].revents = 0;
|
||||
}
|
||||
|
||||
/* now accept incoming connections - forking a new process
|
||||
* for each incoming connection */
|
||||
while (1) {
|
||||
fd_set fds;
|
||||
pid_t pid;
|
||||
int fd;
|
||||
struct sockaddr_storage addr;
|
||||
socklen_t addrlen = sizeof addr;
|
||||
|
||||
/* close log file before the potentially very long select so
|
||||
/* close log file before the potentially very long wait so the
|
||||
* file can be trimmed by another process instead of growing
|
||||
* forever */
|
||||
logfile_close();
|
||||
|
||||
#ifdef FD_COPY
|
||||
FD_COPY(&deffds, &fds);
|
||||
#else
|
||||
fds = deffds;
|
||||
#endif
|
||||
for (i = 0; i < nsp; i++)
|
||||
pfds[i].revents = 0;
|
||||
|
||||
if (select(maxfd + 1, &fds, NULL, NULL, NULL) < 1)
|
||||
if (poll(pfds, nsp, -1) < 1)
|
||||
continue;
|
||||
|
||||
for (i = 0, fd = -1; sp[i] >= 0; i++) {
|
||||
if (FD_ISSET(sp[i], &fds)) {
|
||||
for (i = 0, fd = -1; i < nsp; i++) {
|
||||
if (pfds[i].revents & (POLLIN | POLLERR | POLLHUP)) {
|
||||
fd = accept(sp[i], (struct sockaddr *)&addr, &addrlen);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,84 +1,120 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regression test for issue #231: rsync must not hang when its I/O file
|
||||
descriptors are numbered at/above FD_SETSIZE.
|
||||
"""Regression test for issue #231: high-numbered I/O descriptors.
|
||||
|
||||
rsync's I/O loops in io.c used to call select() with fd_set bitmaps, which can
|
||||
only represent descriptors below FD_SETSIZE (1024 with glibc). If rsync is
|
||||
started with many descriptors already open -- e.g. inherited from a parent that
|
||||
leaked fds -- its own socket/pipe fds get allocated above 1024. FD_SET() and
|
||||
rsync's I/O readiness loops used to call select() with fd_set bitmaps, which can
|
||||
only represent descriptors below FD_SETSIZE. If rsync is started with many
|
||||
descriptors already open -- e.g. inherited from a parent that leaked fds -- its
|
||||
own socket and pipe fds get allocated at or above that limit. FD_SET() and
|
||||
FD_ISSET() then index past the end of the fixed-size fd_set, which is undefined
|
||||
behavior: select() reports the fd ready, but FD_ISSET() reads the out-of-bounds
|
||||
bit as 0, so the read/write never happens and rsync spins at 100% CPU forever
|
||||
("rsync hangs, 100% of one CPU, no progress").
|
||||
behaviour: with a plain build select() reports the fd ready while FD_ISSET()
|
||||
reads the out-of-bounds bit as 0, so the read/write never happens and rsync
|
||||
spins at 100% CPU forever; with a fortified libc the process instead aborts
|
||||
("bit out of range 0 - FD_SETSIZE in fd_set"). Either way the transfer never
|
||||
completes.
|
||||
|
||||
We reproduce that deterministically by opening enough inheritable dummy fds to
|
||||
push rsync's descriptors past FD_SETSIZE, then running an ordinary transfer
|
||||
with close_fds=False so the child inherits them. With the poll()-based io.c
|
||||
the transfer finishes instantly; with the old select()-based code it never
|
||||
finishes and is caught by the timeout. The cross-over is binary (instant vs.
|
||||
infinite), so the timeout is a robust signal rather than a timing race.
|
||||
We reproduce that by opening enough inheritable dummy fds to push rsync's
|
||||
descriptors past FD_SETSIZE, then running an ordinary transfer with
|
||||
close_fds=False so the child inherits them. With the poll()-based I/O the
|
||||
transfer finishes immediately; without it, it hangs or aborts.
|
||||
|
||||
FD_SETSIZE is *not* hardcoded: it is 1024 with glibc but 65536 on 64-bit
|
||||
Solaris, and assuming 1024 there would open too few fds to cross the limit and
|
||||
pass vacuously. We ask the C library for the real value instead, and skip when
|
||||
we cannot (no compiler, or the fd limit cannot be raised far enough).
|
||||
"""
|
||||
|
||||
import os
|
||||
import resource
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
from rsyncfns import (
|
||||
FROMDIR, TODIR, rmtree, rsync_argv, test_fail, test_skipped,
|
||||
)
|
||||
|
||||
# select()'s fd_set holds FD_SETSIZE bits; glibc and most libcs use 1024.
|
||||
FD_SETSIZE = 1024
|
||||
NDUMMY = FD_SETSIZE + 80 # force rsync's fds comfortably past the limit
|
||||
TIMEOUT = 30 # poll build: ~instant; select build: hangs forever
|
||||
TIMEOUT = 30 # poll() build: ~instant; the bug: hangs (or aborts)
|
||||
|
||||
|
||||
def probe_fd_setsize():
|
||||
"""Ask the C library for FD_SETSIZE rather than assuming a value."""
|
||||
cc = os.environ.get('CC') or shutil.which('cc') or shutil.which('gcc')
|
||||
if not cc:
|
||||
return None
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
src = os.path.join(td, 'fdss.c')
|
||||
exe = os.path.join(td, 'fdss')
|
||||
with open(src, 'w') as f:
|
||||
f.write('#include <stdio.h>\n'
|
||||
'#include <sys/select.h>\n'
|
||||
'int main(void){ printf("%d\\n", (int)FD_SETSIZE); return 0; }\n')
|
||||
if subprocess.run([cc, src, '-o', exe],
|
||||
capture_output=True).returncode != 0:
|
||||
return None
|
||||
proc = subprocess.run([exe], capture_output=True, text=True)
|
||||
if proc.returncode != 0:
|
||||
return None
|
||||
try:
|
||||
return int(proc.stdout.strip())
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
fd_setsize = probe_fd_setsize()
|
||||
if not fd_setsize:
|
||||
test_skipped("could not determine FD_SETSIZE (no usable C compiler)")
|
||||
|
||||
# Push rsync's descriptors comfortably past the limit.
|
||||
ndummy = fd_setsize + 80
|
||||
want = ndummy + 64
|
||||
|
||||
# We must be able to open enough descriptors to cross FD_SETSIZE.
|
||||
soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
|
||||
want = NDUMMY + 64
|
||||
if soft < want:
|
||||
if hard != resource.RLIM_INFINITY and hard < want:
|
||||
test_skipped(f"RLIMIT_NOFILE hard cap {hard} < {want}; cannot place "
|
||||
"fds above FD_SETSIZE to exercise issue #231")
|
||||
test_skipped(f"RLIMIT_NOFILE hard cap {hard} < {want}; cannot place fds "
|
||||
f"above FD_SETSIZE ({fd_setsize}) to exercise issue #231")
|
||||
resource.setrlimit(resource.RLIMIT_NOFILE, (want, hard))
|
||||
|
||||
# A small tree to transfer.
|
||||
rmtree(FROMDIR)
|
||||
rmtree(TODIR)
|
||||
FROMDIR.mkdir(parents=True, exist_ok=True)
|
||||
payload = {f"f{i}": os.urandom(1000) for i in range(20)}
|
||||
TODIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
payload = {f'f{i}': os.urandom(1000) for i in range(20)}
|
||||
for name, data in payload.items():
|
||||
(FROMDIR / name).write_bytes(data)
|
||||
|
||||
# Occupy the low fd numbers with inheritable dummy descriptors so that the
|
||||
# rsync child's socket/pipe fds are forced above FD_SETSIZE. Python opens fds
|
||||
# O_CLOEXEC by default, so mark each inheritable and use close_fds=False.
|
||||
# Occupy the low fd numbers with inheritable dummy descriptors so the rsync
|
||||
# child's socket/pipe fds land above FD_SETSIZE. Python opens fds O_CLOEXEC by
|
||||
# default, so mark each inheritable and use close_fds=False.
|
||||
dummies = []
|
||||
try:
|
||||
while True:
|
||||
fd = os.open(os.devnull, os.O_RDONLY)
|
||||
os.set_inheritable(fd, True)
|
||||
dummies.append(fd)
|
||||
if fd >= NDUMMY:
|
||||
if fd >= ndummy:
|
||||
break
|
||||
|
||||
argv = rsync_argv('-a', f'{FROMDIR}/', f'{TODIR}/')
|
||||
try:
|
||||
proc = subprocess.run(argv, timeout=TIMEOUT, close_fds=False)
|
||||
except subprocess.TimeoutExpired:
|
||||
test_fail("rsync hung with high-numbered fds -- select()/fd_set "
|
||||
"overflow (issue #231 regression)")
|
||||
test_fail(f"rsync did not finish within {TIMEOUT}s with fds above "
|
||||
f"FD_SETSIZE ({fd_setsize}) -- select()/fd_set overflow "
|
||||
"(issue #231 regression)")
|
||||
finally:
|
||||
for fd in dummies:
|
||||
os.close(fd)
|
||||
|
||||
if proc.returncode != 0:
|
||||
test_fail(f"rsync exited {proc.returncode}: {' '.join(argv)}")
|
||||
test_fail(f"rsync exited {proc.returncode} with fds above FD_SETSIZE "
|
||||
f"({fd_setsize}); a fortified libc aborts on the fd_set overflow "
|
||||
"(issue #231 regression)")
|
||||
|
||||
# The cap only matters if it stayed correct: verify every file transferred.
|
||||
for name, data in payload.items():
|
||||
got = (TODIR / name).read_bytes()
|
||||
if got != data:
|
||||
if (TODIR / name).read_bytes() != data:
|
||||
test_fail(f"{name} differs after a high-fd transfer")
|
||||
|
||||
print(f"issue #231: transfer with fds >= {NDUMMY} (above FD_SETSIZE) "
|
||||
print(f"issue #231: transfer with fds above FD_SETSIZE ({fd_setsize}) "
|
||||
"completed correctly")
|
||||
Reference in new issue
Block a user