mirror of
https://github.com/RsyncProject/rsync.git
synced 2026-05-24 23:05:52 -04:00
t_chmod_secure: probe kernel RESOLVE_BENEATH at runtime; drop test skip
The chmod-symlink-race test was previously a no-op on Solaris,
OpenBSD, NetBSD, and Cygwin via a case 'uname -s' skip. The skip
was too broad: of the four scenarios the helper exercises, only
the 'legitimate within-tree dir-symlink' one actually needs
RESOLVE_BENEATH-equivalent kernel support. The other three
(attack rejection, plain relative path, top-level file) behave
identically on the per-component O_NOFOLLOW fallback and would
have caught the t_stub.c max_alloc=0 bug fixed in the previous
commit if the test had been allowed to run.
Make the helper probe the running kernel for either
openat2(RESOLVE_BENEATH) on Linux 5.6+ or openat(O_RESOLVE_BENEATH)
on FreeBSD 13+ / macOS 15+ by opening '.' under the requested
confinement. Honour the result:
- If RESOLVE_BENEATH-equivalent confinement is available, the
within-tree symlink scenario must succeed (status quo).
- If not, the per-component O_NOFOLLOW fallback rejects every
symlink including legitimate ones; expect the within-tree
symlink scenario to be rejected (rc != 0) and the file mode
to remain unchanged.
The attack-rejection, plain-path and top-level scenarios are
unchanged: they expect the same outcome on both code paths.
Drop the case-based skip from chmod-symlink-race.test so the test
runs everywhere and the per-component fallback gets the CI
coverage that the SunOS/OpenBSD/NetBSD/Cygwin runners can
provide. HPE NonStop -- which lacks RESOLVE_BENEATH but isn't in
the existing skip list -- is also covered by this change.
This commit is contained in:
committed by
Andrew Tridgell
parent
cfdc27c613
commit
e1c5f0e93a
2
.github/workflows/cygwin-build.yml
vendored
2
.github/workflows/cygwin-build.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
- name: info
|
||||
run: bash -c '/usr/local/bin/rsync --version'
|
||||
- name: check
|
||||
run: bash -c 'RSYNC_EXPECT_SKIPPED=acls-default,acls,bare-do-open-symlink-race,chdir-symlink-race,chmod-symlink-race,chown,daemon-chroot-acl,devices,dir-sgid,open-noatime,protected-regular,sender-flist-symlink-leak,simd-checksum,symlink-dirlink-basis make check'
|
||||
run: bash -c 'RSYNC_EXPECT_SKIPPED=acls-default,acls,bare-do-open-symlink-race,chdir-symlink-race,chown,daemon-chroot-acl,devices,dir-sgid,open-noatime,protected-regular,sender-flist-symlink-leak,simd-checksum,symlink-dirlink-basis make check'
|
||||
- name: ssl file list
|
||||
run: bash -c 'PATH="/usr/local/bin:$PATH" rsync-ssl --no-motd download.samba.org::rsyncftp/ || true'
|
||||
- name: save artifact
|
||||
|
||||
@@ -17,6 +17,11 @@
|
||||
|
||||
#include <sys/stat.h>
|
||||
|
||||
#ifdef __linux__
|
||||
#include <sys/syscall.h>
|
||||
#include <linux/openat2.h>
|
||||
#endif
|
||||
|
||||
int dry_run = 0;
|
||||
int am_root = 0;
|
||||
int am_sender = 0;
|
||||
@@ -30,6 +35,42 @@ short info_levels[COUNT_INFO], debug_levels[COUNT_DEBUG];
|
||||
|
||||
static int errs = 0;
|
||||
|
||||
/* Probe the running kernel for the RESOLVE_BENEATH-equivalent confinement
|
||||
* that secure_relative_open() prefers over the per-component O_NOFOLLOW
|
||||
* walk. Returns 1 if either openat2(RESOLVE_BENEATH) on Linux 5.6+ or
|
||||
* openat(O_RESOLVE_BENEATH) on FreeBSD 13+ / macOS 15+ is honoured by
|
||||
* the running kernel, 0 otherwise. The probe opens "." (a directory
|
||||
* the helper has just chdir'd into) so it can't fail for any reason
|
||||
* other than the kernel rejecting the requested confinement flag. */
|
||||
static int kernel_resolve_beneath_supported(void)
|
||||
{
|
||||
int fd;
|
||||
#ifdef __linux__
|
||||
{
|
||||
struct open_how how;
|
||||
memset(&how, 0, sizeof how);
|
||||
how.flags = O_RDONLY | O_DIRECTORY;
|
||||
how.resolve = RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS;
|
||||
fd = syscall(SYS_openat2, AT_FDCWD, ".", &how, sizeof how);
|
||||
if (fd >= 0) {
|
||||
close(fd);
|
||||
return 1;
|
||||
}
|
||||
/* ENOSYS = kernel < 5.6. Fall through to the O_RESOLVE_BENEATH
|
||||
* probe in case we're a Linux build running on a kernel that
|
||||
* gained O_RESOLVE_BENEATH via some out-of-tree backport. */
|
||||
}
|
||||
#endif
|
||||
#ifdef O_RESOLVE_BENEATH
|
||||
fd = openat(AT_FDCWD, ".", O_RDONLY | O_DIRECTORY | O_RESOLVE_BENEATH);
|
||||
if (fd >= 0) {
|
||||
close(fd);
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void check(const char *label, int actual_rc, int expect_ok,
|
||||
const char *path, mode_t expected_mode)
|
||||
{
|
||||
@@ -87,10 +128,35 @@ int main(int argc, char **argv)
|
||||
* files to mode 0600 so we have a clean baseline to compare.
|
||||
*/
|
||||
|
||||
/* Scenario A: legitimate parent dir-symlink, chmod must succeed. */
|
||||
/* Scenario A: legitimate parent dir-symlink.
|
||||
*
|
||||
* On platforms whose kernel offers RESOLVE_BENEATH-equivalent
|
||||
* confinement (Linux 5.6+ openat2, FreeBSD 13+ / macOS 15+
|
||||
* O_RESOLVE_BENEATH), the within-tree symlink is followed and
|
||||
* the chmod must succeed.
|
||||
*
|
||||
* On platforms that fall back to the per-component O_NOFOLLOW
|
||||
* walk (OpenBSD, NetBSD, Solaris, older Cygwin, HPE NonStop,
|
||||
* and pre-5.6 Linux), every symlink is rejected -- including
|
||||
* this legitimate one. That's a real platform limitation (the
|
||||
* same one that causes the #715 regression there) and the
|
||||
* expected outcome is rejection.
|
||||
*
|
||||
* Detect at runtime and expect accordingly. The other three
|
||||
* scenarios behave identically on both code paths and need no
|
||||
* adjustment. */
|
||||
int kernel_has_rb = kernel_resolve_beneath_supported();
|
||||
fprintf(stderr, "INFO: kernel RESOLVE_BENEATH-equivalent confinement: %s\n",
|
||||
kernel_has_rb ? "available" : "not available (per-component fallback)");
|
||||
|
||||
int rc = do_chmod_at("inside_link/sentinel", 0640);
|
||||
check("A: legit dir-symlink within tree",
|
||||
rc, 1, "realdir/sentinel", 0640);
|
||||
if (kernel_has_rb) {
|
||||
check("A: legit dir-symlink within tree (kernel confined)",
|
||||
rc, 1, "realdir/sentinel", 0640);
|
||||
} else {
|
||||
check("A: legit dir-symlink within tree (per-component fallback rejects)",
|
||||
rc, 0, "realdir/sentinel", 0600);
|
||||
}
|
||||
|
||||
/* Scenario B: parent symlink escapes the tree -- chmod must be
|
||||
* rejected and the outside file's mode must be unchanged. */
|
||||
|
||||
@@ -14,30 +14,30 @@
|
||||
# receiver's check and its act, and the syscall escapes the module.
|
||||
#
|
||||
# This test exercises the new do_chmod_at() wrapper via the
|
||||
# t_chmod_secure helper. The helper sets up two scenarios:
|
||||
# t_chmod_secure helper. The helper sets up four scenarios:
|
||||
# - a parent dir-symlink that resolves WITHIN the module tree
|
||||
# (legitimate -K-style use, must continue to work)
|
||||
# (legitimate -K-style use)
|
||||
# - a parent dir-symlink that escapes the module tree (the
|
||||
# attack, must be rejected)
|
||||
# plus two regression scenarios (plain relative path, top-level
|
||||
# file) that just confirm the safe wrapper doesn't break the
|
||||
# normal case.
|
||||
# attack, must be rejected on every platform)
|
||||
# - plain relative path (regression check)
|
||||
# - top-level file with no parent component (regression check)
|
||||
#
|
||||
# The kernel-enforced "stay below dirfd" path resolution is
|
||||
# only available on Linux 5.6+, FreeBSD 13+, and macOS 15+.
|
||||
# Skip on platforms that fall back to per-component O_NOFOLLOW
|
||||
# (Solaris, OpenBSD, NetBSD, Cygwin); the per-component fallback
|
||||
# would also reject the attack but the legitimate dir-symlink
|
||||
# scenario would fail there.
|
||||
# Kernel-enforced "stay below dirfd" path resolution is available
|
||||
# on Linux 5.6+, FreeBSD 13+, and macOS 15+. On those platforms
|
||||
# the legitimate within-tree symlink must be followed and the
|
||||
# chmod must succeed. On platforms that fall back to the
|
||||
# per-component O_NOFOLLOW walk (Solaris, OpenBSD, NetBSD,
|
||||
# older Cygwin, HPE NonStop, pre-5.6 Linux), every symlink --
|
||||
# including the legitimate one -- is rejected; that's a real
|
||||
# platform limitation, not a security regression. The helper
|
||||
# probes the running kernel at startup and adjusts the expected
|
||||
# outcome for the within-tree-symlink scenario accordingly, so
|
||||
# this test runs everywhere and gives the per-component fallback
|
||||
# real CI coverage (the attack-rejection, plain-path, and
|
||||
# top-level scenarios all behave identically on both code paths).
|
||||
|
||||
. "$suitedir/rsync.fns"
|
||||
|
||||
case "$(uname -s)" in
|
||||
SunOS|OpenBSD|NetBSD|CYGWIN*)
|
||||
test_skipped "do_chmod_at relies on RESOLVE_BENEATH-equivalent kernel support not available on $(uname -s)"
|
||||
;;
|
||||
esac
|
||||
|
||||
mod="$scratchdir/module"
|
||||
trap_outside="$scratchdir/trap"
|
||||
rm -rf "$mod" "$trap_outside"
|
||||
|
||||
Reference in New Issue
Block a user