Files
Andrew Tridgell b61342e4c9 vfs: docs pass for the unified dirfd+flags API
Rewrite the vfs.h header contract to describe the current model: the two-layer
vfs/ structure (primitives + compounds), the single vfs_<op>(int dirfd, ...,
int flags) form, the meaning of VFS_AT_FDCWD / VFS_ALLOW_SYMLINK /
VFS_OPERATOR_PATH / VFS_REMOVEDIR, that the operator policy is an explicit
per-call flag (not ambient state), and which ops keep explicit forms
(rename/link two-path, open variants, fd-based fstat/fileio).

Update the stale threat-model cross-references that pointed at the old
vfs_chmod_at() (now the static vfs__chmod_secure()) and the two
vfs_mknod_at()/vfs_symlink_at() mentions left by the unification.
2026-08-13 10:20:13 +10:00

206 lines
4.9 KiB
C

/*
* vfs/open.c - open wrappers (path, parent-resolved, held-dirfd, nofollow).
*
* 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"
int vfs_open(const char *pathname, int flags, mode_t mode)
{
RETURN_ERROR_IF_NULL(pathname);
if (flags != O_RDONLY) {
RETURN_ERROR_IF(dry_run, 0);
RETURN_ERROR_IF_RO_OR_LO;
}
#ifdef O_NOATIME
if (open_noatime)
flags |= O_NOATIME;
#endif
return open(pathname, flags | O_BINARY, mode);
}
/*
Symlink-race-safe variant of vfs_open() for receiver-side use. See
the comment on vfs__chmod_secure() for the threat model. open() resolves
parent components, so a parent-symlink swap can redirect the open
to a file outside the module. This wrapper is defence-in-depth for
bare-path vfs_open() sites that callers know are otherwise
protected by secure parent-syscalls (e.g. generator.c's in-place
backup creation, where robust_unlink() rejects the symlinked
parent before this open is reached): if any of those upstream
protections is later removed or regresses, the open here still
refuses to escape the module.
Defence: open the parent of pathname under vfs_resolve_open()
and call openat() against the resulting dirfd with O_NOFOLLOW
(so the basename itself isn't followed if it happens to be a
pre-planted symlink, which is what we want for O_CREAT|O_EXCL).
*/
int vfs_open_at(const char *pathname, int flags, mode_t mode, int vfs_flags)
{
#ifdef AT_FDCWD
char dirpath[MAXPATHLEN];
const char *bname;
const char *slash;
int dfd, ret, e;
size_t dlen;
if (flags != O_RDONLY) {
RETURN_ERROR_IF(dry_run, 0);
RETURN_ERROR_IF_RO_OR_LO;
}
#if defined O_NOFOLLOW && defined O_DIRECTORY
if (vfs_flags & VFS_OPERATOR_PATH) {
if (vfs_symlink_optout_allowed())
return vfs_open(pathname, flags, mode);
dfd = vfs_owner_walk_parent(pathname, &bname, 1);
if (dfd < 0)
return -1;
ret = openat(dfd, bname, flags | O_NOFOLLOW, mode);
e = errno;
close(dfd);
errno = e;
return ret;
}
#endif
if (!vfs_relpath_active())
return vfs_open(pathname, flags, mode);
if (!pathname || !*pathname || *pathname == '/')
return vfs_open(pathname, flags, mode);
slash = strrchr(pathname, '/');
if (!slash)
return vfs_open(pathname, flags, mode);
dlen = slash - pathname;
if (dlen >= sizeof dirpath) {
errno = ENAMETOOLONG;
return -1;
}
memcpy(dirpath, pathname, dlen);
dirpath[dlen] = '\0';
bname = slash + 1;
dfd = vfs_resolve_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0);
if (dfd < 0)
return -1;
#ifdef O_NOATIME
if (open_noatime)
flags |= O_NOATIME;
#endif
ret = openat(dfd, bname, flags | O_NOFOLLOW | O_BINARY, mode);
e = errno;
close(dfd);
errno = e;
return ret;
#else
return vfs_open(pathname, flags, mode);
#endif
}
int vfs_open_nofollow(const char *pathname, int flags)
{
#ifndef O_NOFOLLOW
STRUCT_STAT f_st, l_st;
#endif
int fd;
if (flags != O_RDONLY) {
RETURN_ERROR_IF(dry_run, 0);
RETURN_ERROR_IF_RO_OR_LO;
#ifndef O_NOFOLLOW
/* This function doesn't support write attempts w/o O_NOFOLLOW. */
errno = EINVAL;
return -1;
#endif
}
#ifdef O_NOATIME
if (open_noatime)
flags |= O_NOATIME;
#endif
#ifdef O_NOFOLLOW
fd = open(pathname, flags|O_NOFOLLOW);
#else
if (vfs_lstat(VFS_AT_FDCWD, pathname, &l_st, VFS_ALLOW_SYMLINK) < 0)
return -1;
if (S_ISLNK(l_st.st_mode)) {
errno = ELOOP;
return -1;
}
if ((fd = open(pathname, flags)) < 0)
return fd;
if (vfs_fstat(fd, &f_st) < 0) {
close_and_return_error:
{
int save_errno = errno;
close(fd);
errno = save_errno;
}
return -1;
}
if (l_st.st_dev != f_st.st_dev || l_st.st_ino != f_st.st_ino) {
errno = EINVAL;
goto close_and_return_error;
}
#endif
return fd;
}
/*
varient of vfs_open/vfs_open_nofollow which does vfs_open() if the
copy_links or copy_unsafe_links options are set and does
vfs_open_nofollow() otherwise
This is used to prevent a race condition where an attacker could be
switching a file between being a symlink and being a normal file
The open is always done with O_RDONLY flags
*/
int vfs_open_checklinks(const char *pathname)
{
if (copy_links || copy_unsafe_links) {
return vfs_open(pathname, O_RDONLY, 0);
}
return vfs_open_nofollow(pathname, O_RDONLY);
}
int vfs_open_atfd(int dfd, const char *name, int flags, mode_t mode)
{
#ifdef AT_FDCWD
if (flags != O_RDONLY) {
RETURN_ERROR_IF(dry_run, 0);
RETURN_ERROR_IF_RO_OR_LO;
}
#ifdef O_NOATIME
if (open_noatime)
flags |= O_NOATIME;
#endif
return openat(dfd, name, flags | O_NOFOLLOW | O_BINARY, mode);
#else
(void)dfd; (void)name; (void)flags; (void)mode;
errno = ENOSYS;
return -1;
#endif
}