Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@
on macOS. Native configuration now proves lookup, creation,
permission-revocation and no-follow behaviour before selecting a traversal
descriptor. Unverified targets fail closed instead of using raceable paths.
- Rejected config and filter files after read or stream setup errors instead
of accepting partial contents. MOTD and `--early-input` reads now stop on
I/O errors instead of looping; interrupted reads retain their data.
- Fixed an out-of-bounds read while reporting an incomplete daemon config
section header.
- Preserved sender file-open errors when closing a held directory descriptor
during `--copy-links` transfers.
- Changed to `/` immediately after a module chroot, before selecting an inner
module directory. A failed working-directory reset aborts the connection.

# NEWS for rsync 3.5.2 (4 Nov 2026)

Expand Down
47 changes: 43 additions & 4 deletions clientserver.c
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,21 @@ int start_socket_client(char *host, int remote_argc, char *remote_argv[],
return ret ? ret : client_run(fd, fd, -1, argc, argv);
}

static size_t fread_retry(void *buffer, size_t length, FILE *file)
{
size_t total = fread(buffer, 1, length, file);
while (total < length && ferror(file) && errno == EINTR) {
clearerr(file);
/* Preserve partial input across EINTR. Clang 21 does not model
* the recoverable position after an interrupted stream read. */
#ifdef __clang_analyzer__
__attribute__((suppress))
#endif
total += fread((char *)buffer + total, 1, length - total, file);
}
return total;
}

