Files
Andrew Tridgell 39c6c31bfc vfs: adapt the merged-in base changes to the VFS layer
Linear-rebase counterpart of the conflict resolutions made when the
sec-fixes base was re-integrated (see the merge for reference).  The base
gained 5cb4b829 ("syscall: build without AT_SYMLINK_NOFOLLOW") and its CI
compile-check, which touch code this branch relocated into vfs/:

- Move syscall.c's RSYNC_TEST_NO_AT_FDCWD undef block into vfs/vfs.h,
  before the VFS_AT_FDCWD sentinel binds (so the sentinel takes its
  no-AT_FDCWD value rather than dangling on the undefined AT_FDCWD).
- Port the AT_SYMLINK_NOFOLLOW-absent fallbacks into the relocated code:
  vfs/chown.c (vfs__lchown_secure + held-fd vfs_lchown gate on
  AT_SYMLINK_NOFOLLOW), vfs/stat.c (do_xstat_at's unused-arg casts, the
  vfs_lstat AT_SYMLINK_NOFOLLOW-absent arm, held-fd gate), vfs/mkdir.c
  (guard rand_bytes on AT_FDCWD, its only caller).
- Retarget the CHECK_COMPILE_OBJS compile-check from syscall.c to a
  portable shell loop over the vfs sources built with
  -DRSYNC_TEST_NO_AT_FDCWD (Makefile.in).

Tree is byte-identical to the validated merge result.
2026-08-13 10:20:13 +10:00

324 lines
9.1 KiB
C

