mirror of
https://github.com/RsyncProject/rsync.git
synced 2026-05-11 16:34:25 -04:00
syscall: use openat2(RESOLVE_BENEATH) on Linux for secure_relative_open
The CVE fix in commit c35e283 made secure_relative_open() walk every
component of relpath with O_NOFOLLOW. That blocks every symlink in the
path, which is stricter than the threat model required: legitimate
directory symlinks within the destination tree (e.g. when using -K /
--copy-dirlinks) are also rejected, breaking delta transfers with
"failed verification -- update discarded". See issue #715.
On Linux 5.6+, openat2(RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS) gives
us exactly what we want: the kernel rejects any resolution that would
escape the starting directory (via "..", absolute paths, or symlinks
pointing outside dirfd) while still following symlinks that resolve
within it. /proc magic-links are blocked too.
Use openat2 first; fall back to the existing per-component O_NOFOLLOW
walk on ENOSYS (kernel < 5.6). The lexical "../" checks at the head
of the function are kept as defense in depth. The Linux gate is
plain #ifdef __linux__: the runtime ENOSYS fallback covers the only
case that actually matters (header present + old kernel), and any
Linux build environment without linux/openat2.h will fail with a
clear "no such file" error rather than silently disabling the
protection.
Verified manually that openat2(RESOLVE_BENEATH) blocks all four
escape patterns (absolute symlink, ../ symlink, lexical .., absolute
path) while allowing direct and within-tree symlinks. The new
testsuite/symlink-dirlink-basis.test (taken from PR #864 by Samuel
Henrique) exercises the issue #715 regression and passes; full
make check passes 47/47.
Test: testsuite/symlink-dirlink-basis.test (8 scenarios)
Fixes: https://github.com/RsyncProject/rsync/issues/715
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
60
syscall.c
60
syscall.c
@@ -33,6 +33,11 @@
|
||||
#include <sys/syscall.h>
|
||||
#endif
|
||||
|
||||
#ifdef __linux__
|
||||
#include <sys/syscall.h>
|
||||
#include <linux/openat2.h>
|
||||
#endif
|
||||
|
||||
#include "ifuncs.h"
|
||||
|
||||
extern int dry_run;
|
||||
@@ -720,12 +725,49 @@ int do_open_nofollow(const char *pathname, int flags)
|
||||
/*
|
||||
open a file relative to a base directory. The basedir can be NULL,
|
||||
in which case the current working directory is used. The relpath
|
||||
must be a relative path, and the relpath must not contain any
|
||||
elements in the path which follow symlinks (ie. like O_NOFOLLOW, but
|
||||
applies to all path components, not just the last component)
|
||||
must be a relative path. The kernel must guarantee that resolution
|
||||
cannot escape basedir (or the cwd, when basedir is NULL): no ".."
|
||||
jumps above the start, no symlinks pointing outside, no absolute
|
||||
paths, no /proc magic-link tricks.
|
||||
|
||||
The relpath must also not contain any ../ elements in the path
|
||||
Symlinks *within* basedir are followed normally — earlier rsync
|
||||
versions rejected every symlink with O_NOFOLLOW on each component,
|
||||
which broke legitimate directory symlinks on the receiver side
|
||||
(https://github.com/RsyncProject/rsync/issues/715). The escape
|
||||
prevention is handled by the kernel via openat2(RESOLVE_BENEATH)
|
||||
on Linux 5.6+; older systems fall back to the per-component
|
||||
O_NOFOLLOW walk below.
|
||||
|
||||
The relpath must also not contain any ../ elements in the path.
|
||||
*/
|
||||
|
||||
#ifdef __linux__
|
||||
static int secure_relative_open_linux(const char *basedir, const char *relpath, int flags, mode_t mode)
|
||||
{
|
||||
struct open_how how;
|
||||
int dirfd, retfd;
|
||||
|
||||
memset(&how, 0, sizeof how);
|
||||
how.flags = flags;
|
||||
how.mode = mode;
|
||||
how.resolve = RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS;
|
||||
|
||||
if (basedir == NULL) {
|
||||
dirfd = AT_FDCWD;
|
||||
} else {
|
||||
dirfd = openat(AT_FDCWD, basedir, O_RDONLY | O_DIRECTORY);
|
||||
if (dirfd == -1)
|
||||
return -1;
|
||||
}
|
||||
|
||||
retfd = syscall(SYS_openat2, dirfd, relpath, &how, sizeof how);
|
||||
|
||||
if (dirfd != AT_FDCWD)
|
||||
close(dirfd);
|
||||
return retfd;
|
||||
}
|
||||
#endif
|
||||
|
||||
int secure_relative_open(const char *basedir, const char *relpath, int flags, mode_t mode)
|
||||
{
|
||||
if (!relpath || relpath[0] == '/') {
|
||||
@@ -739,6 +781,16 @@ int secure_relative_open(const char *basedir, const char *relpath, int flags, mo
|
||||
return -1;
|
||||
}
|
||||
|
||||
#ifdef __linux__
|
||||
{
|
||||
int fd = secure_relative_open_linux(basedir, relpath, flags, mode);
|
||||
/* ENOSYS = kernel < 5.6 doesn't have the syscall even though
|
||||
* glibc/kernel-headers do; fall through to the portable path. */
|
||||
if (fd != -1 || errno != ENOSYS)
|
||||
return fd;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !defined(O_NOFOLLOW) || !defined(O_DIRECTORY) || !defined(AT_FDCWD)
|
||||
// really old system, all we can do is live with the risks
|
||||
if (!basedir) {
|
||||
|
||||
Reference in New Issue
Block a user