static int exchange_protocols(int f_in, int f_out, char *buf, size_t bufsiz, int am_client)
{
int remote_sub = -1;
Expand All @@ -191,9 +206,20 @@ static int exchange_protocols(int f_in, int f_out, char *buf, size_t bufsiz, int
* symlinks not owned by uid 0 or our euid. */
int motd_fd = open_no_attacker_symlinks(motd, O_RDONLY, 0);
FILE *f = motd_fd >= 0 ? fdopen(motd_fd, "r") : NULL;
if (!f && motd_fd >= 0) close(motd_fd);
if (!f && motd_fd >= 0) {
int fdopen_errno = errno;
close(motd_fd);
rsyserr(FLOG, fdopen_errno, "failed to open motd file %s", motd);
return -1;
}
while (f && !feof(f)) {
int len = fread(buf, 1, bufsiz - 1, f);
int len = fread_retry(buf, bufsiz - 1, f);
if (ferror(f)) {
int read_errno = errno;
fclose(f);
rsyserr(FLOG, read_errno, "failed to read motd file %s", motd);
return -1;
}
if (len > 0)
write_buf(f_out, buf, len);
}
Expand Down Expand Up @@ -306,7 +332,11 @@ int start_inband_exchange(int f_in, int f_out, const char *user, int argc, char
* our euid anywhere in the path. */
int ei_fd = open_no_attacker_symlinks(early_input_file, O_RDONLY, 0);
FILE *f = ei_fd >= 0 ? fdopen(ei_fd, "rb") : NULL;
if (!f && ei_fd >= 0) close(ei_fd);
if (!f && ei_fd >= 0) {
int fdopen_errno = errno;
close(ei_fd);
errno = fdopen_errno;
}
if (!f || do_fstat(fileno(f), &st) < 0) {
rsyserr(FERROR, errno, "failed to open %s", early_input_file);
if (f)
Expand All @@ -331,7 +361,14 @@ int start_inband_exchange(int f_in, int f_out, const char *user, int argc, char
free(modname);
return -1;
}
len = fread(line, 1, early_input_len, f);
len = fread_retry(line, early_input_len, f);
if (ferror(f)) {
int read_errno = errno;
fclose(f);
free(modname);
rsyserr(FERROR, read_errno, "failed to read early-input file %s", early_input_file);
return -1;
}
if (len > 0) {
write_buf(f_out, line, len);
early_input_len -= len;
Expand Down Expand Up @@ -1056,6 +1093,8 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
io_printf(f_out, "@ERROR: chroot failed\n");
return -1;
}
if (chdir("/") < 0)
return path_failure(f_out, "/", True);
am_chrooted = 1;
module_chdir = module_dir;
}
Expand Down
38 changes: 29 additions & 9 deletions exclude.c
Original file line number Diff line number Diff line change
Expand Up @@ -1598,6 +1598,21 @@ void parse_filter_str(filter_rule_list *listp, const char *rulestr,
}
}

static int get_filter_char(FILE *file)
{
int c = getc(file);
while (c == EOF && ferror(file) && errno == EINTR) {
clearerr(file);
/* EINTR leaves the unread byte in place, including on pipes.
* Clang 21's stream model does not recognise this recovery. */
#ifdef __clang_analyzer__
__attribute__((suppress))
#endif
c = getc(file);
}
return c;
}

void parse_filter_file(filter_rule_list *listp, const char *fname, const filter_rule *template, int xflags)
{
FILE *fp;
Expand Down Expand Up @@ -1685,8 +1700,11 @@ void parse_filter_file(filter_rule_list *listp, const char *fname, const filter_

if (fd < 0)
fp = NULL;
else if (!(fp = fdopen(fd, "rb")))
else if (!(fp = fdopen(fd, "rb"))) {
int fdopen_errno = errno;
close(fd);
errno = fdopen_errno;
}
} else
fp = stdin;

Expand Down Expand Up @@ -1762,21 +1780,15 @@ void parse_filter_file(filter_rule_list *listp, const char *fname, const filter_
if (pending != EOF) { /* a CR lookahead we could not push back */
ch = pending;
pending = EOF;
} else if ((ch = getc(fp)) == EOF) {
if (ferror(fp) && errno == EINTR) {
clearerr(fp);
continue;
}
} else if ((ch = get_filter_char(fp)) == EOF) {
break;
}
if (word_split && isspace(ch))
break;
if (eol_nulls? !ch : (ch == '\n' || ch == '\r')) {
if (ch == '\r') { /* CRLF is one line, not two */
int nxt;
while ((nxt = getc(fp)) == EOF
&& ferror(fp) && errno == EINTR)
clearerr(fp);
nxt = get_filter_char(fp);
if (nxt == EOF) {
if (!ferror(fp))
ch = EOF; /* real end of file */
Expand All @@ -1796,6 +1808,14 @@ void parse_filter_file(filter_rule_list *listp, const char *fname, const filter_
else
overflow = 1;
}
if (ferror(fp)) {
if (named_by_file)
rprintf(FERROR, "failed to read filter file %s\n", rule_text(template, src_name));
else
rsyserr(FERROR, errno, "failed to read filter file %s", src_name);
fclose(fp);
exit_cleanup(RERR_FILEIO);
}
if (overflow) {
rprintf(FERROR, "discarding over-long filter: %s\n",
rule_text_len(NULL, line, 0));
Expand Down
60 changes: 43 additions & 17 deletions params.c
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,16 @@ static BOOL (*the_pfunc)(char *, char *);
* Functions...
*/

static int get_char(FILE *file)
{
int c;
if (feof(file) || ferror(file))
return EOF;
while ((c = getc(file)) == EOF && ferror(file) && errno == EINTR)
clearerr(file);
return c;
}

static int EatWhitespace( FILE *InFile )
/* ------------------------------------------------------------------------ **
* Scan past whitespace (see ctype(3C)) and return the first non-whitespace
Expand All @@ -121,7 +131,7 @@ static int EatWhitespace( FILE *InFile )
{
int c;

for( c = getc( InFile ); isspace( c ) && ('\n' != c); c = getc( InFile ) )
for( c = get_char( InFile ); isspace( c ) && ('\n' != c); c = get_char( InFile ) )
;
return( c );
} /* EatWhitespace */
Expand All @@ -146,7 +156,7 @@ static int EatComment( FILE *InFile )
{
int c;

for( c = getc( InFile ); ('\n'!=c) && (EOF!=c) && (c>0); c = getc( InFile ) )
for( c = get_char( InFile ); ('\n'!=c) && (EOF!=c) && (c>0); c = get_char( InFile ) )
;
return( c );
} /* EatComment */
Expand Down Expand Up @@ -239,7 +249,7 @@ static BOOL Section( FILE *InFile, BOOL (*sfunc)(char *) )
return( False );
}
end = ( (i > 0) && (' ' == bufr[i - 1]) ) ? (i - 1) : (i);
c = getc( InFile ); /* Continue with next line. */
c = get_char( InFile ); /* Continue with next line. */
break;

default: /* All else are a valid name chars. */
Expand All @@ -253,12 +263,15 @@ static BOOL Section( FILE *InFile, BOOL (*sfunc)(char *) )
{
bufr[i++] = c;
end = i;
c = getc( InFile );
c = get_char( InFile );
}
}
}

/* We arrive here if we've met the EOF before the closing bracket. */
if (ferror(InFile))
return False;
bufr[end] = '\0';
rprintf(FLOG, "%s Unexpected EOF in the config file: %s\n", func, bufr );
return( False );
} /* Section */
Expand Down Expand Up @@ -326,11 +339,13 @@ static BOOL Parameter( FILE *InFile, BOOL (*pfunc)(char *, char *), int c )
return( True );
}
end = ( (i > 0) && (' ' == bufr[i - 1]) ) ? (i - 1) : (i);
c = getc( InFile ); /* Read past eoln. */
c = get_char( InFile ); /* Read past eoln. */
break;

case '\0': /* Shouldn't have EOF within param name. */
case EOF:
if (ferror(InFile))
return False;
bufr[i] = '\0';
rprintf(FLOG, "%s Unexpected end-of-file at: %s\n", func, bufr );
return( True );
Expand Down Expand Up @@ -359,7 +374,7 @@ static BOOL Parameter( FILE *InFile, BOOL (*pfunc)(char *, char *), int c )
{
bufr[i++] = c;
end = i;
c = getc( InFile );
c = get_char( InFile );
}
}
}
Expand All @@ -377,7 +392,7 @@ static BOOL Parameter( FILE *InFile, BOOL (*pfunc)(char *, char *), int c )
switch( c )
{
case '\r': /* Explicitly remove '\r' because the older */
c = getc( InFile ); /* version called fgets_slash() which also */
c = get_char( InFile ); /* version called fgets_slash() which also */
break; /* removes them. */

case '\n': /* Marks end of value unless there's a '\'. */
Expand All @@ -388,20 +403,22 @@ static BOOL Parameter( FILE *InFile, BOOL (*pfunc)(char *, char *), int c )
{
for( end = i; end >= 0 && isSpace(bufr + end); end-- )
;
c = getc( InFile );
c = get_char( InFile );
}
break;

default: /* All others verbatim. Note that spaces do */
bufr[i++] = c; /* not advance <end>. This allows trimming */
if( !isspace( c ) ) /* of whitespace at the end of the line. */
end = i;
c = getc( InFile );
c = get_char( InFile );
break;
}
}
bufr[end] = '\0'; /* End of value. */

