Files
rsync/t_symlink_secure.c
T
Andrew Tridgell 0b7ee36157 build: add t_rename_secure and t_symlink_secure check helpers
The secure-rename and secure-symlink/mknod regression tests drive these
unit helpers (which exercise do_rename_at()/do_symlink_at()/do_mknod_at()
directly), mirroring the existing t_secure_relpath/t_chmod_secure helpers.
Built by `make check-progs`.

Enables: rename-mixed-parent-symlink-race, rename-mixed-parent-escape-poc,
symlink-mknod-fakesuper-symlink-race.
2026-06-15 12:21:57 +10:00

145 lines
4.1 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 (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
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");
if (errs)
fprintf(stderr, "%d failure(s)\n", errs);
return errs ? 1 : 0;
#endif
}