Files
Andrew Tridgell 359baac8eb vfs: fail loud in the held-fd lstat no-AT_SYMLINK_NOFOLLOW arm; make the compile-check atomic
Two fixes from codex review of the no-AT_FDCWD port:

- vfs/stat.c: the held-dirfd vfs_lstat branch fell back to
  fstatat(dirfd, path, st, 0) when AT_SYMLINK_NOFOLLOW is unavailable,
  which FOLLOWS the leaf and breaks lstat's no-follow contract.  On a
  system with SUPPORT_LINKS but no AT_SYMLINK_NOFOLLOW, return ENOSYS
  instead (mirroring the held-fd vfs_lchown arm) so a symlink-sensitive
  caller fails loud rather than silently following; the !SUPPORT_LINKS
  arm keeps fstatat(...,0) since there is nothing to follow.  The CI
  compile-check config also undefines AT_FDCWD so no held fd is produced
  there; this hardens the standalone "no AT_SYMLINK_NOFOLLOW" shape.

- Makefile.in: the vfs-no-at-fdcwd.o compile loop wrote every object to
  $@, so a mid-loop failure left a fresh-timestamped $@ and a retry could
  skip the check.  Compile to $@.tmp and mv to $@ only after the whole
  loop succeeds (rm the stale target up front); clean the .tmp too.
2026-08-13 10:20:13 +10:00

190 lines
5.3 KiB
C

/*
* vfs/stat.c - stat/lstat/fstat wrappers (path, parent-resolved, held-dirfd).
*
* 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"
static int vfs__stat_plain(const char *path, STRUCT_STAT *st)
{
RETURN_ERROR_IF_NULL(path);
#ifdef USE_STAT64_FUNCS
return stat64(path, st);
#else
return stat(path, st);
#endif
}
static int vfs__lstat_plain(const char *path, STRUCT_STAT *st)
{
RETURN_ERROR_IF_NULL(path);
#ifdef SUPPORT_LINKS
# ifdef USE_STAT64_FUNCS
return lstat64(path, st);
# else
return lstat(path, st);
# endif
#else
return vfs__stat_plain(path, st);
#endif
}
/*
Symlink-race-safe variants of vfs_stat() / vfs_lstat() for receiver-
side use. See the comment on vfs__chmod_secure() for the threat model.
stat() and lstat() resolve parent components, so a parent-symlink
swap can make the receiver's stat see attributes of a victim file
outside the module -- which then drives later behaviour (e.g.
"this isn't a directory, delete it" -> attacker-controlled unlink
on something outside the module).
Defence: open the parent under vfs_resolve_open() and use
fstatat() with AT_SYMLINK_NOFOLLOW (lstat) or 0 (stat) against
that dirfd. Same fall-through gating as the other wrappers.
*/
static int do_xstat_at(const char *path, STRUCT_STAT *st, int at_flags, int (*fallback)(const char *, STRUCT_STAT *), int vfs_flags)
{
#ifdef AT_FDCWD
char dirpath[MAXPATHLEN];
const char *bname;
const char *slash;
int dfd, ret, e;
size_t dlen;
#if defined O_NOFOLLOW && defined O_DIRECTORY
if (vfs_flags & VFS_OPERATOR_PATH) {
if (vfs_symlink_optout_allowed())
return fallback(path, st);
dfd = vfs_owner_walk_parent(path, &bname, 1);
if (dfd < 0)
return -1;
ret = fstatat(dfd, bname, st, at_flags);
e = errno;
close(dfd);
errno = e;
return ret;
}
#endif
if (!vfs_relpath_active())
return fallback(path, st);
if (!path || !*path || *path == '/')
return fallback(path, st);
slash = strrchr(path, '/');
if (!slash)
return fallback(path, st);
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 = fstatat(dfd, bname, st, at_flags);
e = errno;
close(dfd);
errno = e;
return ret;
#else
(void)at_flags; (void)vfs_flags;
return fallback(path, st);
#endif
}
/* Unified stat/lstat. dirfd == VFS_AT_FDCWD resolves `path` (VFS_ALLOW_SYMLINK =
* plain libc stat/lstat, default 0 = secure receiver resolve, VFS_OPERATOR_PATH =
* ownership walk); a real held dirfd fstatat()s a single validated component
* under it. vfs_stat follows the leaf, vfs_lstat does not. */
int vfs_stat(int dirfd, const char *path, STRUCT_STAT *st, int flags)
{
RETURN_ERROR_IF_NULL(path);
if (dirfd != VFS_AT_FDCWD) {
#ifdef AT_FDCWD
/* Held-fd: reject empty, multi-component (a '/' would resolve a path
* under the pinned dir) and ".." (escapes to the parent). "." is
* allowed: a read-only fstatat of the dir itself is legitimate
* (link_stat_at). */
if (!*path || strchr(path, '/')
|| (path[0] == '.' && path[1] == '.' && path[2] == '\0')) {
errno = EINVAL;
return -1;
}
return fstatat(dirfd, path, st, 0);
#else
(void)dirfd; errno = ENOSYS; return -1;
#endif
}
if (flags & VFS_ALLOW_SYMLINK)
return vfs__stat_plain(path, st);
return do_xstat_at(path, st, 0, vfs__stat_plain, flags);
}
int vfs_lstat(int dirfd, const char *path, STRUCT_STAT *st, int flags)
{
RETURN_ERROR_IF_NULL(path);
if (dirfd != VFS_AT_FDCWD) {
#ifdef AT_FDCWD
/* Held-fd: reject empty, multi-component and ".."; "." is allowed
* (read-only fstatat of the dir itself -- link_stat_at). */
if (!*path || strchr(path, '/')
|| (path[0] == '.' && path[1] == '.' && path[2] == '\0')) {
errno = EINVAL;
return -1;
}
# if defined SUPPORT_LINKS && defined AT_SYMLINK_NOFOLLOW
return fstatat(dirfd, path, st, AT_SYMLINK_NOFOLLOW);
# elif defined SUPPORT_LINKS
/* No AT_SYMLINK_NOFOLLOW: an fstatat via a held dirfd cannot honour
* lstat's no-follow contract, so fail loud rather than silently
* follow the leaf (mirrors the held-fd vfs_lchown ENOSYS arm); a
* symlink-sensitive caller must use the path-based form. */
(void)dirfd; errno = ENOSYS; return -1;
# else
/* No link support: nothing to follow, fstatat == lstat. */
return fstatat(dirfd, path, st, 0);
# endif
#else
(void)dirfd; errno = ENOSYS; return -1;
#endif
}
if (flags & VFS_ALLOW_SYMLINK)
return vfs__lstat_plain(path, st);
#if defined SUPPORT_LINKS && defined AT_FDCWD && defined AT_SYMLINK_NOFOLLOW
return do_xstat_at(path, st, AT_SYMLINK_NOFOLLOW, vfs__lstat_plain, flags);
#elif defined SUPPORT_LINKS
return vfs__lstat_plain(path, st);
#else
return do_xstat_at(path, st, 0, vfs__stat_plain, flags);
#endif
}
int vfs_fstat(int fd, STRUCT_STAT *st)
{
#ifdef USE_STAT64_FUNCS
return fstat64(fd, st);
#else
return fstat(fd, st);
#endif
}