if (ferror(InFile))
return False;
return( pfunc( bufr, &bufr[vstart] ) ); /* Pass name & value to pfunc(). */
} /* Parameter */

Expand Down Expand Up @@ -586,7 +603,11 @@ static FILE *OpenConfFile( char *FileName )
int cfg_fd = open_no_attacker_symlinks( FileName, O_RDONLY, 0 );
OpenedFile = cfg_fd >= 0 ? fdopen( cfg_fd, "r" ) : NULL;
if( !OpenedFile && cfg_fd >= 0 )
{
int fdopen_errno = errno;
close( cfg_fd );
errno = fdopen_errno;
}
}
if( NULL == OpenedFile )
{
Expand Down Expand Up @@ -617,21 +638,26 @@ int pm_process( char *FileName,
{
int result;
FILE *InFile;
BOOL own_buffer = bufr == NULL;
char *func = "params.c:pm_process() -";

InFile = OpenConfFile( FileName ); /* Open the config file. */
if( NULL == InFile )
return( False );

if( NULL != bufr ) /* If we already have a buffer */
result = Parse( InFile, sfunc, pfunc ); /* (recursive call), then just */
/* use it. */

else /* If we don't have a buffer */
{ /* allocate one, then parse, */
bSize = BUFR_INC; /* then free. */
if (own_buffer)
{
bSize = BUFR_INC;
bufr = new_array( char, bSize );
result = Parse( InFile, sfunc, pfunc );
}
result = Parse( InFile, sfunc, pfunc );
if (ferror(InFile))
{
rsyserr(FLOG, errno, "failed to read config file \"%s\"", FileName);
result = 0;
}
if (own_buffer)
{
free( bufr );
bufr = NULL;
bSize = 0;
Expand Down
10 changes: 6 additions & 4 deletions sender.c
Original file line number Diff line number Diff line change
Expand Up @@ -313,11 +313,13 @@ static int sender_open_copylinks_confined(const char *anchor, const char *relpat
if (n < 0) {
/* EINVAL: not a symlink -> the resolved target file. Open it
* O_NOFOLLOW (a raced symlink flip is still refused). */
fd = e == EINVAL
? openat(pdfd, bname, O_RDONLY | O_NOFOLLOW | O_BINARY | extra, 0)
: -1;
if (e == EINVAL) {
fd = openat(pdfd, bname, O_RDONLY | O_NOFOLLOW | O_BINARY | extra, 0);
e = fd < 0 ? errno : 0;
} else
fd = -1;
close(pdfd);
if (n < 0 && e != EINVAL)
if (fd < 0)
errno = e;
return fd;
}
Expand Down
40 changes: 40 additions & 0 deletions testsuite/chroot-cwd.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#define _GNU_SOURCE
#include <dlfcn.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static int jailed, marker = -1;

int chroot(const char *path)
{
int (*original)(const char *) = dlsym(RTLD_NEXT, "chroot");
const char *target = getenv("RSYNC_TEST_CHROOT_PATH");
int selected = target && strcmp(path, target) == 0;
int result;
if (selected)
marker = open(getenv("RSYNC_TEST_CHROOT_MARKER"), O_WRONLY | O_CREAT | O_APPEND, 0600);
result = original(path);
if (selected && result == 0)
jailed = 1;
return result;
}

int chdir(const char *path)
{
int (*original)(const char *) = dlsym(RTLD_NEXT, "chdir");
if (jailed) {
jailed = 0;
if (marker >= 0) {
(void)write(marker, path, strlen(path));
(void)write(marker, "\n", 1);
}
if (getenv("RSYNC_TEST_CHROOT_DENY") && strcmp(path, "/") == 0) {
errno = EACCES;
return -1;
}
}
return original(path);
}
Loading
Loading