/*
* vfs/mkdir.c - mkdir and mkstemp wrappers, plus the trim_trailing_slashes
* path helper and the race-safe vfs_secure_mkstemp / vfs_mkstemp_atfd create loop.
*
* Moved verbatim out of syscall.c.
*
* Copyright (C) 1998-2022 Andrew Tridgell, Martin Pool, Wayne Davison
* Copyright (C) 2026 Wayne Davison, Andrew Tridgell
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*/
#include "rsync.h"
#include "ifuncs.h"
#include "vfs/vfs_internal.h"
/* Fill buf with len random bytes for the mkstemp-style temp-name suffix. Only
* collision avoidance is needed here -- the O_EXCL|O_NOFOLLOW create is the real
* guard against a guessed/pre-planted name -- so an rand() fallback is fine when
* /dev/urandom can't be opened or read (e.g. a chroot/container without /dev).
* We read /dev/urandom directly rather than probing getrandom()/arc4random_buf()
* to match authenticate.c and avoid new configure checks. */
#ifdef AT_FDCWD /* only vfs_mkstemp_atfd's held-dirfd create loop uses this */
static void rand_bytes(unsigned char *buf, size_t len)
{
#ifndef O_CLOEXEC
#define O_CLOEXEC 0
#endif
int fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC);
if (fd >= 0) {
ssize_t n = read(fd, buf, len);
close(fd);
if (n == (ssize_t)len) {
return;
}
}
for (size_t i = 0; i < len; i++) {
buf[i] = (unsigned char)rand();
}
}
#endif
void trim_trailing_slashes(char *name)
{
int l;
/* Some BSD systems cannot make a directory if the name
* contains a trailing slash.
* <http://www.opensource.apple.com/bugs/X/BSD%20Kernel/2734739.html> */
/* Don't change empty string; and also we can't improve on
* "/" */
l = strlen(name);
while (l > 1) {
if (name[--l] != '/')
break;
name[l] = '\0';
}
}
/* Secure receiver-side resolve for a path mkdir. An operator path
* (--backup-dir/--temp-dir, may live outside the tree) uses the ownership walk;
* otherwise the strict transfer-path resolver (refuse all symlinks, confine
* beneath the transfer root). mkdir() resolves parent symlinks at every
* component, so a parent-component swap can place an attacker-named directory
* outside the module -- defence is to resolve the parent securely and mkdirat()
* the leaf. Falls through to a plain mkdir() in non-daemon/sender, chrooted,
* no-parent and absolute-path cases. */
static int vfs__mkdir_secure(char *path, mode_t mode, int flags)
{
#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY
char dirpath[MAXPATHLEN];
const char *bname, *slash;
int dfd, ret, e;
size_t dlen;
if (flags & VFS_OPERATOR_PATH) {
if (vfs_symlink_optout_allowed())
return mkdir(path, mode);
dfd = vfs_owner_walk_parent(path, &bname, 1);
if (dfd < 0)
return -1;
ret = mkdirat(dfd, bname, mode);
e = errno;
close(dfd);
errno = e;
return ret;
}
if (!vfs_relpath_active())
return mkdir(path, mode);
if (!*path || *path == '/')
return mkdir(path, mode);
slash = strrchr(path, '/');
if (!slash)
return mkdir(path, mode);
dlen = slash - path;
if (dlen >= sizeof dirpath) {
errno = ENAMETOOLONG;
return -1;
}
memcpy(dirpath, path, dlen);
dirpath[dlen] = '\0';
bname = slash + 1;
dfd = vfs_resolve_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0);
if (dfd < 0)
return -1;
ret = mkdirat(dfd, bname, mode);
e = errno;
close(dfd);
errno = e;
return ret;
#else
(void)flags;
return mkdir(path, mode);
#endif
}
/* Unified mkdir. dirfd == VFS_AT_FDCWD resolves `path`; a real held dirfd makes
* `path` a single component created directly under it. flags: VFS_ALLOW_SYMLINK
* (trusted, follow symlinks), VFS_OPERATOR_PATH (operator-supplied path),
* default 0 (secure receiver resolve). */
int vfs_mkdir(int dirfd, char *path, mode_t mode, int flags)
{
if (dry_run) return 0;
RETURN_ERROR_IF_RO_OR_LO;
RETURN_ERROR_IF_NULL(path);
if (dirfd != VFS_AT_FDCWD) {
#ifdef AT_FDCWD
/* Held-fd: `path` must be a single harmless component -- reject
* anything that could reintroduce path resolution under the pinned
* dir (empty, any '/', "." or ".."). */
if (!*path || strchr(path, '/')
|| (path[0] == '.' && (path[1] == '\0'
|| (path[1] == '.' && path[2] == '\0')))) {
errno = EINVAL;
return -1;
}
return mkdirat(dirfd, path, mode);
#else
(void)dirfd; (void)mode;
errno = ENOSYS;
return -1;
#endif
}
trim_trailing_slashes(path);
/* VFS_ALLOW_SYMLINK takes precedence: the call site asserts the path is
* trusted, so follow symlinks (the legacy plain mkdir). */
if (flags & VFS_ALLOW_SYMLINK)
return mkdir(path, mode);
return vfs__mkdir_secure(path, mode, flags);
}
/* like mkstemp but forces permissions */
int vfs_mkstemp(char *template, mode_t perms)
{
RETURN_ERROR_IF(dry_run, 0);
RETURN_ERROR_IF(read_only, EROFS);
perms |= S_IWUSR;
#if defined HAVE_SECURE_MKSTEMP && defined HAVE_FCHMOD && (!defined HAVE_OPEN64 || defined HAVE_MKSTEMP64)
{
int fd = mkstemp(template);
if (fd == -1)
return -1;
if (fchmod(fd, perms) != 0 && preserve_perms) {
int errno_save = errno;
close(fd);
unlink(template);
errno = errno_save;
return -1;
}
#if defined HAVE_SETMODE && O_BINARY
setmode(fd, O_BINARY);
#endif
return fd;
}
#else
if (!mktemp(template))
return -1;
return vfs_open(template, O_RDWR|O_EXCL|O_CREAT, perms);
#endif
}
/* Create a unique temp file directly in directory `dfd` for the held-dirfd
* traversal: `filename` is the basename ending in "XXXXXX", rewritten in place
* to the chosen name. O_EXCL|O_NOFOLLOW so a planted name can't be followed or
* clobbered. Does NOT close dfd (the caller owns it). Returns the fd, or -1.
* This is the create loop shared with vfs_secure_mkstemp(). */
int vfs_mkstemp_atfd(int dfd, char *filename, mode_t perms)
{
#ifdef AT_FDCWD
static const char letters[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
size_t filename_len = strlen(filename);
char *suffix;
int fd = -1;
if (filename_len < 6) {
errno = EINVAL;
return -1;
}
suffix = filename + filename_len - 6; /* Points to XXXXXX */
if (strcmp(suffix, "XXXXXX") != 0) {
errno = EINVAL;
return -1;
}
perms |= S_IWUSR;
for (int tries = 0; tries < 100; tries++) {
unsigned char rbytes[6];
rand_bytes(rbytes, sizeof(rbytes));
for (int i = 0; i < 6; i++)
suffix[i] = letters[rbytes[i] % (sizeof(letters) - 1)];
fd = openat(dfd, filename, O_RDWR | O_CREAT | O_EXCL | O_NOFOLLOW, perms);
if (fd >= 0)
break;
if (errno != EEXIST)
return -1;
}
if (fd >= 0) {
if (fchmod(fd, perms) != 0 && preserve_perms) {
int errno_save = errno;
close(fd);
unlinkat(dfd, filename, 0);
errno = errno_save;
return -1;
}
#if defined HAVE_SETMODE && O_BINARY
setmode(fd, O_BINARY);
#endif
}
return fd;
#else
(void)dfd; (void)filename; (void)perms;
errno = ENOSYS;
return -1;
#endif
}
/*
Secure version of mkstemp that prevents symlink attacks on parent directories.
Like vfs_resolve_open(), this walks the path checking each component
with O_NOFOLLOW to prevent TOCTOU race conditions.
The template may be relative or absolute, but must not contain ../ components.
Returns fd on success, -1 on error.
*/
int vfs_secure_mkstemp(char *template, mode_t perms, int operator_path)
{
#if !defined(O_NOFOLLOW) || !defined(O_DIRECTORY) || !defined(AT_FDCWD)
/* Fall back to regular mkstemp on old systems */
return vfs_mkstemp(template, perms);
#else
char *lastslash;
int dirfd = AT_FDCWD;
int fd = -1;
if (!template) {
errno = EINVAL;
return -1;
}
if (strncmp(template, "../", 3) == 0 || strstr(template, "/../")) {
errno = EINVAL;
return -1;
}
/* An operator-supplied --temp-dir may point outside the tree; --insecure-links
* (or a daemon module's "insecure links =") restores legacy following. */
if (operator_path && vfs_symlink_optout_allowed())
return vfs_mkstemp(template, perms);
/* Open the temp file's directory. For an operator --temp-dir use the
* ownership walk (follow a uid0/euid-owned symlink, refuse a foreign one,
* absolute and relative alike); otherwise -- the deep-entry-dir fallback when
* the held-dirfd cache declines -- use the strict transfer-path resolver
* (refuse all symlinks, confine beneath the transfer root). The temp file
* itself is created below with O_EXCL|O_NOFOLLOW, so a planted name can't be
* followed either way. */
lastslash = strrchr(template, '/');
if (lastslash) {
char dirbuf[MAXPATHLEN];
size_t dlen = lastslash - template;
const char *dir;
if (dlen == 0)
dir = "/";
else {
if (dlen >= sizeof dirbuf) {
errno = ENAMETOOLONG;
return -1;
}
memcpy(dirbuf, template, dlen);
dirbuf[dlen] = '\0';
dir = dirbuf;
}
dirfd = operator_path
? vfs_open_owner_walk(dir, O_RDONLY | O_DIRECTORY, 0, 0)
: vfs_resolve_open(dir, ".", O_RDONLY | O_DIRECTORY, 0);
if (dirfd < 0)
return -1;
}
/* Create the temp file in the securely-opened directory. */
{
char *filename = lastslash ? lastslash + 1 : template;
int e;
fd = vfs_mkstemp_atfd(dirfd, filename, perms);
e = errno;
if (dirfd != AT_FDCWD) close(dirfd);
errno = e;
}
return fd;
#endif
}