Files
rsync/t_symlink_secure.c
T
Andrew Tridgell c549abd02f testsuite: fix the two macOS cells the 3.5.0 fleet run flagged
Running the 3.5.0 suite against this branch across the fleet left four
not-OK cells, all macOS, identical on both backport branches.  Both causes
are here, not in the code under test.

t_symlink_secure.c lives at the tree ROOT, so fleettest's overlay -- which
replaces runtests.py and testsuite/ -- does not update it.  This branch was
still running a version predating the HAVE_MKNODAT guard, so on a platform
without mknodat() (macOS 10.13) it asserted a confinement the build
deliberately compiles out and reported "basename symlink was followed ->
module escape".  Alarming and wrong: there is no escape, the helper was just
older than the test driving it.  Taking the current helper makes it skip
there, which is what the fleet config already expects for that target.

partial-protected-regular-retry-policy exercises the receiver's EACCES
recovery path.  That path is 3.5.0-only -- the chmod-on-denied-open work is
not backported, which is also why partial_nowrite is already excluded here --
so the test cannot reach what it checks and reports "inconclusive" rather
than skipping.  Excluded for the same reason as its sibling.

Verified on the x86-64 macOS box: the policy test is excluded and
symlink-mknod-fakesuper-symlink-race now skips, overall result 0.  A 3.5.0
build on the same hardware passes both, so neither is a platform limitation.
2026-08-07 12:34:21 +10:00

161 lines
4.8 KiB
C

/*
* Test harness for the fake-super branches of do_symlink_at()/do_mknod_at().
* Fake-super stores a symlink/device as a placeholder file, so the create
* resolves the final component; the no-slash branch used to fall back to
* do_symlink()/do_mknod(), whose plain open() followed a planted basename
* symlink and escaped the module. Checks the fixed wrappers refuse it;
* --poc shows the old fallback escaping. Not linked into rsync. GPL version 2.
*/
#include "rsync.h"
#include <sys/stat.h>
/* The symlink placeholder (and thus this escape) exists only where symlink
* xattrs are unavailable -- the same guard do_symlink() uses. Elsewhere
* symlink() fails EEXIST on a planted link, so only the device path applies. */
#if defined SUPPORT_LINKS && (defined NO_SYMLINK_XATTRS || defined NO_SYMLINK_USER_XATTRS)
#define TEST_SYMLINK_PLACEHOLDER 1
#endif
int dry_run = 0;
int am_root = -1; /* --fake-super */
int am_sender = 0;
int read_only = 0;
int list_only = 0;
int copy_links = 0;
int copy_unsafe_links = 0;
extern int am_daemon, am_chrooted;
short info_levels[COUNT_INFO], debug_levels[COUNT_DEBUG];
static int errs = 0;
static void check_preserved(const char *label, const char *victim, const char *want)
{
char buf[256];
int fd = open(victim, O_RDONLY);
ssize_t n = fd >= 0 ? read(fd, buf, sizeof buf - 1) : -1;
if (fd >= 0)
close(fd);
if (n < 0)
n = 0;
buf[n] = '\0';
if (n > 0 && buf[n-1] == '\n')
buf[n-1] = '\0';
if (strcmp(buf, want) != 0) {
fprintf(stderr, "FAIL [%s]: victim %s = \"%s\", expected \"%s\" "
"(basename symlink was followed -> module escape)\n",
label, victim, buf, want);
errs++;
return;
}
fprintf(stderr, "OK [%s]: victim %s preserved\n", label, victim);
}
static void check_clobbered(const char *label, const char *victim, const char *unwanted)
{
char buf[256];
int fd = open(victim, O_RDONLY);
ssize_t n = fd >= 0 ? read(fd, buf, sizeof buf - 1) : -1;
if (fd >= 0)
close(fd);
if (n < 0)
n = 0;
buf[n] = '\0';
if (n > 0 && buf[n-1] == '\n')
buf[n-1] = '\0';
if (strcmp(buf, unwanted) != 0) {
fprintf(stderr, "FAIL [%s]: victim %s = \"%s\", expected the escape to write \"%s\"\n",
label, victim, buf, unwanted);
errs++;
return;
}
fprintf(stderr, "OK [%s]: victim %s clobbered as expected (escape demonstrated)\n",
label, victim);
}
int main(int argc, char **argv)
{
#ifndef AT_FDCWD
fprintf(stderr, "SKIP: AT_FDCWD not available\n");
return 77;
#else
int poc = 0;
const char *moddir;
# if !defined(HAVE_MKNODAT) && !defined(TEST_SYMLINK_PLACEHOLDER)
/* Nothing left to assert: the do_mknod_at() checks need mknodat(), and
* the do_symlink_at() ones are not compiled here. Skip rather than
* pass vacuously. */
(void)argc; (void)argv;
fprintf(stderr, "SKIP: no mknodat() and no symlink placeholders -- "
"nothing this helper asserts applies to this build\n");
return 77;
# endif
if (argc == 3 && strcmp(argv[1], "--poc") == 0) {
poc = 1;
moddir = argv[2];
} else if (argc == 2) {
moddir = argv[1];
} else {
fprintf(stderr, "usage: %s [--poc] <module-dir>\n", argv[0]);
return 2;
}
if (chdir(moddir) < 0) {
perror("chdir");
return 2;
}
am_daemon = 1;
am_chrooted = 0;
am_root = -1; /* fake-super: symlinks/devices stored as files */
if (poc) {
/* Pre-fix fallback: a no-slash path went to do_symlink()/do_mknod(),
* which open() the basename without O_NOFOLLOW. */
#ifdef TEST_SYMLINK_PLACEHOLDER
do_symlink("VULN_SYM_PAYLOAD", "sympath");
check_clobbered("poc do_symlink bare", "../outside/secret_sym",
"VULN_SYM_PAYLOAD");
#endif
do_mknod("nodpath", S_IFCHR | 0600, 0);
check_clobbered("poc do_mknod bare", "../outside/secret_nod", "");
return errs ? 1 : 0;
}
/* Fixed wrappers: a bare-path basename symlink must not be followed;
* the victim outside the module stays untouched. */
#ifdef TEST_SYMLINK_PLACEHOLDER
do_symlink_at("FIXED_SYM_PAYLOAD", "sympath");
check_preserved("do_symlink_at bare", "../outside/secret_sym", "VICTIM_SYM");
/* Slashed path for parity (already protected before the fix). */
do_symlink_at("FIXED_SYM_PAYLOAD", "sub/sympath2");
check_preserved("do_symlink_at slashed", "../outside/secret_sym2", "VICTIM_SYM2");
#endif
# ifdef HAVE_MKNODAT
/* Without mknodat() do_mknod_at() IS do_mknod(): the confinement is
* compiled out by design (SECURITY.md), so these would assert a
* property the build deliberately does not have. The do_symlink_at()
* checks above do not depend on it and still run. */
do_mknod_at("nodpath", S_IFCHR | 0600, 0);
check_preserved("do_mknod_at bare", "../outside/secret_nod", "VICTIM_NOD");
do_mknod_at("sub/nodpath2", S_IFCHR | 0600, 0);
check_preserved("do_mknod_at slashed", "../outside/secret_nod2", "VICTIM_NOD2");
# endif
if (errs)
fprintf(stderr, "%d failure(s)\n", errs);
return errs ? 1 : 0;
#endif
}