Compare commits

..
69 Commits
Author SHA1 Message Date
Andrew Tridgell 2358081d3d fleettest: mac2-hfs runs the backup-dir ownership race too
operator-path-backup-chown probes for a cross-device directory, because
make_backup() renames into a same-filesystem backup dir and never reaches
the set_file_attrs() path the test is about.  On macOS there is normally
no such directory, so the test is a macOS-wide expected skip -- but this
target puts the scratch trees on a separate HFS+ volume, which supplies
exactly the condition it was looking for.

Same reason backup-crossdev-copy and chmod-temp-dir are already omitted
here.  The test PASSES on the target (10.9s); only the expected-skip
bookkeeping was wrong.
2026-08-13 10:20:13 +10:00
Andrew Tridgell 1030929cba vfs: adapt the merged-in base changes to the VFS layer
The base gained 13 commits.  Three needed hand-porting because the change
lands on code the VFS split moved or renamed:

- 31130112 (--confine-root) is almost entirely in syscall.c, which does
  not exist here.  abspath_excluded_by_module() becomes
  abspath_outside_confinement() in vfs/dirstack.c, taking its root from
  vfs.module_dir when we are a daemon and from confine_root otherwise,
  plus the fd-pin helpers; ona_open() in vfs/owner_walk.c gains the
  getcwd() seed and the pin_transit exemption.  The VFS passes is_operator
  as an argument where the base reads operator_path_resolve, so the
  refusal takes it from there rather than the deleted global.

- d09edb85 (--link-dest hard-link fallback) keeps the VFS call
  vfs_link_at(cmpbuf, fname, !am_daemon ? VFS_OPERATOR_PATH : 0, 0) and
  adopts the base's cannot_hardlink/match_level fallback around it.

- t_stub.c gains the confine_root/confine_rootlen stubs beside the VFS
  curr_dir note.

Tree is byte-identical to the merge oracle (tag merge-reference-11).
2026-08-13 10:20:13 +10:00
Andrew Tridgell 893e88ade1 testsuite: cover the backup-dir ownership set under a parent swap
The previous commit gave set_file_attrs()' path-based chmod/chown the
operator ownership walk, but nothing exercised the chown side: the
instrumented branch was reached 92 times by the existing suite, all of
them chmod.  That is the same gap that let the upstream fix ship inert.

The uncovered path is a SYMLINK backup.  op_pin cannot pin a symlink --
there is no O_NOFOLLOW open of one -- so make_backup()'s set_file_attrs()
falls through to the full-path lchown.  Without the walk, a backup parent
flipped to an attacker-owned symlink redirects that lchown onto a victim
outside the backup tree and retags it as the attacker's.

Two things make this awkward to test, and both are why it was missed:

  - On one filesystem make_backup() hard-links or renames the item into
    the backup dir and never calls set_file_attrs() at all.  The fixture
    therefore puts the backup dir on tmpfs so link and rename fail EXDEV
    and the recreate path runs.  backup-crossdev-copy probes the same way.

  - A statically planted symlink proves nothing: rsync's own backup-dir
    validation deletes a non-directory component before using it.  The
    plant has to be a live flip, as in operator-path-backup-symlink.

The positive control checks both that the symlink reached the backup tree
and that the backup copy carries the attacker's uid -- i.e. that an lchown
actually ran.  Without that second assertion the race would pass
vacuously on any build where the chown never happens.

RED on the parent of the previous commit (victim outside/f94 retagged
away from root); GREEN here.  Runs on Linux CI as root; registered as an
expected skip on Cygwin (root-only) and macOS (root-only, and no
cross-device tmpfs -- backup-crossdev-copy is listed there for the same
reason).  Full expected-skip oracle passes exactly.
2026-08-13 10:20:13 +10:00
Andrew Tridgell 730e619767 vfs: give set_file_attrs' path-based chmod/chown the operator policy
Base commit 0bfcd3b0 taught do_chmod_at()/do_lchown_at() to resolve an
operator-supplied path through the ownership walk.  The previous commit
ported that into vfs_chmod()/vfs_lchown(), but on this branch the policy
is a per-call argument rather than the ambient operator_path_resolve
global, and set_file_attrs() still passed 0 -- so the ported branch was
unreachable and the fix was inert.

op_pin already covers a reg/dir/fifo leaf with a pinned fd, which is
stronger than the walk.  What it does not cover reaches the path-based
fallbacks: a symlink or device leaf never enters op_pin, and a non-root
operator can fail the pin open with an ordinary EACCES and fall through
with op_refuse clear.  Both then resolved the full operator path with a
bare lchown()/chmod().

Derive the flag from ATTRS_OPERATOR_PATH and pass it to the two
VFS_AT_FDCWD fallbacks only; the held-dirfd arms stay at 0, since a
pinned parent already confines them.  vfs_chmod()'s operator branch
skips S_ISLNK on its own, so a symlink-as-object keeps the
lchmod()/setattrlist() path, matching the base.

Also drop ten .gitignore entries the rebase duplicated: this branch's
own test-helper block and the base's new one list the same binaries.

Suite 259/0/83; the operator-path, backup and temp-dir families pass as
root over --use-tcp.  Instrumenting the branch shows it is now reached
92 times across those tests (the chmod side); the lchown side has no
test exercising it, which is how the gap survived upstream.
2026-08-13 10:20:13 +10:00
Andrew Tridgell fca1d10ff5 vfs: adapt the merged-in base changes to the VFS layer
The base gained 99 commits since the last rebase.  Nine files needed a
hand-port because the change lands on code the VFS split moved or
renamed, and syscall.c no longer exists here:

- backup.c: make_path() now runs on a private copy of backup_dir_buf
  (c933f622), so vfs_make_path() takes dirbuf and drops the restore.
- clientserver.c: keep both the module-root snapshot and the new
  daemon_config_filter_file window.
- exclude.c: the peer-driven merge-file confinement is the
  vfs_open_owner_walk() is_operator argument, not a global.
- fileio.c: the coalesced --sparse writer's new helpers use
  vfs_lseek()/vfs_punch_hole().
- generator.c/vfs/mknod.c: gen_entry_mknod() falls back through
  vfs_mknod(); the atfd path keys its mknodat() off HAVE_MKNODAT.
- receiver.c: secure_recv_open() passes VFS_OPERATOR_PATH instead of
  toggling operator_path_resolve; open_readonly_inplace() uses the VFS
  stat/chmod/open wrappers.
- sender.c: absolute --relative cleanup anchors at "/" via
  vfs_resolve_open(), the copy-links walk uses
  vfs_resolve_open_at_beneath(), and the source removal goes through
  vfs_unlink().
- vfs/chmod.c, vfs/chown.c: VFS_OPERATOR_PATH now takes the ownership
  walk, and the no-follow chmod grows the non-Linux fd path.
- vfs/secure_open.c: the fd-anchored resolver splits into a shared
  internal with an allow-dotdot entry point, and secure_walk_at() routes
  a literal "."/".." through ds_descend() before the leaf fast paths.

Tree is byte-identical to the merge oracle (tag merge-reference-10).
2026-08-13 10:20:13 +10:00
Andrew Tridgell 97d7cfc5cb 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).  Three of
the base's new commits touch code this branch relocated or reworked:

- options.c (3fe1ed51 "rsync: confine the daemon files-from open to the
  module root"): the base wraps the files-from open in the
  operator_path_resolve global, which no longer exists here.  Pass the
  operator context explicitly instead: vfs_open_owner_walk(..., 1).

- receiver.c (cfd40f55 "receiver: confine peer-selected partial basis
  paths"): the new relative-partial-basis branch uses the VFS resolver
  name, vfs_resolve_open().

- vfs/chmod.c (7b16872e "syscall: silence scan-build dead-store in
  do_fchmodat_nofollow fallback"): syscall.c is deleted here, so the
  mode masking move and the unused-arg casts land in the relocated
  do_fchmodat_nofollow.

Tree is byte-identical to the validated merge result.
2026-08-13 10:20:13 +10:00
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
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
Andrew Tridgell 518c09fccf gitignore: ignore the remaining test-helper binaries
t_hashtable_overflow, t_iwildmatch, t_clean_fname, and t_safe_arg were
built by the suite but missing from .gitignore, so a stray git add -A
sweeps them into a commit (as happened during this rebase round).  List
them alongside the other t_* harnesses.
2026-08-13 10:20:13 +10:00
Andrew Tridgell 01e0de397b testsuite: keep daemon test ports out of the 13000+ bloatware range
daemon-exclude-namebased bound its daemon on 13010, and the
setup_chroot_inner helper hashed into 12940-13139 -- both reach into
13000+, where ASUS Armoury Crate on the Cygwin CI host parks localhost
listeners (13010, 13030-13032), making the port probe fail the test.
The helper also used str hash(), which is per-process randomized
(PYTHONHASHSEED), so its port wandered run to run.

Move the fixed port to 12931 and the helper to a deterministic
crc32-based slot in the otherwise-unused 12800-12859 band.
2026-08-13 10:20:13 +10:00
Andrew Tridgell ea03fe0e9a vfs: fix the no-AT_FDCWD fallback in vfs__symlink_secure
The #else arm for platforms without AT_FDCWD called the four-argument
vfs_symlink() with two arguments -- a compile error on such systems.
Call vfs__symlink_plain(), matching the base's do_symlink() fallback.
Found by codex review; predates this rebase round.
2026-08-13 10:20:13 +10:00
Andrew Tridgell 4c142f671b 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 integrated (see the merge for reference):

- Port ddda7ba5's operator-path confinement of do_symlink_at and
  do_rmdir_at into the relocated VFS code.  vfs__symlink_secure gains
  the VFS_OPERATOR_PATH ownership-walk branch (parent confined via
  vfs_owner_walk_parent, shared leaf-creation preserved so fake-super
  emulation still applies); vfs__unlink_secure extends its existing
  operator branch to the rmdir/AT_REMOVEDIR case.  Callers pass the
  policy explicitly where the base set operator_path_resolve: the
  keep_backup symlink create (backup.c), the backup-tree rmdir in
  delete_item (delete.c, DEL_FOR_BACKUP), and handle_partial_dir's
  rmdir (util1.c).
- Port 1f8f89c2's robust_rename EXDEV-fallback confinement into
  vfs/robust.c: an absolute --temp-dir/--partial-dir operand routes the
  copy_file dest-write and the source-unlink through the ownership walk
  (VFS_OPERATOR_PATH), so a raced parent symlink can't redirect either
  out of the module.
- Drop the stale "no ownership-walk branch" notes in vfs/vfs.h and
  vfs/symlink.c now that symlink and rmdir carry the branch.

Tree is byte-identical to the validated merge result.
2026-08-13 10:20:13 +10:00
Andrew Tridgell 331188aaf6 acls: use vfs_relpath_active in the Solaris ACL branch
The HAVE_SOLARIS_ACLS facl(2) paths called secure_relpath_active() -- the
base-branch name -- which does not exist on the VFS branch (here the gate is
vfs_relpath_active()).  Linux/BSD never compile that branch, so it stayed latent;
a real Solaris build fails with an implicit-declaration error.  Rename both call
sites (set_rsync_acl default-ACL delete and the access/default set fallback).
2026-08-13 10:20:13 +10:00
Andrew Tridgell 1a8c81d9d7 github: run the ASan suite under --enable-strict-confinement
Enable the strict confinement assertion in the ASan/UBSan CI build so the suite
enforces "no confined-regime raw path metadata op" on every run.
2026-08-13 10:20:13 +10:00
Andrew Tridgell fc23ad74c9 rsync,generator: assert the xattr/ACL pin invariant under STRICT_CONFINEMENT
Guard the two receiver decision points that drive the xattr/ACL setters down a
raw path-based branch -- set_file_attrs() and gen_entry_copy_xattrs().  In a
strict build, a confined pinnable non-operator leaf that reached the setters
without a confined fd (held_fd/xfd < 0 and not refused) aborts: that is exactly
the copy-xattrs fallback class.  The guards mirror the pin/re-pin conditions, so
they cannot false-abort a legitimate transfer.

Verified: under --enable-strict-confinement the suite is 210/0 with zero aborts;
neutering the re-pin makes copy-xattrs-symlink-race abort at the guard.
2026-08-13 10:20:13 +10:00
Andrew Tridgell 531f941c7d vfs: add STRICT_CONFINEMENT build-time confinement assertion
Add a CI/dev hardening mode (--enable-strict-confinement) that turns any
confined-regime raw path-based metadata op into a hard abort, so a reintroduced
copy-xattrs-class fallback fails the test suite instead of silently escaping
through a flipped parent symlink.

vfs_must_be_confined() is the predicate: the modern *at/O_NOFOLLOW primitives are
present, the path is non-operator, relative and multi-component (a parent the
attacker could flip), and vfs_relpath_active().  vfs_strict_confine_fail() logs
and aborts.  Both are absent/no-op without STRICT_CONFINEMENT, so there is no
production behaviour change.
2026-08-13 10:20:13 +10:00
Andrew Tridgell 5210c5fd53 vfs: review fixes
cleanups from review comments by Sam James. Thanks!
2026-08-13 10:20:13 +10:00
Andrew Tridgell eab19ec7f9 rsync: never path-resolve a confined receiver's xattr/ACL write (copy-xattrs race)
set_file_attrs() pins the entry via the cached held dir fd and drives the xattr
/ACL ops off that fd (fsetxattr).  But when the pin missed -- vfs_cached_dirfd()
returns -1 (its dir isn't the held one, or the path is deeper than the dirfd
cache), or the leaf openat() loses a race -- held_fd stayed -1 and
set_stat_xattr()/set_xattr()/get_acl_fdat()/set_acl_fdat() fell through to the
path-based branch (sys_lsetxattr(fname,...)).  Unlike the chmod/chown/times path
wrappers (which secure-resolve), that raw lsetxattr re-resolves the parent, so a
concurrent flip of a dest parent component to a symlink->outside lands the xattr
OUTSIDE the destination tree (the intermittent copy-xattrs-symlink-race escape
that surfaces under -j load, which widens the open->setxattr window).

Re-pin through the secure resolver when the cached pin misses on a confined,
non-operator receiver path, so the xattr/ACL ops always use a confined fd -- NOT
a raw path lsetxattr; if the re-pin also fails (a genuinely raced parent/leaf
symlink) skip the path-based ops (xattr_refuse) rather than redirecting them.
The re-pin passes O_DIRECTORY for a directory leaf (the secure resolver refuses a
bare dir open with EISDIR), so dir xattrs/ACLs are still preserved on a cache
miss.  Apply the same re-pin/refuse to gen_entry_copy_xattrs() (the dir xattr
copy), whose dfd<0 path likewise fell to copy_xattrs() with dest_fd==-1.
chmod/chown/times are unchanged (already confined via their *at wrappers);
operator paths keep op_pin/op_refuse.

Confirmed: under heavy parallel load the dangerous lsetxattr fallback was reached
52+ times before and 0 times after; dir xattrs preserved; suite 210/0/66,
root metadata PoC tests pass, builds +/-xattr.
2026-08-13 10:20:13 +10:00
Andrew Tridgell 955115c5a5 testsuite: per-operand policy regression for vfs_rename_at (PR #30)
Extend the t_rename_secure harness with PS-refuse/PS-follow: rename to the SAME
operator-owned escaping symlink path (oplink -> ../trap) with the SAME operator
old-side flag, differing ONLY in the new-side flag.  The ownership walk (operator)
follows the operator's own uid0/euid symlink; the secure receiver resolve
(transfer, flag 0) refuses it because it leaves the cwd anchor.  PS-refuse must be
rejected and PS-follow must succeed -- proving the new side's confinement is
independent of the old side's policy.

Deterministic, non-root, no race.  Verified RED on the old whole-call behaviour
(emulated by OR-ing both operands' flags, the new side then follows oplink and
escapes to ../trap) and GREEN with the per-operand split.  The driver adds the
oplink fixture and the per-side source files.

Suggested-by: Zen Dodd <mail@steadytao.com>
2026-08-13 10:20:13 +10:00
Andrew Tridgell 62122367a4 vfs: split two-path ops to per-operand policy flags (rename/link)
vfs_rename_at() and vfs_link_at() took a single vfs_flags applied to BOTH
operands, so VFS_OPERATOR_PATH (ownership walk: follow uid0/euid symlinks, refuse
foreign) was applied to a transfer operand even when only one side was operator-
supplied -- relaxing the secure receiver resolve that a transfer/file-list path
should get.  The default branch confined each side by absolute-vs-relative, but
that missed a *relative* operator path (--backup-dir=bdir, --partial-dir=.rsync,
relative --link-dest).  Reported in the rsync-private PR #30 review.

Take old_flags and new_flags separately and resolve each operand under its own
policy via a shared vfs_twopath_side() helper (operator walk if the side is
VFS_OPERATOR_PATH or absolute; secure receiver resolve if relative-with-slash;
AT_FDCWD for a bare name).  An operator basis/backup path on one side can no
longer relax the other side's confinement.

Callers now pass the correct per-side policy:
- backup link/rename: source (live dest file) = transfer (0), backup target = operator.
- receiver partial-dir rename: partialptr = operator, final dest = transfer.
- hard_link_one / generator link-dest: basis source = operator (non-daemon), dest = transfer.
- finish_transfer / gen_entry_rename / robust fallback: both transfer (0,0; default per-side).
- robust_unlink ETXTBSY sibling rename: both share the caller's policy.
Also drops the operator flag on backup's "just in case" robust_unlink of the
transfer-side source.

Behaviour-preserving for the operator side (the backup/partial-dir parents still
owner-walk); it only tightens the transfer side back to the secure resolve.
Builds +/-xattr; suite 210/0/66; root operator-path PoC tests all pass.

Suggested-by: Zen Dodd <mail@steadytao.com>
2026-08-13 10:20:13 +10:00
Andrew Tridgell 139c305814 delete: confine the backup-tree unlink via the operator ownership walk
A backup-tree delete (delete_item with DEL_FOR_BACKUP -- removing an existing
leaf under an absolute --backup-dir before the new backup is placed) fell through
del_unlink() to robust_unlink(fbuf, 0), which resolves the leaf's parent by path
with no ownership walk.  A local attacker who flips a backup-path parent to a
symlink in that window could redirect the unlink outside the backup tree.

The base confines this: make_backup() wraps make_backup_inner() in
operator_path_resolve, so the same unlink reaches do_unlink_at()'s owner-walk
branch.  The VFS refactor replaced the global with explicit vfs_flags but left
del_unlink() passing 0, dropping the confinement.  Thread VFS_OPERATOR_PATH into
del_unlink()'s path-based fallback when DEL_FOR_BACKUP is set (the exact cases
that ran under the base's operator wrap); a held-dirfd delete is already confined
and ignores it.  The rmdir path keeps no owner-walk branch, matching base.
2026-08-13 10:20:13 +10:00
Andrew Tridgell 4099664250 vfs: round-3 operator-path reconciliation (snap to merge oracle)
The -X theirs rebase replayed the 47 VFS commits onto the new base, letting each
VFS commit win its refactor on conflict; that drops the round-3 base changes that
overlap the operator-path code (they were authored against the deleted syscall.c
and the pre-vfs_flags model).  This commit snaps those files byte-for-byte from
the validated merge oracle (merge-reference-3 = 6afdd389), so the branch tip is
tree-identical to the merge that built clean +/-xattr and passed the suite +
root operator-path PoC tests.

Reconciled: the syscall.c->vfs/ ports (secure_open/owner_walk/dirstack/rename/
link/chmod/chown/times/copy_file + vfs.h/vfs_internal.h), the set_file_attrs
op_pin via ATTRS_OPERATOR_PATH (rsync.c/rsync.h + backup.c callers), the
secure_basis_open/basis_link_stat operator branches (receiver.c/generator.c), the
copy_file/gen_entry_copy_xattrs held-fd source reads, the change_dir/sender
opt-out sites (util1.c/sender.c/clientserver.c), and Makefile.in.
2026-08-13 10:20:13 +10:00
Andrew Tridgell fbfa40fe4d vfs: port 37dbb263's operator-path mknod FIFO/socket fallback
Linear-rebase counterpart of the round-2 merge resolution: 37dbb263 fixed
do_mknod_at()'s operator branch in syscall.c (deleted on this branch) to
fall back to mkfifoat() for a FIFO and EOPNOTSUPP for a nested socket when
mknodat() can't make a special file on the BSDs/macOS/Solaris.  Re-express
it in the VFS_OPERATOR_PATH branch of vfs__mknod_secure() in vfs/mknod.c,
mirroring the secure-relpath branch already in that function.

Tree is byte-identical to the validated round-2 merge result.
2026-08-13 10:20:13 +10:00
Andrew Tridgell e0f2f52670 vfs: adapt the merged-in base changes to the VFS layer
Linear-rebase counterpart of the two conflict resolutions made when the
sec-fixes base was integrated (see the merge for reference):

- Port 8a7e9a16's fixed-array dirstack into the relocated VFS code:
  vfs/vfs_internal.h (DS_MAXDEPTH + inline int fds[]) and vfs/dirstack.c
  (drop malloc/realloc/free; ENOMEM past the cap).  8a7e9a16 changed the
  dirstack in syscall.c, which this branch deleted, so the fix is
  re-expressed in vfs/.
- Map the base's new F_XATTR ndx<0 guard (1d36a565) from the old
  do_chmod_at() to the unified vfs_chmod(VFS_AT_FDCWD, fname, ..., 0).

Tree is byte-identical to the validated merge result.
2026-08-13 10:20:13 +10:00
Andrew Tridgell 18e30dd23e vfs: codex review fixes for the unified stat/chmod/lchown
Three issues from the post-unification review:

- The !SUPPORT_XATTRS x_stat/x_lstat macros hardcoded VFS_ALLOW_SYMLINK,
  silently downgrading an operator-path stat (backup-dir) from the ownership
  walk + module confinement to plain follow-stat in no-xattr builds (a
  pre-existing gap the unified vfs_stat now lets us close).  Forward the
  caller's vfs_flags instead, so no-xattr builds get the same policy as
  xattr builds.

- Held-fd ".." was accepted by the stat/chmod/lchown dispatchers, which
  would operate on the PARENT of the pinned dirfd -- the wrong default for a
  security boundary even though no current call site passes it.  Reject ".."
  while still allowing "." (a stat/chmod/chown of the dir itself, which
  link_stat_at and set_file_attrs legitimately do for directory entries).

- Two stale comment/test strings still said vfs_chmod_at (now vfs_chmod).

Verified with a local --disable-xattr build; full suite 190/50.
2026-08-13 10:20:13 +10:00
Andrew Tridgell af68dac222 vfs: update the !SUPPORT_XATTRS x_stat/x_lstat macros for the unified stat
vfs_stat/vfs_lstat are now vfs_<op>(int dirfd, path, st, int flags); the
no-xattr fallback macros still expanded to the old 2-arg vfs_stat(fn,fst),
breaking the build without xattr support.  Map them to the unified plain
form: vfs_stat(VFS_AT_FDCWD, fn, fst, VFS_ALLOW_SYMLINK) (and lstat), which
preserves the prior plain-stat behaviour.  Caught by the fleettest on
openbsd; verified with a local --disable-xattr build.
2026-08-13 10:20:13 +10:00
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
Andrew Tridgell 1214c4a2f9 vfs: unify stat/lstat, chmod, lchown into dirfd+flags form
Collapse the remaining three-form ops into one call each, matching
mkdir/mknod/symlink/unlink:
  vfs_stat(int dirfd, path, st, flags)
  vfs_lstat(int dirfd, path, st, flags)
  vfs_chmod(int dirfd, path, mode, flags)
  vfs_lchown(int dirfd, path, owner, group, flags)
dirfd == VFS_AT_FDCWD resolves the path (VFS_ALLOW_SYMLINK = plain libc op,
default 0 = secure receiver resolve, VFS_OPERATOR_PATH = ownership walk for
stat); a real held dirfd operates on a single component under it.  The old
plain/_at/_atfd bodies become static helpers behind the dispatchers;
vfs_fstat stays (fd-based).  chmod/lchown keep their no-owner-walk behaviour
(VFS_OPERATOR_PATH resolves the same as the default secure walk).

Pure API-narrowing -- the operator policy was already explicit; no global is
involved.  This is behaviour-preserving (plain sites -> VFS_ALLOW_SYMLINK,
_at -> the flag they already carried, _atfd -> held-fd 0).

Held-fd validation differs by op: mkdir/mknod/symlink/unlink reject "."/".."
(creating/removing them is nonsensical), but stat/chmod/lchown ALLOW "."
(a read or metadata op on the dir itself is legitimate -- link_stat_at and
set_file_attrs do it for directory entries); only empty and multi-component
("/") names are rejected there.  Caught by the suite (chmod/metadata/
ownership-depth + the implied-"." dir mkdir path).

Full suite 190/50.
2026-08-13 10:20:13 +10:00
Andrew Tridgell f436561d08 vfs: fix the !SUPPORT_XATTRS x_stat/x_lstat macros for the vfs_flags arg
The stat/lstat threading added a vfs_flags argument to x_stat()/x_lstat(),
but on a build without xattr support those are 3-arg macros in rsync.h
(mapping to plain vfs_stat/vfs_lstat), so the 4-arg call sites failed to
compile ("too many arguments to macro").  Give the macros the extra
(ignored) parameter.  Caught by the fleettest on openbsd; verified with a
local --disable-xattr build.
2026-08-13 10:20:13 +10:00
Andrew Tridgell b226005967 vfs: delete the operator_path_resolve global
With every operator-context call site now passing VFS_OPERATOR_PATH
explicitly, nothing reads vfs.operator_path_resolve any more.  Remove the
four now-dead set/clear blocks (make_backup, handle_partial_dir, and the two
generator in-place-backup paths) and delete the field from struct vfs.

The operator-supplied path resolution policy -- which selects the ownership
walk and the daemon module-root confinement for --backup-dir/--temp-dir/
--partial-dir/--link-dest operations -- is no longer ambient dynamic-scope
state poked into the VFS from mainline.  It travels as an explicit per-call
VFS_OPERATOR_PATH flag through the primitives and compounds, resolving the
layering violation that motivated this work.

Full suite 190/50.
2026-08-13 10:20:13 +10:00
Andrew Tridgell 871d1974f1 vfs: thread the operator flag through stat/lstat + x_stat/x_lstat
vfs_stat_at / vfs_lstat_at (and their shared do_xstat_at helper) gain a
vfs_flags argument so the ownership-walk branch reads VFS_OPERATOR_PATH
instead of vfs.operator_path_resolve -- this was the last primitive still
reading the global.  The metadata wrappers x_stat()/x_lstat() (which stay in
xattrs.c, above the VFS) gain a vfs_flags argument forwarded to the
primitives.

Call sites classified: every backup-dir stat (validate_backup_dir,
copy_valid_path's x_stat, make_backup_inner's x_lstat + bak lstat) and the
partial-dir lstat (handle_partial_dir) pass VFS_OPERATOR_PATH; the transfer
enumeration (flist link_stat + friends), the in-place dir stat (generator),
and the set_stat_xattr lstat pass 0.  generator's non-daemon --link-dest
basis lookup passes 0 too (is_operator only gates the daemon module
confinement, a no-op there).

With this, NOTHING reads vfs.operator_path_resolve any more -- it is now
write-only (set by four now-dead blocks, removed next).  Full suite 190/50;
backup/partial/link-dest/xattr/daemon subset green.
2026-08-13 10:20:13 +10:00
Andrew Tridgell 4a0f20f657 vfs: flag vfs_link_at + thread hard_link_one; retire the hard-link block
Give vfs_link_at an explicit vfs_flags argument (kept two-parent form) so its
ownership-walk branch reads VFS_OPERATOR_PATH instead of the global.

hard_link_one() gains a vfs_flags argument forwarded to vfs_link_at, so the
generator's alt-dest hard-link no longer sets vfs.operator_path_resolve
around the call: it passes (!am_daemon ? VFS_OPERATOR_PATH : 0), matching the
prior op = !am_daemon gating (a non-daemon --link-dest uses the ownership
walk; a daemon keeps the stronger module-anchored vfs_relpath_active path).
That operator_path_resolve block is removed.

Other call sites: the backup link (make_backup link_or_rename) passes
VFS_OPERATOR_PATH; the try_dests alt-dest link and the hlink finish-up
hard_link_one pass 0 (transfer context, as the global was there).

Set-sites: 5 -> 4 (backup make_backup, generator 2085/2130, util1
handle_partial_dir -- all wrapping the not-yet-flagged metadata layer
set_file_attrs/x_stat/x_lstat).  Full suite 190/50; hardlink/link-dest/
backup/daemon subset green.
2026-08-13 10:20:13 +10:00
Andrew Tridgell 5c97ad3403 vfs: flag vfs_open_owner_walk + secure_basis_open; retire two more blocks
Give vfs_open_owner_walk an explicit is_operator argument so its module-root
confinement (abspath_excluded_by_module) reads the policy from the caller
instead of the vfs.operator_path_resolve global -- the last owner-walk
function still reading it.  secure_basis_open gains an is_operator parameter
threaded to it; its gate (owner-walk vs strict resolve) now branches on that
param too.

Faithful conversion (no behaviour change): every direct caller passes the
value the global held at that site -- config/log/motd/early-input/files-from/
batch/connection/exclude/authenticate/params, change_dir's daemon dest-chdir,
and vfs_secure_mkstemp's --temp-dir all pass 0; secure_basis_open passes
VFS_OPERATOR_PATH only for the operator cases (a --partial-dir basis,
fnamecmp_type == FNAMECMP_PARTIAL_DIR; and one_inplace partial-dir staging).
This preserves the existing --temp-dir behaviour (unconfined) by deliberate
choice; hardening that is a separate decision.

The receiver partial-dir-basis and one_inplace operator_path_resolve blocks
are removed (receiver.c is now free of the global).  Set-sites: 7 -> 5
(backup make_backup, generator 1071/2085/2130, util1 handle_partial_dir).

The operator-path-partial-dir-daemon test caught a real regression mid-change
(secure_basis_open's gate still read the global after its set/clear block was
removed, dropping the confinement); fixed by gating on is_operator.

Full suite 190/50.
2026-08-13 10:20:13 +10:00
Andrew Tridgell c89283a500 vfs: fix stale comment in unlink_and_reopen (robust_unlink now flagged)
The comment predated flagging robust_unlink; vfs_flags now reaches both the
robust_unlink and the create.  (codex review nit.)
2026-08-13 10:20:13 +10:00
Andrew Tridgell d037014ff4 vfs: flag vfs_rename_at + retire three operator-path blocks
Give vfs_rename_at an explicit vfs_flags argument (it keeps its two-parent
form, per the kept-explicit scope for rename) so its ownership-walk branch
reads VFS_OPERATOR_PATH instead of the vfs.operator_path_resolve global.

Call sites: the backup link/rename (make_backup) and the in-place/partial
rename (receiver) pass VFS_OPERATOR_PATH; the transfer renames (generator
entry rename, finish_transfer) and robust_rename pass 0; robust_unlink's
ETXTBSY rename-retry now forwards robust_unlink's own vfs_flags -- so
robust_unlink is fully explicit (both its unlink and its rename retry).

With unlink and rename both flagged, three operator_path_resolve set/clear
blocks that wrapped only a single now-flagged op are removed: the receiver
partial-dir rename and the two partial-dir unlinks (generator + receiver).
The remaining seven blocks still wrap not-yet-flagged ops (link, stat/lstat,
chmod/lchown via set_file_attrs, secure_basis_open) and stay for now.

Full suite 190/50; rename/partial/backup/inplace/daemon subset green.
2026-08-13 10:20:13 +10:00
Andrew Tridgell e12eb04e12 vfs: unify the unlink/rmdir family + flag robust_unlink
Collapse vfs_unlink / vfs_unlink_at / vfs_unlink_atfd / vfs_rmdir /
vfs_rmdir_at into one vfs_unlink(int dirfd, const char *path, int flags).
VFS_REMOVEDIR selects rmdir/AT_REMOVEDIR; dirfd == VFS_AT_FDCWD resolves the
path, a real held dirfd removes a single validated component.  The secure
body reads the operator context from VFS_OPERATOR_PATH instead of the
global; the rmdir path keeps its pre-existing no-owner-walk behaviour (it
never read the global -- matched the old vfs_rmdir_at).

robust_unlink() gains a vfs_flags argument threaded to vfs_unlink, so the
operator policy reaches it explicitly: the backup-tree callers (make_backup
link_or_rename, generator in-place backup) and copy_file's dest unlink (when
copying a backup) pass VFS_OPERATOR_PATH; delete and robust_rename's
transfer-context calls pass 0.  Its ETXTBSY rename-retry still uses
vfs_rename_at (the rename primitive isn't flagged yet), which reads the
global -- still set by the surrounding backup blocks -- so behaviour is
unchanged.

Call sites classified: --remove-source-files fallback (sender) follows
symlinks (VFS_ALLOW_SYMLINK); partial-dir unlink/rmdir (handle_partial_dir)
and in-place-backup/partial unlinks operator; the rest secure or held-fd.
Full suite 190/50; delete/backup/partial/remove-source/daemon subset green.
2026-08-13 10:20:13 +10:00
Andrew Tridgell ae819474d7 vfs: flag vfs_open_at + thread vfs_flags through copy_file's dest open
Wiring phase: give vfs_open_at an explicit vfs_flags argument so the
operator-path resolution policy reaches it from the caller instead of the
vfs.operator_path_resolve global, and thread that argument through
copy_file()/unlink_and_reopen() to the dest create.

Call sites classified: the in-place backup open (generator) and the backup
copy (backup.c, generator in-place) pass VFS_OPERATOR_PATH; the copy-dest
basis copy (copy_altdest_file) passes 0 -- it must NOT use the ownership
walk (re-opening the copy_xattrs parent-symlink race, see generator.c
comment), which the explicit argument now expresses directly; robust_rename
runs in transfer context and passes 0.

copy_file's dest UNLINK (robust_unlink) still reads the operator global,
still set by the surrounding backup blocks, so behaviour is unchanged at
every site (flag and global agree).  Full suite 190/50; backup/copy-dest/
link-dest/partial/daemon subset green.
2026-08-13 10:20:13 +10:00
Andrew Tridgell cbe4ae7744 gitignore: ignore the vfs test binaries
These compiled test harnesses (t_*_secure, t_acl, simdtest) were
accidentally committed by a git add -A; ignore them like the other
built test programs (t_unsafe, wildtest, ...) so they stay out of the tree.
2026-08-13 10:20:13 +10:00
Andrew Tridgell 1695148937 vfs: move copy_file + robust_unlink/rename into the VFS compound layer
Relocate the file-copy / robust-unlink / robust-rename cluster from util1.c
into vfs/copy_file.c and vfs/robust.c.  These are filesystem mechanics built
on the vfs_* open/read/write/unlink/rename primitives, so they belong in the
VFS compound layer; this is a verbatim move (no behaviour change).

The cluster is mutually coupled -- robust_rename falls back to copy_file for
the cross-filesystem (EXDEV) case, and copy_file's unlink_and_reopen uses
robust_unlink -- so all four move together.  They call out to the metadata/
protocol layer for what isn't pure filesystem (copy_xattrs for the held-fd
xattr copy, handle_partial_dir for the EXDEV partial-dir path); those stay in
their current modules, keeping the dependency one-way (vfs -> nothing
above it pulled in).  safe_read (a static helper only copy_file used) moves
with it.

Operator context still flows via the vfs.operator_path_resolve field for now
(legitimately VFS-internal once these callers live in vfs/); a later commit
threads it as an explicit vfs_flags argument and removes the field.  The
t_*_secure harnesses don't reference the cluster, so no xattr/protocol
symbols are pulled into them.  Full suite 190/50; copy-dest/backup/partial/
link-dest subset green.
2026-08-13 10:20:13 +10:00
Andrew Tridgell 056a9fe6bc vfs: move make_path into the VFS compound layer as vfs_make_path
First step of the VFS three-layer architecture: filesystem-mechanic helpers
that compose the vfs_* primitives belong inside vfs/, so the operator-path
resolution policy travels as an explicit argument instead of leaking across
the vfs<->mainline boundary as ambient state.

make_path() (recursive mkdir over vfs_mkdir/vfs_stat) moves verbatim from
util1.c to vfs/make_path.c and becomes vfs_make_path(fname, mkp_flags,
vfs_flags): mkp_flags keeps the path-handling bits (MKP_DROP_NAME/
MKP_SKIP_SLASH), vfs_flags carries the resolution policy (VFS_OPERATOR_PATH
for the operator-supplied --backup-dir tree, else 0).  The transitional
MKP_OPERATOR flag added in the mkdir step is dropped in favour of the
explicit vfs_flags argument.

Callers: get_backup_name (backup dir) passes VFS_OPERATOR_PATH; the transfer
callers (main/receiver/generator) pass 0.  Full suite 190/50.
2026-08-13 10:20:13 +10:00
Andrew Tridgell 6e305172e5 vfs: unify the symlink family into vfs_symlink(lnk, dirfd, path, flags)
Collapse vfs_symlink / vfs_symlink_at / vfs_symlink_atfd into one call.
dirfd == VFS_AT_FDCWD resolves `path` (secure receiver resolve by default,
plain follow under VFS_ALLOW_SYMLINK); a real held dirfd makes `path` a
single validated component.  The old bodies become static helpers behind
the public dispatcher.

Unlike mkdir/mknod, the symlink secure path has NO ownership-walk branch --
it never read vfs.operator_path_resolve.  That pre-existing asymmetry is
preserved: VFS_OPERATOR_PATH is accepted but resolves the same as the
default secure walk (only the parent dir is confined; the link target is
stored verbatim and never resolved at creation).  The backup-symlink site
passes flags=0, which exactly reproduces the old vfs_symlink_at behavior.

Full suite 190/50.
2026-08-13 10:20:13 +10:00
Andrew Tridgell 31c3b8f3d0 vfs: unify the mknod family into vfs_mknod(dirfd, path, mode, dev, flags)
Collapse vfs_mknod / vfs_mknod_at / vfs_mknod_atfd into one call, mirroring
vfs_mkdir.  dirfd == VFS_AT_FDCWD resolves the path (secure receiver resolve
by default, ownership walk under VFS_OPERATOR_PATH, plain follow under
VFS_ALLOW_SYMLINK); a real held dirfd makes path a single validated
component.  The old plain/secure/atfd bodies become static helpers
(vfs__mknod_plain/_secure/_atfd) behind the public dispatcher; the secure
body reads the operator context from VFS_OPERATOR_PATH instead of the
vfs.operator_path_resolve global.

Call sites: backup file node (make_backup_inner, operator) ->
VFS_OPERATOR_PATH; transfer device/fifo (generator) -> secure / held-fd.
The t_symlink_secure harness's plain PoC call becomes VFS_ALLOW_SYMLINK
(the vulnerable follow it demonstrates) and its secure calls flags=0.

Full suite 190/50.
2026-08-13 10:20:13 +10:00
Andrew Tridgell e34177b124 vfs: unify the mkdir family into vfs_mkdir(dirfd, path, mode, flags)
Collapse vfs_mkdir / vfs_mkdir_at / vfs_mkdir_atfd into one call.  dirfd ==
VFS_AT_FDCWD resolves the path argument; a real held dirfd makes path a
single component created directly under it (the held-fd form validates
that path is a lone harmless component -- rejects empty, any '/', "." and
".." -- so it cannot reintroduce path resolution under the pinned dir).

The plain-vs-secure choice is now an explicit per-call flag instead of a
function-name choice plus the vfs.operator_path_resolve global:
  VFS_ALLOW_SYMLINK  the call site asserts it is safe to follow symlinks
                     (the old plain vfs_mkdir); checked first, wins over
                     VFS_OPERATOR_PATH (no caller passes both)
  VFS_OPERATOR_PATH  operator-supplied path: ownership walk + module
                     confinement (the old global)
  default 0          secure receiver resolve (the old vfs_mkdir_at)

VFS_AT_FDCWD is a VFS-owned sentinel (maps to AT_FDCWD where available,
else a value that routes the held-fd form to ENOSYS) so mainline code
never has to mention AT_FDCWD directly -- avoids a compile break on
platforms lacking it.

mkdir stops reading vfs.operator_path_resolve.  vfs_owner_walk_parent now
takes the operator context as an is_operator parameter; its non-mkdir
callers (the other _at wrappers, generator hard-link) still pass the
global verbatim and convert in later steps.  make_path() gains MKP_OPERATOR
(translated to VFS_OPERATOR_PATH) so the backup-dir creation stays an
operator path by path-semantics, not by relying on the global being set.

Call sites classified: dest dir (main.c) follows symlinks (user's local
dest); transfer dirs (generator, make_path transfer callers) secure;
backup-dir (backup.c, get_backup_name->make_path) and partial-dir
(handle_partial_dir) operator.  The operator_path_resolve=1 blocks stay
for now since they still wrap non-mkdir ops; the global value and the
explicit flag agree at every converted mkdir site.

Full suite 190/50; backup/partial/daemon/exclude/filter/symlink-race
subset green.
2026-08-13 10:20:13 +10:00
Andrew Tridgell 230d25b32b vfs: thread the operator context into abspath_excluded_by_module
First step of the API redesign.  Define the per-call VFS_* flags
(VFS_ALLOW_SYMLINK / VFS_OPERATOR_PATH / VFS_REMOVEDIR) and stop
abspath_excluded_by_module() from reading the vfs.operator_path_resolve
global directly -- it now takes an explicit is_operator argument.

The strict resolver stays confined beneath its anchor (always within the
module), so its two call sites pass is_operator=0 (the check never fires
there anyway).  The ownership walk is reached in BOTH operator context
(--backup-dir/--temp-dir/--partial-dir) and non-operator context (daemon
log-file/motd/config opens, which may legitimately live outside the
module), so vfs_open_owner_walk()/vfs_owner_walk_parent() capture the
global and pass it down through ona_open().  No behavior change -- the
value still comes from the global; later commits replace that source
with the explicit flag and delete the global.
2026-08-13 10:20:13 +10:00
Andrew Tridgell 068c086287 vfs: snapshot the daemon module root into struct vfs
Fold the served module root (module_dir / module_dirlen / module_dirfd)
into the vfs.module_* snapshot so the confinement checks read the VFS's
own state rather than implicit clientserver.c externs.

clientserver.c calls vfs_set_module_root() in two stages, matching when
the values become final: once right after the module path is settled
(before the daemon opens any operator path -- filter/include files, the
log file), with the root dirfd still -1, and again once that dirfd is
pinned by identity.  The dirfd is borrowed (open_anchor_dirfd dup()s it);
the VFS never closes it.  vfs_init() now clears the whole snapshot so a
forked connection can never inherit a stale module root.

The vfs/ readers (dirstack, secure_open, owner_walk) switch from the
externs to vfs.module_*; clientserver/flist/main keep their own globals.
Behavior is identical: between the two calls vfs.module_dirfd is -1, so
open_anchor_dirfd re-resolves the path exactly as before the pin.
2026-08-13 10:20:13 +10:00
Andrew Tridgell b4417876b4 vfs: document the layer contract in vfs/vfs.h
Refresh the header comment now that the wrappers are vfs_* and syscall.c
is gone, and document the three operation forms callers choose between:
the plain path wrapper, the parent-resolved _at form (race-safe receiver
path), and the _atfd form (single component under a pinned dirfd).
Comment-only; no code change.  Validated at protocol 30 and 29.
2026-08-13 10:20:13 +10:00
Andrew Tridgell 94fa51be8c vfs: remove the now-empty syscall.c
Every filesystem wrapper has moved into vfs/, so syscall.c held nothing
but its includes and an orphaned comment.  Delete it and drop syscall.o
from the rsync link and from each test-harness object list (they reach
the vfs_* symbols through libvfs.a now).  The S_BLKSIZE fallback define,
used by vfs/fileio.c, moves there.  No behavior change.
2026-08-13 10:20:13 +10:00
Andrew Tridgell 7c15f78bc3 vfs: restore platform includes in vfs/chmod.c
The chmod family carries macOS setattrlist() and the Linux SYS_fchmodat2
raw-syscall fast path, but the move into vfs/chmod.c dropped the two
platform headers that syscall.c had included at file scope: <sys/attr.h>
(macOS) and <sys/syscall.h> (Linux).  A Linux build hid this -- with
SYS_fchmodat2 undefined the code silently fell back to fchmodat() -- but
macOS failed to compile setattrlist().  Re-add both, matching the
original syscall.c includes.

Found by fleettest (mac2 BUILD-FAIL).
2026-08-13 10:20:12 +10:00
Andrew Tridgell 2341b7b9a0 vfs: move the file-data ops into vfs/fileio.c
Relocate do_ftruncate/do_lseek/do_fallocate/do_punch_hole out of
syscall.c into vfs/fileio.c as the vfs_* names, declared in vfs/vfs.h.
The SUPPORT_PREALLOCATION / HAVE_FALLOCATE / HAVE_SYS_FALLOCATE /
FALLOC_FL_PUNCH_HOLE guards travel verbatim.  This was the last
operation family: syscall.c no longer defines any filesystem wrapper.
No behavior change.  (Portability-sensitive; wants a fleettest.)
2026-08-13 10:20:12 +10:00
Andrew Tridgell 7f2ebc55f4 vfs: move the times family into vfs/times.c
Relocate the timestamp wrappers (do_utimensat/_at/_atfd, do_lutimes,
do_utimes, do_utime) and the crtime paths (do_setattrlist_times/_crtime,
get_create_time, do_SetFileTime) out of syscall.c into vfs/times.c as the
vfs_* names.  The struct create_time / #pragma pack / Cygwin windows.h
and sys/attr.h includes travel with them, as do the SUPPORT_CRTIMES /
HAVE_SETATTRLIST / HAVE_GETATTRLIST / HAVE_UTIMENSAT / HAVE_LUTIMES /
HAVE_UTIMES / HAVE_UTIME guards.  No behavior change.  (Portability-
sensitive; wants a fleettest.)
2026-08-13 10:20:12 +10:00
Andrew Tridgell 951489db55 vfs: move the mknod family into vfs/mknod.c
Relocate do_mknod/do_mknod_at/do_mknod_atfd out of syscall.c into
vfs/mknod.c as the vfs_* names, declared in vfs/vfs.h.  The
HAVE_MKNOD/HAVE_MKNODAT/HAVE_MKFIFO guards and the AF_UNIX socket-bind
fallback (with its <sys/un.h> include) travel verbatim.  No behavior
change.  (Portability-sensitive family; wants a fleettest.)
2026-08-13 10:20:12 +10:00
Andrew Tridgell 546e04b005 vfs: move the lchown family into vfs/chown.c
Relocate do_lchown/do_lchown_at/do_lchown_atfd out of syscall.c into
vfs/chown.c as the vfs_* names, declared in vfs/vfs.h.  The HAVE_LCHOWN
fallback guard travels verbatim.  No behavior change.
2026-08-13 10:20:12 +10:00
Andrew Tridgell 26236b156e vfs: move the mkdir/mkstemp family into vfs/mkdir.c
Relocate do_mkdir/do_mkdir_at/do_mkdir_atfd and do_mkstemp/
do_mkstemp_atfd/secure_mkstemp out of syscall.c into vfs/mkdir.c as the
vfs_* names (secure_mkstemp -> vfs_secure_mkstemp).  The
trim_trailing_slashes path helper (used by trimslash and the mkdir
wrappers) moves with them, keeping its name and now declared in vfs.h.
The static rand_bytes helper, used only by the mkstemp create loop,
moves along too.  No behavior change.
2026-08-13 10:20:12 +10:00
Andrew Tridgell c6e86483b6 vfs: move the link family into vfs/link.c
Relocate do_link/do_link_at/do_link_atfd out of syscall.c into
vfs/link.c as the vfs_* names, declared in vfs/vfs.h.  The
HAVE_LINK/HAVE_LINKAT guards travel verbatim.  No behavior change.
2026-08-13 10:20:12 +10:00
Andrew Tridgell e76b927378 vfs: move the symlink/readlink family into vfs/symlink.c
Relocate do_symlink/do_symlink_at/do_symlink_atfd and do_readlink/
do_readlink_atfd out of syscall.c into vfs/symlink.c as the vfs_* names.
The fake-super (NO_SYMLINK_*XATTRS) placeholder handling travels
verbatim.  vfs_readlink stays a function only in fake-super builds and a
macro -> readlink() otherwise (the rsync.h macro is renamed to match);
its vfs.h declaration is guarded accordingly.  No behavior change.
2026-08-13 10:20:12 +10:00
Andrew Tridgell 07dd81b17b vfs: move the chmod family into vfs/chmod.c
Relocate do_chmod/do_chmod_at/do_chmod_atfd (and the leaf-safe
do_fchmodat_nofollow helper) out of syscall.c into vfs/chmod.c as the
vfs_* names, declared in vfs/vfs.h.  The HAVE_CHMOD / HAVE_LCHMOD /
HAVE_SETATTRLIST / SYS_fchmodat2 platform guards travel verbatim.
Function bodies unchanged; no behavior change.

(Portability-sensitive family; wants a fleettest with the rest of the
#ifdef-heavy moves.)
2026-08-13 10:20:12 +10:00
Andrew Tridgell e84b935d3b vfs: move the open family into vfs/open.c
Relocate do_open/do_open_at/do_open_atfd/do_open_nofollow/
do_open_checklinks out of syscall.c into vfs/open.c as the vfs_* names,
declared in vfs/vfs.h.  Function bodies unchanged; no behavior change.
2026-08-13 10:20:12 +10:00
Andrew Tridgell 7f03474b2b vfs: move the unlink/rmdir family into vfs/unlink.c
Relocate do_unlink/do_unlink_at/do_unlink_atfd and do_rmdir/do_rmdir_at
out of syscall.c into vfs/unlink.c as the vfs_* names, declared in
vfs/vfs.h.  Function bodies unchanged; no behavior change.
2026-08-13 10:20:12 +10:00
Andrew Tridgell 02ea67ab6c vfs: move the rename family into vfs/rename.c
Relocate do_rename / do_rename_at / do_rename_atfd out of syscall.c into
vfs/rename.c as vfs_rename / vfs_rename_at / vfs_rename_atfd, declared in
vfs/vfs.h.  Function bodies unchanged.

Also centralize the option-global externs (dry_run, am_root, am_sender,
inplace, preserve_*, open_noatime, copy_*, insecure_links, module_id, …)
in vfs/vfs_internal.h, replacing syscall.c's local extern block, so each
relocated family picks them up from one place.  No behavior change.
2026-08-13 10:20:12 +10:00
Andrew Tridgell ad5c09ad9e vfs: move the stat family into vfs/stat.c
Relocate the stat/lstat/fstat wrappers out of syscall.c into vfs/stat.c
with the vfs_* names:

  do_stat       -> vfs_stat        do_stat_at   -> vfs_stat_at
  do_lstat      -> vfs_lstat       do_lstat_at  -> vfs_lstat_at
  do_fstat      -> vfs_fstat       do_stat_atfd -> vfs_stat_atfd
                                   do_lstat_atfd-> vfs_lstat_atfd

do_xstat_at stays a file-local helper.  The x_stat/x_lstat/x_fstat
fallback macros in rsync.h now expand to the vfs_* names.  First Phase-3
family move, so the shared RETURN_ERROR_IF* dry-run/read-only guard
macros (and the read_only/list_only externs they expand to) move from
syscall.c into vfs/vfs_internal.h where every vfs/ source can use them.
Function bodies unchanged; no behavior change.
2026-08-13 10:20:12 +10:00
Andrew Tridgell 48250edd8d vfs: fold operator_path_resolve into struct vfs
Move the operator-path resolver-mode flag into vfs.operator_path_resolve.
Its definition leaves vfs/owner_walk.c, the extern declarations (in
backup/generator/receiver/util1 and vfs_internal.h) are dropped, and the
uses across the do_*_at wrappers and the vfs/ internals are updated to
the struct field directly.  This completes moving the scattered VFS state
(dirfd cache, curr_dir, operator_path_resolve) into struct vfs.
No behavior change.
2026-08-13 10:20:12 +10:00
Andrew Tridgell 540245fa61 vfs: fold curr_dir/curr_dir_len into struct vfs
Move the logical-cwd globals into vfs.curr_dir / vfs.curr_dir_len.  The
definition leaves syscall.c, the per-file `extern char curr_dir[]` /
`extern unsigned int curr_dir_len` declarations are dropped (the struct
is reached via the vfs.h `extern struct vfs vfs`), and the uses across
exclude/flist/log/main/util1 and vfs/secure_open are updated directly --
no compatibility alias macros.  change_dir() writes vfs.curr_dir.  The
separate curr_dir_depth global is untouched.  No behavior change.
2026-08-13 10:20:12 +10:00
Andrew Tridgell a76ef875aa vfs: fold the dirfd cache statics into struct vfs
Replace the file-local dpc_* statics in vfs/dircache.c (anchor, base,
fd[], name[][], depth) with the vfs.dpc fields already declared in
struct vfs and initialized by the designated initializer in vfs/vfs.c.
Pure encapsulation: the cache logic is unchanged, DPC_MAXDEPTH becomes
the shared VFS_DPC_MAXDEPTH, and vfs_dircache_reset() now resets vfs.dpc
(consistent with vfs_init()).  No behavior change.
2026-08-13 10:20:12 +10:00
Andrew Tridgell db6c5ac6f1 vfs: tidy comments and drop unused externs after the core move
Post-relocation cleanup (no behavior change):
 - fix a doubled-prefix typo "vfs_vfs_owner_walk_parent" in the
   vfs/owner_walk.c header comment
 - vfs/vfs.c comment now points at vfs/dircache.c (not syscall.c) for the
   live dpc_* cache
 - drop unused extern decls left from assembling the moved files
   (am_sender/insecure_links in owner_walk.c, curr_dir/curr_dir_len in
   dircache.c)
2026-08-13 10:20:12 +10:00
Andrew Tridgell a5f52ee62e vfs: move the held-dirfd cache into vfs/dircache.c
Relocate the persistent ancestor-dirfd cache out of syscall.c into
vfs/dircache.c with the vfs_* public names:

  open_dir_secure    -> vfs_opendir
  get_dir_fd         -> vfs_get_dirfd
  held_dir_path_fd   -> vfs_path_dirfd
  held_dfd_for       -> vfs_cached_dirfd
  reset_dir_fd_cache -> vfs_dircache_reset

The dpc_* cache statics and dpc_dir_fd stay file-local (they fold into
the vfs struct in a later commit).  Function bodies are unchanged; the
callers in generator/receiver/sender/delete/util1/rsync are updated and
the five entry points declared in vfs/vfs.h.  This completes moving the
security core (resolver, owner-walk, dirfd cache) out of syscall.c.
No behavior change.
2026-08-13 10:20:12 +10:00
Andrew Tridgell b5d30f926b vfs: move the operator-path ownership walk into vfs/owner_walk.c
Relocate the operator-supplied-path resolver out of syscall.c into
vfs/owner_walk.c with the vfs_* public names:

  open_no_attacker_symlinks -> vfs_open_owner_walk
  owner_walk_parent         -> vfs_owner_walk_parent

The static helpers ona_open and abspath_step move with them, as does the
operator_path_resolve flag definition (commit-8 will fold it into the vfs
struct).  Function bodies are unchanged; the call sites across the daemon
and option-parsing files are updated and the two entry points declared in
vfs/vfs.h.

With the owner-walk gone, syscall.c no longer references am_daemon, so
the now-dead `extern int am_daemon` declarations (the do_*_at wrappers
delegate to vfs_relpath_active) are dropped.  No behavior change.
2026-08-13 10:20:12 +10:00
Andrew Tridgell e1b4784a08 vfs: move the secure path resolver into vfs/secure_open.c
Relocate the race-safe resolver and its policy gates out of syscall.c
into vfs/secure_open.c, and give them the vfs_* public names that the
mainline code will use going forward:

  secure_relative_open    -> vfs_resolve_open
  secure_relative_open_at -> vfs_resolve_open_at
  secure_relpath_active   -> vfs_relpath_active
  symlink_optout_allowed  -> vfs_symlink_optout_allowed

secure_walk_at stays file-local.  The function bodies are unchanged; the
call sites across receiver/sender/generator/flist/util1/clientserver/
main/options and the test harnesses are updated to the new names, and
the four entry points are declared in vfs/vfs.h.

The resolver's only consumer of am_chrooted in syscall.c left with it,
so the now-dead `am_chrooted` is dropped from the do_*_at wrappers'
local externs (and the file-scope extern), which is otherwise unused.
No behavior change.
2026-08-13 10:20:12 +10:00
Andrew Tridgell 366bbf68f0 vfs: move the dirstack path-walk primitives into vfs/dirstack.c
First step of relocating the security core.  The component-walk dirfd
stack (struct dirstack + the ds_* helpers) and the module-confinement
helpers (path_has_dotdot_component, abspath_excluded_by_module,
open_anchor_dirfd) move verbatim out of syscall.c into vfs/dirstack.c.
The secure resolver (still in syscall.c) and the held-dirfd cache reach
them through a new private header vfs/vfs_internal.h.

The functions are byte-identical to before; only their linkage changes
(the ones syscall.c still calls become non-static; ds_path_push/
ds_path_pop/ds_push stay file-local).  struct dirstack and
SECURE_OPEN_MAXSYMLINKS now live in vfs_internal.h so both sides see one
definition.  vfs_internal.h also centralizes the option/daemon externs
the VFS internals read.  No behavior change.
2026-08-13 10:20:12 +10:00
Andrew Tridgell 1e4a52e6b4 vfs: scaffold the virtual-filesystem layer
Introduce a vfs/ subtree that will house rsync's filesystem-handling code
(the do_* syscall wrappers, the race-safe path resolver, the held-dirfd
cache, the operator-path ownership walk and daemon module confinement),
separating those security-critical details from the protocol/transfer
logic.  This first commit only stands up the layer; no code is moved yet,
so behavior is identical.

 - vfs/vfs.h: public interface, included by rsync.h just after proto.h.
   Declares "struct vfs" -- the single global that will hold the state
   currently scattered across syscall.c statics (the dirfd cache, curr_dir,
   operator_path_resolve) and the clientserver.c module_* globals -- plus
   vfs_init().
 - vfs/vfs.c: defines the global instance with a designated initializer so
   the cache and module snapshot are safe by construction (a plain
   definition would zero base/module_dirfd, making fd 0 look valid); the
   t_*_secure harnesses never run main(), so this must not rely on
   vfs_init().  vfs_init() resets between transfers (inert for now).
 - Build: bundle the VFS into a static libvfs.a linked last on rsync and
   every test harness, so later commits can move code out of syscall.o
   without breaking a harness link (the linker pulls only what it needs).
   AC_CHECK_TOOL(AR)/AC_PROG_RANLIB added for portable archiving; a
   vfs/dummy config-file output creates vfs/ in VPATH builds.
 - main.c calls vfs_init() early (establishes the call site).
2026-08-13 10:20:12 +10:00
65 changed files with 5818 additions and 4987 deletions

No files matched your search

+1 -1
View File
@@ -57,7 +57,7 @@ jobs:
CC=clang \
CFLAGS="-fsanitize=address,undefined -fno-sanitize-recover=undefined -fno-omit-frame-pointer -g -O1 -DNDEBUG" \
LDFLAGS="-fsanitize=address,undefined" \
./configure --with-rrsync --disable-md2man
./configure --with-rrsync --disable-md2man --enable-strict-confinement
- name: make
# check-progs builds rsync plus the test helper programs (tls, trimslash,
# t_unsafe, ...) that runtests.py requires; plain "make" builds only rsync
+8 -8
View File
@@ -43,19 +43,19 @@ aclocal.m4
/testrun
/trimslash
/t_unsafe
/wildtest
/getfsdev
/simdtest
/t_acl
/t_chmod_secure
/t_clean_fname
/t_hashtable_overflow
/t_iwildmatch
/t_rename_secure
/t_safe_arg
/t_safe_arg_main
/t_secure_relpath
/t_symlink_secure
/t_hashtable_overflow
/t_iwildmatch
/t_clean_fname
/t_safe_arg
/simdtest
/wildtest
/getfsdev
/t_safe_arg_main
/rounding.h
/doc/rsync.pdf
/doc/rsync.ps
+44 -19
View File
@@ -18,6 +18,9 @@ CXXFLAGS=@CXXFLAGS@
EXEEXT=@EXEEXT@
LDFLAGS=@LDFLAGS@
LIBOBJDIR=lib/
AR=@AR@
ARFLAGS=cr
RANLIB=@RANLIB@
INSTALLCMD=@INSTALL@
INSTALLMAN=@INSTALL@
@@ -38,13 +41,13 @@ GENFILES=configure.sh aclocal.m4 config.h.in rsync.1 rsync.1.html \
rsync-ssl.1 rsync-ssl.1.html rsyncd.conf.5 rsyncd.conf.5.html \
@GEN_RRSYNC@
HEADERS=byteorder.h config.h errcode.h proto.h rsync.h ifuncs.h itypes.h inums.h \
lib/pool_alloc.h lib/mdigest.h lib/md-defines.h
lib/pool_alloc.h lib/mdigest.h lib/md-defines.h vfs/vfs.h
LIBOBJ=lib/wildmatch.o lib/compat.o lib/snprintf.o lib/mdfour.o lib/md5.o \
lib/permstring.o lib/pool_alloc.o lib/sysacls.o lib/sysxattrs.o lib/acl.o @LIBOBJS@
zlib_OBJS=zlib/deflate.o zlib/inffast.o zlib/inflate.o zlib/inftrees.o \
zlib/trees.o zlib/zutil.o zlib/adler32.o zlib/compress.o zlib/crc32.o
OBJS1_NO_MAIN=flist.o rsync.o generator.o receiver.o cleanup.o sender.o exclude.o \
util1.o util2.o checksum.o match.o syscall.o log.o backup.o delete.o
util1.o util2.o checksum.o match.o log.o backup.o delete.o
OBJS1=$(OBJS1_NO_MAIN) main.o
OBJS2=options.o io.o compat.o hlink.o token.o uidlist.o socket.o hashtable.o \
usage.o fileio.o batch.o clientname.o chmod.o acls.o xattrs.o
@@ -52,9 +55,10 @@ OBJS3=progress.o pipe.o @MD5_ASM@ @ROLL_SIMD@ @ROLL_ASM@
DAEMON_OBJ = params.o loadparm.o clientserver.o access.o connection.o authenticate.o
popt_OBJS= popt/popt.o popt/poptconfig.o \
popt/popthelp.o popt/poptparse.o popt/poptint.o
OBJS=$(OBJS1) $(OBJS2) $(OBJS3) $(DAEMON_OBJ) $(LIBOBJ) @BUILD_ZLIB@ @BUILD_POPT@
VFS_OBJ=vfs/vfs.o vfs/dirstack.o vfs/secure_open.o vfs/owner_walk.o vfs/dircache.o vfs/stat.o vfs/rename.o vfs/unlink.o vfs/open.o vfs/chmod.o vfs/symlink.o vfs/link.o vfs/mkdir.o vfs/chown.o vfs/mknod.o vfs/times.o vfs/fileio.o vfs/make_path.o vfs/copy_file.o vfs/robust.o
OBJS=$(OBJS1) $(OBJS2) $(OBJS3) $(DAEMON_OBJ) $(LIBOBJ) @BUILD_ZLIB@ @BUILD_POPT@ libvfs.a
TLS_OBJ = tls.o syscall.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/permstring.o lib/sysxattrs.o @BUILD_POPT@
TLS_OBJ = tls.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/permstring.o lib/sysxattrs.o @BUILD_POPT@ libvfs.a
# Programs we must have to run the test cases
CHECK_PROGS = rsync$(EXEEXT) tls$(EXEEXT) getgroups$(EXEEXT) getfsdev$(EXEEXT) \
@@ -67,7 +71,7 @@ CHECK_SYMLINKS = testsuite/chown-fake_test.py testsuite/devices-fake_test.py \
# Objects for CHECK_PROGS to clean
CHECK_OBJS=tls.o testrun.o getgroups.o getfsdev.o t_stub.o t_unsafe.o t_chmod_secure.o t_rename_secure.o t_symlink_secure.o t_secure_relpath.o t_acl.o t_hashtable_overflow.o t_iwildmatch.o t_clean_fname.o t_safe_arg.o trimslash.o wildtest.o
# Compile-only feature-shape checks.
CHECK_COMPILE_OBJS=syscall-no-at-fdcwd.o
CHECK_COMPILE_OBJS=vfs-no-at-fdcwd.o
# note that the -I. is needed to handle config.h when using VPATH
.c.o:
@@ -80,9 +84,20 @@ CHECK_COMPILE_OBJS=syscall-no-at-fdcwd.o
all: Makefile rsync$(EXEEXT) stunnel-rsyncd.conf @MAKE_RRSYNC@ @MAKE_MAN@
.PHONY: all
syscall-no-at-fdcwd.o: syscall.c $(HEADERS)
$(CC) -I. -I$(srcdir) $(CFLAGS) $(CPPFLAGS) \
-DRSYNC_TEST_NO_AT_FDCWD -c $(srcdir)/syscall.c -o $@
# Compile-check the pre-*at() portability tier. syscall.c's *at wrappers were
# split into vfs/, so compile every vfs source with the AT_FDCWD primitives
# undefined (via vfs/vfs_internal.h's RSYNC_TEST_NO_AT_FDCWD block) and confirm
# the fallback arms still build. A shell loop keeps this portable (BSD/Solaris
# make have no pattern rules); the last object compiled is left as the target.
# $(VFS_OBJ:.o=.c) is POSIX suffix substitution, portable across makes.
vfs-no-at-fdcwd.o: $(VFS_OBJ:.o=.c) $(HEADERS) vfs/vfs.h vfs/vfs_internal.h
@rm -f $@ $@.tmp
@for f in $(VFS_OBJ:.o=.c); do \
echo " no-AT_FDCWD compile-check: $$f"; \
$(CC) -I. -I$(srcdir) $(CFLAGS) $(CPPFLAGS) \
-DRSYNC_TEST_NO_AT_FDCWD -c $(srcdir)/$$f -o $@.tmp || exit 1; \
done
@mv $@.tmp $@
.PHONY: install
install: all
@@ -141,7 +156,17 @@ rrsync: support/rrsync
$(OBJS): $(HEADERS)
$(CHECK_OBJS): $(HEADERS)
$(VFS_OBJ): $(HEADERS)
$(VFS_OBJ): vfs/vfs_internal.h
tls.o xattrs.o: lib/sysxattrs.h
# The VFS layer is bundled into a static archive linked last on every target so
# that moving a filesystem family between files never breaks a test harness link
# (the linker pulls only the members each program references).
libvfs.a: $(VFS_OBJ)
rm -f $@
$(AR) $(ARFLAGS) $@ $(VFS_OBJ)
$(RANLIB) $@
usage.o: version.h latest-year.h help-rsync.h help-rsyncd.h git-version.h default-cvsignore.h
loadparm.o: default-dont-compress.h daemon-parm.h
@@ -202,15 +227,15 @@ getgroups$(EXEEXT): getgroups.o
getfsdev$(EXEEXT): getfsdev.o
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ getfsdev.o $(LIBS)
TRIMSLASH_OBJ = trimslash.o syscall.o util2.o t_stub.o lib/compat.o lib/snprintf.o
TRIMSLASH_OBJ = trimslash.o util2.o t_stub.o lib/compat.o lib/snprintf.o libvfs.a
trimslash$(EXEEXT): $(TRIMSLASH_OBJ)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(TRIMSLASH_OBJ) $(LIBS)
T_UNSAFE_OBJ = t_unsafe.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o
T_UNSAFE_OBJ = t_unsafe.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o libvfs.a
t_unsafe$(EXEEXT): $(T_UNSAFE_OBJ)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_UNSAFE_OBJ) $(LIBS)
T_HASHTABLE_OVERFLOW_OBJ = t_hashtable_overflow.o hashtable.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o
T_HASHTABLE_OVERFLOW_OBJ = t_hashtable_overflow.o hashtable.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o libvfs.a
t_hashtable_overflow$(EXEEXT): $(T_HASHTABLE_OVERFLOW_OBJ)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_HASHTABLE_OVERFLOW_OBJ) $(LIBS)
@@ -218,7 +243,7 @@ T_IWILDMATCH_OBJ = t_iwildmatch.o lib/wildmatch.o
t_iwildmatch$(EXEEXT): $(T_IWILDMATCH_OBJ)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_IWILDMATCH_OBJ) $(LIBS)
T_CLEAN_FNAME_OBJ = t_clean_fname.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o
T_CLEAN_FNAME_OBJ = t_clean_fname.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o libvfs.a
t_clean_fname$(EXEEXT): $(T_CLEAN_FNAME_OBJ)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_CLEAN_FNAME_OBJ) $(LIBS)
@@ -231,23 +256,23 @@ t_clean_fname$(EXEEXT): $(T_CLEAN_FNAME_OBJ)
# GNU-make-only; BSD and Solaris make expand it to nothing.
t_safe_arg_main.o: main.c $(HEADERS)
$(CC) -I. -I$(srcdir) $(CFLAGS) $(CPPFLAGS) -Dmain=rsync_unused_main -c $(srcdir)/main.c -o t_safe_arg_main.o
T_SAFE_ARG_OBJ = t_safe_arg.o t_safe_arg_main.o $(OBJS1_NO_MAIN) $(OBJS2) $(OBJS3) $(DAEMON_OBJ) $(LIBOBJ) @BUILD_ZLIB@ @BUILD_POPT@
T_SAFE_ARG_OBJ = t_safe_arg.o t_safe_arg_main.o $(OBJS1_NO_MAIN) $(OBJS2) $(OBJS3) $(DAEMON_OBJ) $(LIBOBJ) @BUILD_ZLIB@ @BUILD_POPT@ libvfs.a
t_safe_arg$(EXEEXT): $(T_SAFE_ARG_OBJ)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_SAFE_ARG_OBJ) $(LIBS)
T_CHMOD_SECURE_OBJ = t_chmod_secure.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o
T_CHMOD_SECURE_OBJ = t_chmod_secure.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o libvfs.a
t_chmod_secure$(EXEEXT): $(T_CHMOD_SECURE_OBJ)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_CHMOD_SECURE_OBJ) $(LIBS)
T_RENAME_SECURE_OBJ = t_rename_secure.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o
T_RENAME_SECURE_OBJ = t_rename_secure.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o libvfs.a
t_rename_secure$(EXEEXT): $(T_RENAME_SECURE_OBJ)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_RENAME_SECURE_OBJ) $(LIBS)
T_SYMLINK_SECURE_OBJ = t_symlink_secure.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o
T_SYMLINK_SECURE_OBJ = t_symlink_secure.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o libvfs.a
t_symlink_secure$(EXEEXT): $(T_SYMLINK_SECURE_OBJ)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_SYMLINK_SECURE_OBJ) $(LIBS)
T_SECURE_RELPATH_OBJ = t_secure_relpath.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o
T_SECURE_RELPATH_OBJ = t_secure_relpath.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o libvfs.a
t_secure_relpath$(EXEEXT): $(T_SECURE_RELPATH_OBJ)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_SECURE_RELPATH_OBJ) $(LIBS)
@@ -348,10 +373,10 @@ rrsync.1: support/rrsync.1.md md-convert Makefile
.PHONY: clean
clean: cleantests
rm -f *~ $(OBJS) $(CHECK_PROGS) $(CHECK_OBJS) $(CHECK_COMPILE_OBJS) $(CHECK_SYMLINKS) @MAKE_RRSYNC@ \
rm -f *~ $(OBJS) $(VFS_OBJ) libvfs.a $(CHECK_PROGS) $(CHECK_OBJS) $(CHECK_COMPILE_OBJS) $(CHECK_COMPILE_OBJS:.o=.o.tmp) $(CHECK_SYMLINKS) @MAKE_RRSYNC@ \
git-version.h rounding rounding.h *.old rsync*.1 rsync*.5 @MAKE_RRSYNC_1@ \
*.html daemon-parm.h help-*.h default-*.h proto.h proto.h-tstamp
rm -f *.gcno *.gcda lib/*.gcno lib/*.gcda zlib/*.gcno zlib/*.gcda popt/*.gcno popt/*.gcda
rm -f *.gcno *.gcda lib/*.gcno lib/*.gcda zlib/*.gcno zlib/*.gcda popt/*.gcno popt/*.gcda vfs/*.gcno vfs/*.gcda
rm -rf coverage coverage-tcp coverage-all coverage-fallback
.PHONY: cleantests
+2 -2
View File
@@ -1213,7 +1213,7 @@ static int set_rsync_acl(int fd, int dirfd, const char *leaf, const char *fname,
* the legacy path fallback (op_pin am_root != 0 rule). */
if (fd >= 0)
rc = sys_acl_delete_def_fd(fd);
else if (secure_relpath_active() && am_root) {
else if (vfs_relpath_active() && am_root) {
errno = ELOOP;
rc = -1;
} else
@@ -1334,7 +1334,7 @@ static int set_rsync_acl(int fd, int dirfd, const char *leaf, const char *fname,
sxp->st.st_mode = cur_mode;
return 0;
}
if (secure_relpath_active() && am_root) {
if (vfs_relpath_active() && am_root) {
/* Real root always can open its own freshly-staged reg/dir/fifo leaf,
* so a missing held fd on a confined receiver means the leaf was raced
* to a symlink; sys_acl_set_file() follows the leaf, so refuse rather
+5 -5
View File
@@ -156,7 +156,7 @@ static const char *check_secret(int module, const char *user, const char *group,
if (!fname || !*fname)
return "no secrets file";
{
int fd = open_no_attacker_symlinks(fname, O_RDONLY, 0);
int fd = vfs_open_owner_walk(fname, O_RDONLY, 0, 0);
if (fd < 0)
return "no secrets file";
fh = fdopen(fd, "r");
@@ -166,7 +166,7 @@ static const char *check_secret(int module, const char *user, const char *group,
}
}
if (do_fstat(fileno(fh), &st) == -1) {
if (vfs_fstat(fileno(fh), &st) == -1) {
rsyserr(FLOG, errno, "fstat(%s)", fname);
ok = 0;
} else if (lp_strict_modes(module)) {
@@ -239,10 +239,10 @@ static const char *getpassf(const char *filename)
/* --password-file=PATH client open. Its first line is sent as the
* auth response, so a planted symlink leaks the target's content
* (e.g. shadow hashes) to a malicious daemon; the do_stat()
* (e.g. shadow hashes) to a malicious daemon; the vfs_stat()
* other-access check runs on the target mode and passes 0640
* root:shadow. Refuse symlinks not owned by uid 0 or our euid. */
if ((fd = open_no_attacker_symlinks(filename, O_RDONLY, 0)) < 0) {
if ((fd = vfs_open_owner_walk(filename, O_RDONLY, 0, 0)) < 0) {
rsyserr(FERROR, errno, "could not open password file %s", filename);
exit_cleanup(RERR_SYNTAX);
}
@@ -252,7 +252,7 @@ static const char *getpassf(const char *filename)
* path between open and check can't make the owner/mode test
* validate a different inode than the one we read the password
* from. */
if (do_fstat(fd, &st) == -1) {
if (vfs_fstat(fd, &st) == -1) {
rsyserr(FERROR, errno, "fstat(%s)", filename);
exit_cleanup(RERR_SYNTAX);
}
+24 -27
View File
@@ -30,7 +30,6 @@ extern int preserve_links;
extern int safe_symlinks;
extern int backup_dir_len;
extern unsigned int backup_dir_remainder;
extern int operator_path_resolve;
extern char backup_dir_buf[MAXPATHLEN];
extern char *backup_suffix;
extern char *backup_dir;
@@ -44,19 +43,14 @@ extern char *backup_dir;
* backup_metadata_hardened() to tell the two -1 cases apart). */
int backup_metadata_hardened(void)
{
return secure_relpath_active() && !symlink_optout_allowed();
return vfs_relpath_active() && !vfs_symlink_optout_allowed();
}
int backup_source_fd(const char *path)
{
#if defined AT_FDCWD && defined O_NOFOLLOW
if (backup_metadata_hardened() && path && *path) {
int save = operator_path_resolve, fd;
operator_path_resolve = 1;
fd = do_open_at(path, O_RDONLY | O_NONBLOCK | O_NOCTTY | O_CLOEXEC, 0);
operator_path_resolve = save;
return fd;
}
if (backup_metadata_hardened() && path && *path)
return vfs_open_at(path, O_RDONLY | O_NONBLOCK | O_NOCTTY | O_CLOEXEC, 0, VFS_OPERATOR_PATH);
#endif
return -1;
}
@@ -66,7 +60,7 @@ static int validate_backup_dir(void)
{
STRUCT_STAT st;
if (do_lstat_at(backup_dir_buf, &st) < 0) {
if (vfs_lstat(VFS_AT_FDCWD, backup_dir_buf, &st, VFS_OPERATOR_PATH) < 0) {
if (errno == ENOENT)
return 0;
rsyserr(FERROR, errno, "backup lstat %s failed", backup_dir_buf);
@@ -125,7 +119,7 @@ static BOOL copy_valid_path(const char *fname)
for ( ; b; name = b + 1, b = strchr(name, '/')) {
*b = '\0';
while (do_mkdir_at(backup_dir_buf, ACCESSPERMS) < 0) {
while (vfs_mkdir(VFS_AT_FDCWD, backup_dir_buf, ACCESSPERMS, VFS_OPERATOR_PATH) < 0) {
if (errno == EEXIST) {
val = validate_backup_dir();
if (val > 0)
@@ -141,7 +135,7 @@ static BOOL copy_valid_path(const char *fname)
/* Try to transfer the directory settings of the actual dir
* that the files are coming from. */
if (x_stat(rel, &sx.st, NULL) < 0)
if (x_stat(rel, &sx.st, NULL, VFS_OPERATOR_PATH) < 0)
rsyserr(FERROR, errno, "backup stat %s failed", full_fname(rel));
else {
struct file_struct *file;
@@ -170,7 +164,7 @@ static BOOL copy_valid_path(const char *fname)
close(bfd);
}
#endif
set_file_attrs(backup_dir_buf, file, NULL, NULL, 0);
set_file_attrs(backup_dir_buf, file, NULL, NULL, ATTRS_OPERATOR_PATH);
unmake_file(file);
}
@@ -203,7 +197,7 @@ char *get_backup_name(const char *fname)
}
if (backup_dir_len > 1)
dirbuf[backup_dir_len-1] = '\0';
ret = make_path(dirbuf, 0);
ret = vfs_make_path(dirbuf, 0, VFS_OPERATOR_PATH);
if (ret < 0)
return NULL;
initialized = 1;
@@ -236,7 +230,11 @@ static inline int link_or_rename(const char *from, const char *to,
if (IS_SPECIAL(stp->st_mode) || IS_DEVICE(stp->st_mode))
return 0; /* Use copy code. */
#endif
if (do_link_at(from, to) == 0) {
/* from = the live dest file being backed up (a transfer path); to = the
* --backup-dir path (operator). Per-operand policy keeps the transfer
* source under the secure receiver resolve and only owner-walks the
* operator backup parent. */
if (vfs_link_at(from, to, 0, VFS_OPERATOR_PATH) == 0) {
if (DEBUG_GTE(BACKUP, 1))
rprintf(FINFO, "make_backup: HLINK %s successful.\n", from);
return 2;
@@ -246,11 +244,12 @@ static inline int link_or_rename(const char *from, const char *to,
return 0;
}
#endif
if (do_rename_at(from, to) == 0) {
if (vfs_rename_at(from, to, 0, VFS_OPERATOR_PATH) == 0) {
if (stp->st_nlink > 1 && !S_ISDIR(stp->st_mode)) {
/* If someone has hard-linked the file into the backup
* dir, rename() might return success but do nothing! */
robust_unlink(from); /* Just in case... */
* dir, rename() might return success but do nothing! from is the
* transfer-side source, so unlink it under the secure resolve (0). */
robust_unlink(from, 0); /* Just in case... */
}
if (DEBUG_GTE(BACKUP, 1))
rprintf(FINFO, "make_backup: RENAME %s successful.\n", from);
@@ -272,7 +271,7 @@ static int make_backup_inner(const char *fname, BOOL prefer_rename)
init_stat_x(&sx);
/* Return success if no file to keep. */
if (x_lstat(fname, &sx.st, NULL) < 0)
if (x_lstat(fname, &sx.st, NULL, VFS_OPERATOR_PATH) < 0)
return 3;
if (!(buf = get_backup_name(fname)))
@@ -288,7 +287,7 @@ static int make_backup_inner(const char *fname, BOOL prefer_rename)
* unsafe symlink. */
if (preserve_links && S_ISLNK(sx.st.st_mode) && safe_symlinks) {
char lnkbuf[MAXPATHLEN];
int llen = do_readlink(fname, lnkbuf, MAXPATHLEN - 1);
int llen = vfs_readlink(fname, lnkbuf, MAXPATHLEN - 1);
/* A failed readlink means we can't verify the target, so fail
* closed: skip the backup rather than let the hard-link fast path
* preserve a possibly-unsafe symlink unchecked. */
@@ -317,7 +316,7 @@ static int make_backup_inner(const char *fname, BOOL prefer_rename)
goto success;
if (errno == EEXIST || errno == EISDIR) {
STRUCT_STAT bakst;
if (do_lstat_at(buf, &bakst) == 0) {
if (vfs_lstat(VFS_AT_FDCWD, buf, &bakst, VFS_OPERATOR_PATH) == 0) {
int flags = get_del_for_flag(bakst.st_mode) | DEL_FOR_BACKUP | DEL_RECURSE;
if (delete_item(buf, bakst.st_mode, flags) != 0)
return 0;
@@ -357,7 +356,7 @@ static int make_backup_inner(const char *fname, BOOL prefer_rename)
/* Check to see if this is a device file, or link */
if ((am_root && preserve_devices && IS_DEVICE(file->mode))
|| (preserve_specials && IS_SPECIAL(file->mode))) {
if (do_mknod_at(buf, file->mode, sx.st.st_rdev) < 0)
if (vfs_mknod(VFS_AT_FDCWD, buf, file->mode, sx.st.st_rdev, VFS_OPERATOR_PATH) < 0)
rsyserr(FERROR, errno, "mknod %s failed", full_fname(buf));
else if (DEBUG_GTE(BACKUP, 1))
rprintf(FINFO, "make_backup: DEVICE %s successful.\n", fname);
@@ -374,7 +373,7 @@ static int make_backup_inner(const char *fname, BOOL prefer_rename)
}
ret = 2;
} else {
if (do_symlink_at(sl, buf) < 0)
if (vfs_symlink(sl, VFS_AT_FDCWD, buf, VFS_OPERATOR_PATH) < 0)
rsyserr(FERROR, errno, "link %s -> \"%s\"", full_fname(buf), sl);
else if (DEBUG_GTE(BACKUP, 1))
rprintf(FINFO, "make_backup: SYMLINK %s successful.\n", fname);
@@ -398,7 +397,7 @@ static int make_backup_inner(const char *fname, BOOL prefer_rename)
/* Copy to backup tree if a file. */
if (!ret) {
if (copy_file(fname, buf, -1, file->mode) < 0) {
if (copy_file(fname, buf, -1, file->mode, VFS_OPERATOR_PATH) < 0) {
rsyserr(FERROR, errno, "keep_backup failed: %s -> \"%s\"",
full_fname(fname), buf);
unmake_file(file);
@@ -417,7 +416,7 @@ static int make_backup_inner(const char *fname, BOOL prefer_rename)
save_preserve_xattrs = preserve_xattrs;
preserve_xattrs = 0;
set_file_attrs(buf, file, NULL, fname, ATTRS_ACCURATE_TIME);
set_file_attrs(buf, file, NULL, fname, ATTRS_OPERATOR_PATH | ATTRS_ACCURATE_TIME);
preserve_xattrs = save_preserve_xattrs;
unmake_file(file);
@@ -442,8 +441,6 @@ int make_backup(const char *fname, BOOL prefer_rename)
* symlink component is refused while the operator's own is followed --
* absolute and relative alike. --insecure-links / "insecure links ="
* restores legacy following. */
operator_path_resolve = 1;
ret = make_backup_inner(fname, prefer_rename);
operator_path_resolve = 0;
return ret;
}
+5 -5
View File
@@ -251,7 +251,7 @@ void open_batch_files(void)
stringjoin(filename, sizeof filename, batch_name, ".sh", NULL);
batch_sh_fd = open_no_attacker_symlinks(filename, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, S_IRUSR | S_IWUSR | S_IXUSR);
batch_sh_fd = vfs_open_owner_walk(filename, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, S_IRUSR | S_IWUSR | S_IXUSR, 0);
if (batch_sh_fd < 0) {
rsyserr(FERROR, errno, "Batch file %s open error", full_fname(filename));
exit_cleanup(RERR_FILESELECT);
@@ -259,12 +259,12 @@ void open_batch_files(void)
/* O_BINARY: the batch stream is binary protocol data; without it
* Cygwin et al apply CRLF translation and corrupt it. Unlike
* do_open(), open_no_attacker_symlinks passes flags verbatim. */
batch_fd = open_no_attacker_symlinks(batch_name, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, S_IRUSR | S_IWUSR);
* vfs_open(), vfs_open_owner_walk passes flags verbatim. */
batch_fd = vfs_open_owner_walk(batch_name, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, S_IRUSR | S_IWUSR, 0);
} else if (strcmp(batch_name, "-") == 0)
batch_fd = STDIN_FILENO;
else
batch_fd = open_no_attacker_symlinks(batch_name, O_RDONLY | O_BINARY, S_IRUSR | S_IWUSR);
batch_fd = vfs_open_owner_walk(batch_name, O_RDONLY | O_BINARY, S_IRUSR | S_IWUSR, 0);
if (batch_fd < 0) {
rsyserr(FERROR, errno, "Batch file %s open error", full_fname(batch_name));
@@ -275,7 +275,7 @@ void open_batch_files(void)
* non-regular files (FIFO, device, socket) at the batch path. */
if (!write_batch && batch_fd != STDIN_FILENO) {
STRUCT_STAT st;
if (do_fstat(batch_fd, &st) == 0 && !S_ISREG(st.st_mode)) {
if (vfs_fstat(batch_fd, &st) == 0 && !S_ISREG(st.st_mode)) {
rprintf(FERROR, "Batch file %s is not a regular file\n",
full_fname(batch_name));
exit_cleanup(RERR_FILEIO);
+1 -1
View File
@@ -423,7 +423,7 @@ void file_checksum(const char *fname, const STRUCT_STAT *st_p, char *sum)
int32 remainder;
int fd;
fd = do_open_checklinks(fname);
fd = vfs_open_checklinks(fname);
if (fd == -1) {
memset(sum, 0, file_sum_len);
return;
+2 -2
View File
@@ -58,7 +58,7 @@ void close_all(void)
max_fd = sysconf(_SC_OPEN_MAX) - 1;
for (fd = max_fd; fd >= 0; fd--) {
if ((ret = do_fstat(fd, &st)) == 0) {
if ((ret = vfs_fstat(fd, &st)) == 0) {
if (is_a_socket(fd))
ret = shutdown(fd, 2);
ret = close(fd);
@@ -198,7 +198,7 @@ NORETURN void _exit_cleanup(int code, const char *file, int line)
switch_step++;
if (cleanup_fname)
do_unlink_at(cleanup_fname);
vfs_unlink(VFS_AT_FDCWD, cleanup_fname, 0);
if (exit_code)
kill_all(SIGUSR1);
if (cleanup_pid && cleanup_pid == getpid()) {
+24 -16
View File
@@ -185,7 +185,7 @@ static int exchange_protocols(int f_in, int f_out, char *buf, size_t bufsiz, int
/* 'motd file = PATH': motd content is sent to every client, so
* a planted symlink would leak the target's bytes. Refuse
* symlinks not owned by uid 0 or our euid. */
int motd_fd = open_no_attacker_symlinks(motd, O_RDONLY, 0);
int motd_fd = vfs_open_owner_walk(motd, O_RDONLY, 0, 0);
FILE *f = motd_fd >= 0 ? fdopen(motd_fd, "r") : NULL;
if (!f && motd_fd >= 0) close(motd_fd);
while (f && !feof(f)) {
@@ -300,10 +300,10 @@ int start_inband_exchange(int f_in, int f_out, const char *user, int argc, char
STRUCT_STAT st;
/* --early-input-file=PATH: refuse symlinks not owned by uid 0 or
* our euid anywhere in the path. */
int ei_fd = open_no_attacker_symlinks(early_input_file, O_RDONLY, 0);
int ei_fd = vfs_open_owner_walk(early_input_file, O_RDONLY, 0, 0);
FILE *f = ei_fd >= 0 ? fdopen(ei_fd, "rb") : NULL;
if (!f && ei_fd >= 0) close(ei_fd);
if (!f || do_fstat(fileno(f), &st) < 0) {
if (!f || vfs_fstat(fileno(f), &st) < 0) {
rsyserr(FERROR, errno, "failed to open %s", early_input_file);
if (f)
fclose(f);
@@ -925,6 +925,12 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
} else
set_filter_dir(module_dir, module_dirlen);
/* Snapshot the module root for the VFS confinement checks now that the
* path is final. The root dirfd is pinned later (below); this first call
* must precede any VFS open of an operator-supplied path -- the filter/
* include files just below, and the log file -- so they see the boundary. */
vfs_set_module_root(module_dir, module_dirlen, -1);
/* Everything loaded from here to the end of the exclude block is the
* operator's own configuration, so it keeps the ownership walk without the
* module-confinement parse_filter_file() applies to peer-driven merges. */
@@ -1064,6 +1070,8 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
#if defined HAVE_FDOPENDIR && defined O_DIRECTORY
module_dirfd = open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
#endif
/* Update the VFS snapshot with the now-pinned root dirfd. */
vfs_set_module_root(module_dir, module_dirlen, module_dirfd);
if (module_dirlen)
sanitize_paths = 1;
@@ -1073,7 +1081,7 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
STRUCT_STAT st;
char prefix[SYMLINK_PREFIX_LEN]; /* NOT +1 ! */
strlcpy(prefix, SYMLINK_PREFIX, sizeof prefix); /* trim the trailing slash */
if (do_stat(prefix, &st) == 0 && S_ISDIR(st.st_mode)) {
if (vfs_stat(VFS_AT_FDCWD, prefix, &st, VFS_ALLOW_SYMLINK) == 0 && S_ISDIR(st.st_mode)) {
rprintf(FLOG, "Symlink munging is unsafe when a %s directory exists.\n",
prefix);
io_printf(f_out, "@ERROR: daemon security issue -- contact admin\n", name);
@@ -1087,11 +1095,11 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
* the receiver finish/rename path must still resolve beneath the module
* root. This prevents TOCTOU race attacks where an attacker could switch a
* directory to a symlink between path validation and file open. Match the
* gate in secure_relpath_active() (syscall.c) -- the protection has nothing
* gate in vfs_relpath_active() (syscall.c) -- the protection has nothing
* to do with symlink munging, so a module configured with "munge symlinks =
* false" must still get the secure-open path. */
use_secure_symlinks = am_daemon && (!am_chrooted || module_dirlen)
&& !symlink_optout_allowed();
&& !vfs_symlink_optout_allowed();
if (gid_list.count) {
gid_t *gid_array = gid_list.items;
@@ -1477,7 +1485,7 @@ int start_daemon(int f_in, int f_out)
}
/* Deliberately do NOT set am_chrooted here. am_chrooted
* gates the per-module symlink-race defenses
* (secure_relative_open() and the do_*_at() wrappers in
* (vfs_resolve_open() and the do_*_at() wrappers in
* syscall.c) and means "the kernel is enforcing path
* confinement at the module boundary". The daemon chroot
* confines path resolution to the daemon-chroot directory,
@@ -1486,7 +1494,7 @@ int start_daemon(int f_in, int f_out)
* subtrees and a sender-controlled symlink in module A
* could redirect a syscall to module B (or to other files
* inside the daemon chroot) without the per-module
* defenses. Leave am_chrooted=0 here so secure_relative_open()
* defenses. Leave am_chrooted=0 here so vfs_resolve_open()
* still fires for "use chroot = no" modules. */
if (chdir("/") < 0) {
rsyserr(FLOG, errno, "daemon chdir(\"/\") failed");
@@ -1618,18 +1626,18 @@ static void create_pid_file(void)
dir = dirbuf;
base = slash + 1;
}
if ((pdfd = do_open(dir, O_RDONLY|O_DIRECTORY, 0)) < 0) {
if ((pdfd = vfs_open(dir, O_RDONLY|O_DIRECTORY, 0)) < 0) {
rsyserr(FLOG, errno, "failed to open pid-file directory \"%s\"", dir);
exit_cleanup(RERR_FILEIO);
}
}
#define PID_LSTAT(stp) do_lstat_atfd(pdfd, base, stp)
#define PID_UNLINK() do_unlink_atfd(pdfd, base, 0)
#define PID_OPEN() do_open_atfd(pdfd, base, O_RDWR|O_CREAT, 0664)
#define PID_LSTAT(stp) vfs_lstat(pdfd, base, stp, 0)
#define PID_UNLINK() vfs_unlink(pdfd, base, 0)
#define PID_OPEN() vfs_open_atfd(pdfd, base, O_RDWR|O_CREAT, 0664)
#else
#define PID_LSTAT(stp) do_lstat(base, stp)
#define PID_LSTAT(stp) vfs_lstat(VFS_AT_FDCWD, base, stp, VFS_ALLOW_SYMLINK)
#define PID_UNLINK() unlink(base)
#define PID_OPEN() do_open(base, O_RDWR|O_CREAT|SAFE_NOFOLLOW, 0664)
#define PID_OPEN() vfs_open(base, O_RDWR|O_CREAT|SAFE_NOFOLLOW, 0664)
#endif
/* These tests make sure that a temp-style lock dir is handled safely. */
@@ -1640,7 +1648,7 @@ static void create_pid_file(void)
fail = S_ISREG(st1.st_mode) ? "open" : "create";
else if (!lock_range(pid_file_fd, 0, 4))
fail = "lock";
else if (do_fstat(pid_file_fd, &st1) < 0)
else if (vfs_fstat(pid_file_fd, &st1) < 0)
fail = "fstat opened";
else if (st1.st_size > (int)sizeof pidbuf)
fail = "find small";
@@ -1651,7 +1659,7 @@ static void create_pid_file(void)
else if (st1.st_dev != st2.st_dev || st1.st_ino != st2.st_ino)
fail = "verify stat info for";
#ifdef HAVE_FTRUNCATE
else if (do_ftruncate(pid_file_fd, 0) < 0)
else if (vfs_ftruncate(pid_file_fd, 0) < 0)
fail = "truncate";
#endif
else {
+10 -2
View File
@@ -60,6 +60,8 @@ AC_PROG_AWK
AC_PROG_EGREP
AC_PROG_INSTALL
AC_PROG_MKDIR_P
AC_CHECK_TOOL([AR], [ar], [ar])
AC_PROG_RANLIB
AC_SUBST(SHELL)
AC_PATH_PROG([PERL], [perl])
AC_PATH_PROG([PYTHON3], [python3])
@@ -104,6 +106,12 @@ dnl by default (the knob only REMOVES a tier when explicitly disabled).
AC_ARG_ENABLE(openat2,
AS_HELP_STRING([--disable-openat2],[do not use Linux openat2(RESOLVE_BENEATH); force the portable resolver (for exercising the fallback tier)]))
AC_ARG_ENABLE(strict-confinement,
AS_HELP_STRING([--enable-strict-confinement],[abort if a confined receiver ever does a raw path-based metadata op (a CI/dev hardening check; no effect on a normal build)]))
if test x"$enable_strict_confinement" = x"yes"; then
AC_DEFINE(STRICT_CONFINEMENT, 1, [Define to abort on a confined-regime raw path-based metadata op (CI hardening check)])
fi
AC_MSG_CHECKING([if md2man can create manpages])
if test x"$ac_cv_path_PYTHON3" = x; then
AC_MSG_RESULT(no - python3 not found)
@@ -371,7 +379,7 @@ return SYS_openat2 + (int)how.resolve;
if test x"$enable_openat2" != x"no"; then
if test x"$rsync_cv_HAVE_OPENAT2" = x"yes"; then
AC_DEFINE([HAVE_OPENAT2], 1,
[Define to use Linux openat2(RESOLVE_BENEATH) in secure_relative_open where available.])
[Define to use Linux openat2(RESOLVE_BENEATH) in vfs_resolve_open where available.])
fi
fi
@@ -1542,7 +1550,7 @@ case "$CC" in
;;
esac
AC_CONFIG_FILES([Makefile lib/dummy zlib/dummy popt/dummy shconfig])
AC_CONFIG_FILES([Makefile lib/dummy zlib/dummy popt/dummy vfs/dummy shconfig])
AC_OUTPUT
AC_MSG_RESULT()
+1 -1
View File
@@ -32,7 +32,7 @@ int claim_connection(char *fname, int max_connections)
/* 'lock file = PATH': refuse symlinks not owned by uid 0 or our euid so
* a planted parent can't redirect the root daemon's O_CREAT open. */
if ((fd = open_no_attacker_symlinks(fname, O_RDWR|O_CREAT, 0600)) < 0)
if ((fd = vfs_open_owner_walk(fname, O_RDWR|O_CREAT, 0600, 0)) < 0)
return 0;
/* Find a free spot. */
+16 -9
View File
@@ -63,18 +63,23 @@ static void del_chmod(const char *fbuf, mode_t mode)
const char *leaf;
int dfd = del_held_dfd(fbuf, &leaf);
if (dfd >= 0)
do_chmod_atfd(dfd, leaf, mode);
vfs_chmod(dfd, leaf, mode, 0);
else
do_chmod_at(fbuf, mode);
vfs_chmod(VFS_AT_FDCWD, fbuf, mode, 0);
}
static int del_unlink(const char *fbuf)
/* vfs_flags carries VFS_OPERATOR_PATH for a backup-tree delete (DEL_FOR_BACKUP):
* the path-based fallback then resolves the leaf's parent via the ownership walk,
* matching the confinement the base gives this unlink under make_backup() (where
* the held dirfd is absent for a cross-tree --backup-dir leaf). A held-dirfd
* delete is already confined, so it ignores the flag. */
static int del_unlink(const char *fbuf, int vfs_flags)
{
const char *leaf;
int dfd = del_held_dfd(fbuf, &leaf);
if (dfd >= 0 && do_unlink_atfd(dfd, leaf, 0) == 0)
if (dfd >= 0 && vfs_unlink(dfd, leaf, 0) == 0)
return 0;
return robust_unlink(fbuf); /* fall back (ETXTBSY retry, or not held) */
return robust_unlink(fbuf, vfs_flags); /* fall back (ETXTBSY retry, or not held) */
}
static inline int is_backup_file(char *fn)
@@ -133,7 +138,7 @@ static enum delret delete_dir_contents(char *fname, uint16 flags)
const char *save_del_prefix = del_dir_prefix;
int save_del_prefix_len = del_dir_prefix_len;
fname[dlen] = '\0';
del_dirfd = open_dir_secure(fname);
del_dirfd = vfs_opendir(fname);
fname[dlen] = '/';
del_dir_prefix = fname;
del_dir_prefix_len = dlen;
@@ -223,18 +228,20 @@ enum delret delete_item(char *fbuf, uint16 mode, uint16 flags)
const char *leaf;
int dfd = del_held_dfd(fbuf, &leaf);
what = "rmdir";
ok = (dfd >= 0 ? do_unlink_atfd(dfd, leaf, AT_REMOVEDIR) : do_rmdir_at(fbuf)) == 0;
ok = (dfd >= 0 ? vfs_unlink(dfd, leaf, VFS_REMOVEDIR)
: vfs_unlink(VFS_AT_FDCWD, fbuf,
VFS_REMOVEDIR | ((flags & DEL_FOR_BACKUP) ? VFS_OPERATOR_PATH : 0))) == 0;
} else {
if (make_backups > 0 && !(flags & DEL_FOR_BACKUP) && (backup_dir || !is_backup_file(fbuf))) {
what = "make_backup";
ok = make_backup(fbuf, True);
if (ok == 2) {
what = "unlink";
ok = del_unlink(fbuf) == 0;
ok = del_unlink(fbuf, (flags & DEL_FOR_BACKUP) ? VFS_OPERATOR_PATH : 0) == 0;
}
} else {
what = "unlink";
ok = del_unlink(fbuf) == 0;
ok = del_unlink(fbuf, (flags & DEL_FOR_BACKUP) ? VFS_OPERATOR_PATH : 0) == 0;
}
}
+12 -21
View File
@@ -41,7 +41,6 @@ extern int sanitize_paths;
extern int protocol_version;
extern int trust_sender_args;
extern int module_id;
extern int operator_path_resolve;
/* Set while the daemon loads its own filter parameters; see parse_filter_file(). */
int daemon_config_filter_file = 0;
@@ -136,8 +135,6 @@ static void filter_rule_err(const char *msg, const char *rulestr)
exit_cleanup(RERR_SYNTAX);
}
extern char curr_dir[MAXPATHLEN];
extern unsigned int curr_dir_len;
extern unsigned int module_dirlen;
filter_rule_list filter_list = { .debug_type = "" };
@@ -155,7 +152,7 @@ int trust_sender_filter = 0;
#define SLASH_WILD3_SUFFIX "/***"
/* The dirbuf is set by push_local_filters() to the current subdirectory
* relative to curr_dir that is being processed. The path always has a
* relative to vfs.curr_dir that is being processed. The path always has a
* trailing slash appended, and the variable dirbuf_len contains the length
* of this path prefix. The path is always absolute. */
static char dirbuf[MAXPATHLEN+1];
@@ -757,9 +754,9 @@ void set_filter_dir(const char *dir, unsigned int dirlen)
{
unsigned int len;
if (*dir != '/') {
memcpy(dirbuf, curr_dir, curr_dir_len);
dirbuf[curr_dir_len] = '/';
len = curr_dir_len + 1;
memcpy(dirbuf, vfs.curr_dir, vfs.curr_dir_len);
dirbuf[vfs.curr_dir_len] = '/';
len = vfs.curr_dir_len + 1;
if (len + dirlen >= MAXPATHLEN)
dirlen = 0;
} else
@@ -853,7 +850,7 @@ struct local_filter_state {
/* Each time rsync changes to a new directory it call this function to
* handle all the per-dir merge-files. The "dir" value is the current path
* relative to curr_dir (which might not be null-terminated). We copy it
* relative to vfs.curr_dir (which might not be null-terminated). We copy it
* into dirbuf so that we can easily append a file name on the end. */
void *push_local_filters(const char *dir, unsigned int dirlen)
{
@@ -1020,10 +1017,10 @@ static int rule_matches(const char *fname, filter_rule *ex, int name_flags)
if ((p = strrchr(name,'/')) != NULL)
name = p+1;
} else if (ex->rflags & FILTRULE_ABS_PATH && *fname != '/'
&& curr_dir_len > module_dirlen + 1) {
&& vfs.curr_dir_len > module_dirlen + 1) {
/* If we're matching against an absolute-path pattern,
* we need to prepend our full path info. */
strings[str_cnt++] = curr_dir + module_dirlen + 1;
strings[str_cnt++] = vfs.curr_dir + module_dirlen + 1;
strings[str_cnt++] = "/";
} else if (ex->rflags & FILTRULE_WILD2_PREFIX && *fname != '/') {
/* Allow "**"+"/" to match at the start of the string. */
@@ -1665,24 +1662,18 @@ void parse_filter_file(filter_rule_list *listp, const char *fname, const filter_
open_path = line;
} else
open_path = fname;
/* Confine the open to the module root. The ownership walk on its own
* is not enough for a peer-driven merge file: a non-chrooted daemon
* writes --backup-dir entries as root, so a raced backup symlink is
* ROOT-owned -- exactly what open_no_attacker_symlinks() treats as
* trusted -- and naming it in a dir-merge rule would read an
* out-of-module file in as filter rules (their text comes back to the
* peer in "Unknown filter rule" errors).
* ROOT-owned -- exactly what the ownership walk treats as trusted --
* and naming it in a dir-merge rule would read an out-of-module file
* in as filter rules (their text comes back to the peer in "Unknown
* filter rule" errors).
*
* The daemon's own "filter"/"include from"/"exclude from" parameters
* are exempt: those are operator-configured and legitimately live
* outside the module (/etc/rsync/excludes and the like). */
int save_opr = operator_path_resolve;
if (!daemon_config_filter_file)
operator_path_resolve = 1;
fd = open_no_attacker_symlinks(open_path, O_RDONLY, 0);
operator_path_resolve = save_opr;
fd = vfs_open_owner_walk(open_path, O_RDONLY, 0, !daemon_config_filter_file);
if (fd < 0)
fp = NULL;
else if (!(fp = fdopen(fd, "rb")))
+10 -10
View File
@@ -45,17 +45,17 @@ int sparse_end(int f, OFF_T size, int updating_basis_or_equiv)
int ret = 0;
if (updating_basis_or_equiv) {
if (sparse_seek && do_punch_hole(f, sparse_past_write, sparse_seek) < 0)
if (sparse_seek && vfs_punch_hole(f, sparse_past_write, sparse_seek) < 0)
ret = -1;
#ifdef HAVE_FTRUNCATE /* A compilation formality -- in-place requires ftruncate() */
else /* Just in case the original file was longer */
ret = do_ftruncate(f, size);
ret = vfs_ftruncate(f, size);
#endif
} else if (sparse_seek) {
#ifdef HAVE_FTRUNCATE
ret = do_ftruncate(f, size);
ret = vfs_ftruncate(f, size);
#else
if (do_lseek(f, sparse_seek-1, SEEK_CUR) != size-1)
if (vfs_lseek(f, sparse_seek-1, SEEK_CUR) != size-1)
ret = -1;
else {
do {
@@ -76,17 +76,17 @@ int sparse_end(int f, OFF_T size, int updating_basis_or_equiv)
* the current file position is in the file. The use_seek arg tells
* us that we should seek over matching data instead of writing it. */
/* Flush any deferred run of zero bytes as a hole, advancing the file
* position past it (both do_lseek() and do_punch_hole() move the offset). */
* position past it (both vfs_lseek() and vfs_punch_hole() move the offset). */
static int flush_sparse_hole(int f)
{
if (!sparse_seek)
return 0;
if (sparse_past_write >= preallocated_len) {
if (do_lseek(f, sparse_seek, SEEK_CUR) < 0) {
if (vfs_lseek(f, sparse_seek, SEEK_CUR) < 0) {
sparse_seek = 0;
return -1;
}
} else if (do_punch_hole(f, sparse_past_write, sparse_seek) < 0) {
} else if (vfs_punch_hole(f, sparse_past_write, sparse_seek) < 0) {
sparse_seek = 0;
return -1;
}
@@ -119,7 +119,7 @@ static int emit_sparse_span(int f, int use_seek, const char *buf, int len)
if (flush_sparse_hole(f) < 0)
return -1;
if (use_seek)
return do_lseek(f, len, SEEK_CUR) < 0 ? -1 : 0;
return vfs_lseek(f, len, SEEK_CUR) < 0 ? -1 : 0;
return full_sparse_write(f, buf, len);
}
@@ -262,7 +262,7 @@ int skip_matched(int fd, OFF_T offset, const char *buf, int len)
if (flush_write_file(fd) < 0)
return -1;
if ((pos = do_lseek(fd, len, SEEK_CUR)) != offset + len) {
if ((pos = vfs_lseek(fd, len, SEEK_CUR)) != offset + len) {
rsyserr(FERROR_XFER, errno, "lseek returned %s, not %s",
big_num(pos), big_num(offset));
return -1;
@@ -345,7 +345,7 @@ char *map_ptr(struct map_struct *map, OFF_T offset, int32 len)
}
if (map->p_fd_offset != read_start) {
OFF_T ret = do_lseek(map->fd, read_start, SEEK_SET);
OFF_T ret = vfs_lseek(map->fd, read_start, SEEK_SET);
if (ret != read_start) {
rsyserr(FERROR, errno, "lseek returned %s, not %s",
big_num(ret), big_num(read_start));
+28 -30
View File
@@ -33,7 +33,6 @@ extern int am_chrooted;
extern char *module_dir;
extern unsigned int module_dirlen;
extern int module_dirfd;
extern unsigned int curr_dir_len;
extern int am_sender;
extern int am_generator;
extern int inc_recurse;
@@ -87,7 +86,6 @@ extern char *usermap, *groupmap;
extern struct name_num_item *file_sum_nni;
extern char curr_dir[MAXPATHLEN];
extern struct chmod_mode_struct *chmod_modes;
@@ -250,8 +248,8 @@ static int scan_readlink(const char *path, char *linkbuf, size_t bufsiz)
&& strncmp(path, scan_dir_prefix, scan_dir_prefix_len) == 0
&& path[scan_dir_prefix_len] == '/'
&& strchr(path + scan_dir_prefix_len + 1, '/') == NULL)
return do_readlink_atfd(scan_dirfd, path + scan_dir_prefix_len + 1, linkbuf, bufsiz);
return do_readlink(path, linkbuf, bufsiz);
return vfs_readlink_atfd(scan_dirfd, path + scan_dir_prefix_len + 1, linkbuf, bufsiz);
return vfs_readlink(path, linkbuf, bufsiz);
}
static int readlink_stat(const char *path, STRUCT_STAT *stp, char *linkbuf)
@@ -269,7 +267,7 @@ static int readlink_stat(const char *path, STRUCT_STAT *stp, char *linkbuf)
rprintf(FINFO,"copying unsafe symlink \"%s\" -> \"%s\"\n",
path, linkbuf);
}
return x_stat(path, stp, NULL);
return x_stat(path, stp, NULL, 0);
}
if (munge_symlinks && am_sender && llen > SYMLINK_PREFIX_LEN
&& strncmp(linkbuf, SYMLINK_PREFIX, SYMLINK_PREFIX_LEN) == 0) {
@@ -279,7 +277,7 @@ static int readlink_stat(const char *path, STRUCT_STAT *stp, char *linkbuf)
}
return 0;
#else
return x_stat(path, stp, NULL);
return x_stat(path, stp, NULL, 0);
#endif
}
@@ -287,17 +285,17 @@ int link_stat(const char *path, STRUCT_STAT *stp, int follow_dirlinks)
{
#ifdef SUPPORT_LINKS
if (copy_links)
return x_stat(path, stp, NULL);
if (x_lstat(path, stp, NULL) < 0)
return x_stat(path, stp, NULL, 0);
if (x_lstat(path, stp, NULL, 0) < 0)
return -1;
if (follow_dirlinks && S_ISLNK(stp->st_mode)) {
STRUCT_STAT st;
if (x_stat(path, &st, NULL) == 0 && S_ISDIR(st.st_mode))
if (x_stat(path, &st, NULL, 0) == 0 && S_ISDIR(st.st_mode))
*stp = st;
}
return 0;
#else
return x_stat(path, stp, NULL);
return x_stat(path, stp, NULL, 0);
#endif
}
@@ -311,17 +309,17 @@ int link_stat_at(int dfd, const char *name, STRUCT_STAT *stp, int follow_dirlink
{
#ifdef SUPPORT_LINKS
if (copy_links)
return do_stat_atfd(dfd, name, stp);
if (do_lstat_atfd(dfd, name, stp) < 0)
return vfs_stat(dfd, name, stp, 0);
if (vfs_lstat(dfd, name, stp, 0) < 0)
return -1;
if (follow_dirlinks && S_ISLNK(stp->st_mode)) {
STRUCT_STAT st;
if (do_stat_atfd(dfd, name, &st) == 0 && S_ISDIR(st.st_mode))
if (vfs_stat(dfd, name, &st, 0) == 0 && S_ISDIR(st.st_mode))
*stp = st;
}
return 0;
#else
return do_stat_atfd(dfd, name, stp);
return vfs_stat(dfd, name, stp, 0);
#endif
}
@@ -1449,7 +1447,7 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,
* options was specified, so there's no need for the
* extra lstat() if one of these options isn't on. */
if ((copy_links || copy_unsafe_links || copy_dirlinks)
&& x_lstat(thisname, &st, NULL) == 0
&& x_lstat(thisname, &st, NULL, 0) == 0
&& S_ISLNK(st.st_mode)) {
io_error |= IOERR_GENERAL;
rprintf(FERROR_XFER, "symlink has no referent: %s\n",
@@ -1564,7 +1562,7 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,
if (copy_devices && am_sender && IS_DEVICE(st.st_mode)) {
if (st.st_size == 0) {
int fd = do_open_checklinks(fname);
int fd = vfs_open_checklinks(fname);
if (fd >= 0) {
st.st_size = get_device_size(fd, fname);
close(fd);
@@ -1677,7 +1675,7 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,
F_ATIME(file) = st.st_atime;
#ifdef SUPPORT_CRTIMES
if (crtimes_ndx)
F_CRTIME(file) = get_create_time(fname, &st);
F_CRTIME(file) = vfs_get_create_time(fname, &st);
#endif
if (basename != thisname)
@@ -2015,14 +2013,14 @@ static void interpret_stat_error(const char *fname, int is_dir)
#if defined HAVE_FDOPENDIR && defined HAVE_DIRFD
/* Open a source directory for scanning confined beneath the transfer root.
* secure_relative_open() does a per-component O_NOFOLLOW walk that refuses a
* vfs_resolve_open() does a per-component O_NOFOLLOW walk that refuses a
* parent component raced into a symlink pointing out of the tree; fdopendir()
* then turns the held fd into the DIR* the scan reads. This mirrors the
* sender's confined content open (sender.c): the directory enumeration must be
* confined the same way, or a parent-symlink race (or, for a daemon following
* mode, an in-module symlink to outside) lets the scan enumerate an out-of-tree
* directory and leak its names/metadata/symlink targets. O_DIRECTORY without
* O_NOFOLLOW makes secure_relative_open() follow in-tree directory symlinks
* O_NOFOLLOW makes vfs_resolve_open() follow in-tree directory symlinks
* beneath the anchor and refuse escapes, so this serves both the default
* no-follow scan and a daemon's symlink-following scan (see the caller).
* Returns NULL with errno set on failure, like opendir(). */
@@ -2033,9 +2031,9 @@ static DIR *secure_opendir(const char *fbuf)
if (am_daemon && (!am_chrooted || module_dirlen)
&& module_dir && module_dir[0] == '/' && *fbuf != '/' && module_dirfd >= 0
&& curr_dir_len >= module_dirlen
&& strncmp(curr_dir, module_dir, module_dirlen) == 0
&& (curr_dir[module_dirlen] == '\0' || curr_dir[module_dirlen] == '/')) {
&& vfs.curr_dir_len >= module_dirlen
&& strncmp(vfs.curr_dir, module_dir, module_dirlen) == 0
&& (vfs.curr_dir[module_dirlen] == '\0' || vfs.curr_dir[module_dirlen] == '/')) {
/* Daemon: anchor the confined scan at the module root pinned by identity
* at module setup (module_dirfd, opened while the daemon was positioned
* there and still privileged), and walk the module-relative path of the
@@ -2043,11 +2041,11 @@ static DIR *secure_opendir(const char *fbuf)
* legitimate in-module ".." climb (sub/climb -> ../sibling) or an in-module
* directory symlink is followed, and an escape refused -- without
* re-walking the absolute module path as the dropped uid (the privilege-
* drop EACCES), and without assuming the lexical curr_dir depth matches the
* drop EACCES), and without assuming the lexical vfs.curr_dir depth matches the
* real cwd (a followed in-module symlink can desync them; anchoring at the
* pinned module root and walking down the logical path is correct either
* way). */
const char *p = curr_dir + module_dirlen;
const char *p = vfs.curr_dir + module_dirlen;
char modrel[MAXPATHLEN];
while (*p == '/')
p++;
@@ -2056,7 +2054,7 @@ static DIR *secure_opendir(const char *fbuf)
errno = ENAMETOOLONG;
return NULL;
}
dfd = secure_relative_open_at(module_dirfd, *modrel ? modrel : ".",
dfd = vfs_resolve_open_at(module_dirfd, *modrel ? modrel : ".",
O_RDONLY | O_DIRECTORY, 0);
} else if (*fbuf == '/') {
/* An absolute scan path (an absolute --relative / --files-from name, or a
@@ -2064,11 +2062,11 @@ static DIR *secure_opendir(const char *fbuf)
const char *relp = fbuf;
while (*relp == '/')
relp++;
dfd = secure_relative_open("/", relp, O_RDONLY | O_DIRECTORY, 0);
dfd = vfs_resolve_open("/", relp, O_RDONLY | O_DIRECTORY, 0);
} else {
/* Non-daemon (or chrooted) sender: confine beneath the cwd the sender
* chdir'd into (the transfer root). */
dfd = secure_relative_open(NULL, fbuf, O_RDONLY | O_DIRECTORY, 0);
dfd = vfs_resolve_open(NULL, fbuf, O_RDONLY | O_DIRECTORY, 0);
}
if (dfd < 0)
@@ -2106,7 +2104,7 @@ static void send_directory(int f, struct file_list *flist, char *fbuf, int len,
/* Confine the enumeration beneath the transfer root. secure_opendir()
* follows in-tree directory symlinks (RESOLVE_BENEATH) and refuses one that
* escapes, so it serves both modes:
* - a daemon/hardened sender (secure_relpath_active()) is confined to the
* - a daemon/hardened sender (vfs_relpath_active()) is confined to the
* module in EVERY mode -- including -L/--copy-dirlinks/--copy-unsafe-
* links, matching the content open (sender_open_copylinks_confined) --
* so a following mode cannot be lured to enumerate outside the module;
@@ -2118,7 +2116,7 @@ static void send_directory(int f, struct file_list *flist, char *fbuf, int len,
* yes", admin-only) -- or a non-daemon --insecure-links -- uses the legacy
* opendir() too, restoring the pre-hardening enumeration (re-opening the
* escape; documented). */
if (f >= 0 && !symlink_optout_allowed() && (secure_relpath_active()
if (f >= 0 && !vfs_symlink_optout_allowed() && (vfs_relpath_active()
|| !(copy_links || copy_unsafe_links || copy_dirlinks || insecure_links)))
d = secure_opendir(fbuf);
else
@@ -2560,7 +2558,7 @@ struct file_list *send_file_list(int f, int argc, char *argv[])
}
if (!orig_dir)
orig_dir = strdup(curr_dir);
orig_dir = strdup(vfs.curr_dir);
while (1) {
char fbuf[MAXPATHLEN], *fn, name_type;
+96 -105
View File
@@ -29,7 +29,6 @@ extern int do_xfers;
extern int stdout_format_has_i;
extern int logfile_format_has_i;
extern int am_root;
extern int operator_path_resolve;
extern int am_server;
extern int am_daemon;
extern int inc_recurse;
@@ -133,7 +132,7 @@ static int start_delete_delay_temp(void)
dry_run = 0;
if (!get_tmpname(fnametmp, "deldelay", False)
|| (deldelay_fd = do_mkstemp(fnametmp, 0600)) < 0) {
|| (deldelay_fd = vfs_mkstemp(fnametmp, 0600)) < 0) {
rprintf(FINFO, "NOTE: Unable to create delete-delay temp file%s.\n",
inc_recurse ? "" : " -- switching to --delete-after");
delete_during = 0;
@@ -414,7 +413,7 @@ static inline int any_time_differs(stat_x *sxp, struct file_struct *file, UNUSED
#ifdef SUPPORT_CRTIMES
if (!differs && crtimes_ndx) {
if (sxp->crtime == 0)
sxp->crtime = get_create_time(fname, &sxp->st);
sxp->crtime = vfs_get_create_time(fname, &sxp->st);
differs = !same_time(sxp->crtime, 0, F_CRTIME(file), 0);
}
#endif
@@ -540,7 +539,7 @@ void itemize(const char *fnamecmp, struct file_struct *file, int ndx, int statre
#ifdef SUPPORT_CRTIMES
if (crtimes_ndx) {
if (sxp->crtime == 0)
sxp->crtime = get_create_time(fnamecmp, &sxp->st);
sxp->crtime = vfs_get_create_time(fnamecmp, &sxp->st);
if (!same_time(sxp->crtime, 0, F_CRTIME(file), 0))
iflags |= ITEM_REPORT_CRTIME;
}
@@ -656,7 +655,7 @@ int quick_check_ok(enum filetype ftype, const char *fn, struct file_struct *file
case FT_SYMLINK: {
#ifdef SUPPORT_LINKS
char lnk[MAXPATHLEN];
int len = do_readlink(fn, lnk, MAXPATHLEN-1);
int len = vfs_readlink(fn, lnk, MAXPATHLEN-1);
if (len <= 0)
return 0;
lnk[len] = '\0';
@@ -932,15 +931,15 @@ static int copy_altdest_file(const char *src, const char *dest, struct file_stru
copy_to = buf;
}
cleanup_set(copy_to, NULL, NULL, -1, -1);
if (copy_file(src, copy_to, fd_w, file->mode) < 0) {
if (copy_file(src, copy_to, fd_w, file->mode, 0) < 0) {
if (INFO_GTE(COPY, 1)) {
rsyserr(FINFO, errno, "copy_file %s => %s",
full_fname(src), copy_to);
}
/* Try to clean up. copy_to's parent components are peer-named
* and can be raced to a symlink, so resolve each with O_NOFOLLOW
* via do_unlink_at() like the other generator-side unlinks. */
do_unlink_at(copy_to);
* via vfs_unlink_at() like the other generator-side unlinks. */
vfs_unlink(VFS_AT_FDCWD, copy_to, 0);
cleanup_disable();
return -1;
}
@@ -955,14 +954,13 @@ static int copy_altdest_file(const char *src, const char *dest, struct file_stru
/* Stat an alternate-basis candidate (basis_dir[j]/fname) for a daemon /./
* inner-module chroot through the secure resolver, so a --compare/copy/link-dest
* basis can't reach outside the inner module via a symlinked parent (the kernel
* chroot confines only the outer path). secure_relative_open() refuses a parent
* chroot confines only the outer path). vfs_resolve_open() refuses a parent
* that escapes beneath the module root. Plain link_stat() everywhere else --
* the non-chroot daemon sanitizes basis paths already, and a local receiver must
* still follow an operator's --link-dest=../backup. */
static int basis_link_stat(const char *path, STRUCT_STAT *stp)
{
extern int am_chrooted;
extern int operator_path_resolve;
extern unsigned int module_dirlen;
#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY
/* The basis dir (--link-dest/--compare-dest/--copy-dest) is an operator-
@@ -976,9 +974,11 @@ static int basis_link_stat(const char *path, STRUCT_STAT *stp)
* resolver) below. Only when am_root >= 0: link_stat_at() omits the
* fake-super %stat xattr that link_stat() folds in, so --fake-super keeps
* the plain path (a lower-severity, non-root basis lookup). */
if (!am_daemon && am_root >= 0 && !symlink_optout_allowed()) {
if (!am_daemon && am_root >= 0 && !vfs_symlink_optout_allowed()) {
const char *leaf;
int dfd = owner_walk_parent(path, &leaf);
/* non-daemon path: is_operator only gates the daemon module-confinement
* (a no-op here), so the ownership walk is identical either way. */
int dfd = vfs_owner_walk_parent(path, &leaf, 0);
int r, e;
if (dfd < 0)
return -1;
@@ -989,7 +989,7 @@ static int basis_link_stat(const char *path, STRUCT_STAT *stp)
return r;
}
/* A non-chroot daemon serving an operator/peer alt-dest basis: resolve through
* the ownership walk with module-ROOT confinement (operator_path_resolve) so an
* the ownership walk with module-ROOT confinement (is_operator=1) so an
* in-module symlink whose target lands OUTSIDE the module is refused -- the
* basis then looks absent and the file transfers normally instead of being
* stat'd/read/linked through the link (closes the --compare-dest=/E read
@@ -1001,16 +1001,14 @@ static int basis_link_stat(const char *path, STRUCT_STAT *stp)
* and must keep the plain link_stat below (#915/#930). The leaf is taken
* under the confined parent with O_NOFOLLOW/AT_SYMLINK_NOFOLLOW, so
* --copy-links can't follow a leaf symlink out of the module. */
if (am_daemon && !am_chrooted && path[0] == '/' && !symlink_optout_allowed()) {
if (am_daemon && !am_chrooted && path[0] == '/' && !vfs_symlink_optout_allowed()) {
const char *leaf;
int dfd, e, save = operator_path_resolve;
operator_path_resolve = 1;
dfd = owner_walk_parent(path, &leaf);
operator_path_resolve = save;
int dfd, e;
dfd = vfs_owner_walk_parent(path, &leaf, 1);
if (dfd < 0)
return -1;
if (am_root >= 0) {
int r = do_lstat_atfd(dfd, leaf, stp);
int r = vfs_lstat(dfd, leaf, stp, 0);
e = errno;
close(dfd);
errno = e;
@@ -1021,12 +1019,12 @@ static int basis_link_stat(const char *path, STRUCT_STAT *stp)
/* --fake-super: O_NOFOLLOW-open the held leaf (the daemon owns its
* fake-super files) so the %stat xattr link_stat() would fold is
* preserved while a leaf symlink is still refused. */
int lfd = do_open_atfd(dfd, leaf, O_RDONLY | O_NOFOLLOW | O_NONBLOCK, 0);
int lfd = vfs_open_atfd(dfd, leaf, O_RDONLY | O_NOFOLLOW | O_NONBLOCK, 0);
STRUCT_STAT xst;
e = errno;
close(dfd);
if (lfd < 0) { errno = e; return -1; }
if (do_fstat(lfd, stp) < 0) { e = errno; close(lfd); errno = e; return -1; }
if (vfs_fstat(lfd, stp) < 0) { e = errno; close(lfd); errno = e; return -1; }
if (get_stat_xattr(NULL, lfd, stp, &xst) == 0)
*stp = xst;
close(lfd);
@@ -1034,7 +1032,7 @@ static int basis_link_stat(const char *path, STRUCT_STAT *stp)
}
#else
{
int r = do_lstat_atfd(dfd, leaf, stp);
int r = vfs_lstat(dfd, leaf, stp, 0);
e = errno;
close(dfd);
errno = e;
@@ -1043,7 +1041,7 @@ static int basis_link_stat(const char *path, STRUCT_STAT *stp)
#endif
}
#endif
if (am_daemon && am_chrooted && module_dirlen && path[0] != '/' && !symlink_optout_allowed()) {
if (am_daemon && am_chrooted && module_dirlen && path[0] != '/' && !vfs_symlink_optout_allowed()) {
const char *slash = strrchr(path, '/');
if (slash) {
char dir[MAXPATHLEN];
@@ -1052,7 +1050,7 @@ static int basis_link_stat(const char *path, STRUCT_STAT *stp)
if (dlen >= sizeof dir) { errno = ENAMETOOLONG; return -1; }
memcpy(dir, path, dlen);
dir[dlen] = '\0';
if ((dfd = secure_relative_open(NULL, dir, O_RDONLY | O_DIRECTORY, 0)) < 0)
if ((dfd = vfs_resolve_open(NULL, dir, O_RDONLY | O_DIRECTORY, 0)) < 0)
return -1;
r = link_stat_at(dfd, slash + 1, stp, 0);
e = errno;
@@ -1115,7 +1113,7 @@ static int try_dests_reg(struct file_struct *file, char *fname, int ndx,
if (find_exact_for_existing) {
if (alt_dest_type == LINK_DEST && real_st.st_dev == sxp->st.st_dev && real_st.st_ino == sxp->st.st_ino)
return -1;
if (do_unlink_at(fname) < 0 && errno != ENOENT)
if (vfs_unlink(VFS_AT_FDCWD, fname, 0) < 0 && errno != ENOENT)
goto got_nothing_for_ya;
}
#ifdef SUPPORT_HARD_LINKS
@@ -1124,15 +1122,11 @@ static int try_dests_reg(struct file_struct *file, char *fname, int ndx,
* resolve the link source via the ownership walk so a foreign-owned
* symlink raced in after the basis_link_stat() check is still
* refused (matching basis_link_stat's !am_daemon gate). A daemon
* keeps its stronger module-anchored confinement (do_link_at's
* secure_relpath_active path) -- the ownership walk would follow an
* keeps its stronger module-anchored confinement (vfs_link_at's
* vfs_relpath_active path) -- the ownership walk would follow an
* operator-owned symlink out of the module. */
int hlok, op = !am_daemon;
if (op)
operator_path_resolve = 1;
hlok = hard_link_one(file, fname, cmpbuf, 1);
if (op)
operator_path_resolve = 0;
int hlok = hard_link_one(file, fname, cmpbuf, 1,
!am_daemon ? VFS_OPERATOR_PATH : 0);
if (!hlok)
goto try_a_copy;
if (atimes_ndx)
@@ -1164,9 +1158,10 @@ static int try_dests_reg(struct file_struct *file, char *fname, int ndx,
#endif
/* NB: the copy-dest basis read is deliberately NOT routed through the
* ownership walk: copy_altdest_file()->copy_file() also opens the dest
* and copies xattrs through a held O_NOFOLLOW fd, and forcing
* operator_path_resolve across that re-opens the copy_xattrs parent-
* symlink race (copy-xattrs-symlink-race). basis_link_stat() already
* and copies xattrs through a held O_NOFOLLOW fd, and passing
* VFS_OPERATOR_PATH across that re-opens the copy_xattrs parent-
* symlink race (copy-xattrs-symlink-race) -- so copy_file gets flags 0.
* basis_link_stat() already
* refuses a foreign-owned basis symlink, closing the static escape; the
* post-stat race on an absolute copy-dest basis is a documented residual. */
if (!dry_run && copy_altdest_file(cmpbuf, fname, file) < 0) {
@@ -1266,7 +1261,11 @@ static int try_dests_non(struct file_struct *file, char *fname, int ndx,
&& !IS_SPECIAL(file->mode) && !IS_DEVICE(file->mode)
#endif
&& !S_ISDIR(file->mode)) {
if (do_link_at(cmpbuf, fname) < 0) {
/* cmpbuf is the alt-dest (--link-dest) basis: for a non-daemon
* receiver it is an operator path (owner walk; matches the
* hard_link_one() path above and basis_link_stat's !am_daemon gate).
* fname is the transfer destination (secure receiver resolve). */
if (vfs_link_at(cmpbuf, fname, !am_daemon ? VFS_OPERATOR_PATH : 0, 0) < 0) {
/* CAN_HARDLINK_SYMLINK/_SPECIAL answer for whatever
* filesystem the build tree sat on; the destination is
* free to disagree, and one host can hold both (macOS
@@ -1382,8 +1381,8 @@ static BOOL is_below(struct file_struct *file, struct file_struct *subtree)
/* Held-dirfd helpers for the per-entry ops below: when the secure resolver is
* active they act on the entry's basename relative to its cached directory fd
* (held_dfd_for, keyed on file->dirname), else fall back to the full-path
* do_*_at wrappers (behaviour-identical). held_dfd_for() declines when fname
* (vfs_cached_dirfd, keyed on file->dirname), else fall back to the full-path
* do_*_at wrappers (behaviour-identical). vfs_cached_dirfd() declines when fname
* isn't in file->dirname (e.g. the single-file local_name dest), and the leaf
* is derived from fname, not file->basename. */
static int gen_entry_stat(const char *fname, struct file_struct *file,
@@ -1392,7 +1391,7 @@ static int gen_entry_stat(const char *fname, struct file_struct *file,
int dfd;
/* link_stat_at folds in no fake-super xattr, so only use it when
* am_root >= 0 (where link_stat's get_stat_xattr is a no-op anyway). */
if (am_root >= 0 && (dfd = held_dfd_for(fname, file)) >= 0) {
if (am_root >= 0 && (dfd = vfs_cached_dirfd(fname, file)) >= 0) {
const char *slash = strrchr(fname, '/');
return link_stat_at(dfd, slash ? slash + 1 : fname, stp, follow_dirlinks);
}
@@ -1401,27 +1400,27 @@ static int gen_entry_stat(const char *fname, struct file_struct *file,
static int gen_entry_mkdir(char *fname, struct file_struct *file, mode_t mode)
{
int dfd = held_dfd_for(fname, file);
int dfd = vfs_cached_dirfd(fname, file);
if (dfd >= 0) {
const char *slash = strrchr(fname, '/');
return do_mkdir_atfd(dfd, slash ? slash + 1 : fname, mode);
char *slash = strrchr(fname, '/');
return vfs_mkdir(dfd, slash ? slash + 1 : fname, mode, 0);
}
return do_mkdir_at(fname, mode);
return vfs_mkdir(VFS_AT_FDCWD, fname, mode, 0);
}
static int gen_entry_chmod(const char *fname, struct file_struct *file, mode_t mode)
{
int dfd = held_dfd_for(fname, file);
int dfd = vfs_cached_dirfd(fname, file);
if (dfd >= 0) {
const char *slash = strrchr(fname, '/');
return do_chmod_atfd(dfd, slash ? slash + 1 : fname, mode);
return vfs_chmod(dfd, slash ? slash + 1 : fname, mode, 0);
}
return do_chmod_at(fname, mode);
return vfs_chmod(VFS_AT_FDCWD, fname, mode, 0);
}
static void gen_entry_set_times(const char *fname, struct file_struct *file, STRUCT_STAT *stp)
{
int dfd = held_dfd_for(fname, file);
int dfd = vfs_cached_dirfd(fname, file);
if (dfd >= 0) {
const char *slash = strrchr(fname, '/');
if (set_times_at(dfd, slash ? slash + 1 : fname, stp) != -2)
@@ -1432,12 +1431,12 @@ static void gen_entry_set_times(const char *fname, struct file_struct *file, STR
static int gen_entry_symlink(const char *slnk, const char *path, struct file_struct *file)
{
int dfd = held_dfd_for(path, file);
int dfd = vfs_cached_dirfd(path, file);
if (dfd >= 0) {
const char *slash = strrchr(path, '/');
return do_symlink_atfd(slnk, dfd, slash ? slash + 1 : path);
return vfs_symlink(slnk, dfd, slash ? slash + 1 : path, 0);
}
return do_symlink_at(slnk, path);
return vfs_symlink(slnk, VFS_AT_FDCWD, path, 0);
}
/* True when this build compiled no fd-relative primitive able to create this
@@ -1471,10 +1470,10 @@ static int no_atfd_mknod_primitive(mode_t mode)
static int gen_entry_mknod(const char *path, struct file_struct *file, mode_t mode, dev_t rdev)
{
int dfd;
/* do_mknod_atfd can't create a socket (no portable bindat); fall back. */
if (!S_ISSOCK(mode) && (dfd = held_dfd_for(path, file)) >= 0) {
/* vfs_mknod_atfd can't create a socket (no portable bindat); fall back. */
if (!S_ISSOCK(mode) && (dfd = vfs_cached_dirfd(path, file)) >= 0) {
const char *slash = strrchr(path, '/');
int ret = do_mknod_atfd(dfd, slash ? slash + 1 : path, mode, rdev);
int ret = vfs_mknod(dfd, slash ? slash + 1 : path, mode, rdev, 0);
/* Fall through to the unconfined path-based create only where this
* build compiled no fd-relative primitive for this kind of node --
* SECURITY.md's rule for a platform that cannot be secure at all.
@@ -1485,17 +1484,17 @@ static int gen_entry_mknod(const char *path, struct file_struct *file, mode_t mo
if (ret == 0 || !no_atfd_mknod_primitive(mode))
return ret;
}
return do_mknod_at(path, mode, rdev);
return vfs_mknod(VFS_AT_FDCWD, path, mode, rdev, 0);
}
static int gen_entry_unlink(const char *path, struct file_struct *file)
{
int dfd = held_dfd_for(path, file);
int dfd = vfs_cached_dirfd(path, file);
if (dfd >= 0) {
const char *slash = strrchr(path, '/');
return do_unlink_atfd(dfd, slash ? slash + 1 : path, 0);
return vfs_unlink(dfd, slash ? slash + 1 : path, 0);
}
return do_unlink_at(path);
return vfs_unlink(VFS_AT_FDCWD, path, 0);
}
/* opath and npath are both expected to live in the entry's directory (the
@@ -1503,14 +1502,14 @@ static int gen_entry_unlink(const char *path, struct file_struct *file)
* single renameat() within it, else fall back to the full-path wrapper. */
static int gen_entry_rename(const char *opath, const char *npath, struct file_struct *file)
{
int odfd = held_dfd_for(opath, file);
int ndfd = held_dfd_for(npath, file);
int odfd = vfs_cached_dirfd(opath, file);
int ndfd = vfs_cached_dirfd(npath, file);
if (odfd >= 0 && ndfd >= 0) {
const char *os = strrchr(opath, '/');
const char *ns = strrchr(npath, '/');
return do_rename_atfd(odfd, os ? os + 1 : opath, ndfd, ns ? ns + 1 : npath);
return vfs_rename_atfd(odfd, os ? os + 1 : opath, ndfd, ns ? ns + 1 : npath);
}
return do_rename_at(opath, npath);
return vfs_rename_at(opath, npath, 0, 0); /* both live in the entry's dir (transfer) */
}
#ifdef SUPPORT_XATTRS
@@ -1522,7 +1521,7 @@ static int gen_entry_rename(const char *opath, const char *npath, struct file_st
* set_file_attrs' held-fd handling). */
static int gen_entry_copy_xattrs(const char *src, const char *fname, struct file_struct *file)
{
int dfd = held_dfd_for(fname, file);
int dfd = vfs_cached_dirfd(fname, file);
int xfd = -1, sfd = -1, ret;
if (dfd >= 0) {
const char *slash = strrchr(fname, '/');
@@ -1540,18 +1539,18 @@ static int gen_entry_copy_xattrs(const char *src, const char *fname, struct file
}
}
#if defined AT_FDCWD && defined O_NOFOLLOW
else if (secure_relpath_active()) {
/* No cached parent dirfd (a path deeper than the dirfd cache, or a raced
* parent) but we must confine: re-pin the dest leaf through the secure
* resolver so copy_xattrs uses fsetxattr, not a path-based lsetxattr a
* flipped parent could redirect out of tree. A raced parent/leaf makes
* this fail -> refuse rather than path-write. */
else if (vfs_relpath_active()) {
/* No cached parent dirfd (e.g. a path deeper than the dirfd cache, or a
* raced parent) but we must confine: re-pin the dest leaf through the
* secure resolver so copy_xattrs uses fsetxattr, not a path-based
* lsetxattr a flipped parent could redirect out of tree. A raced
* parent/leaf makes this fail -> refuse rather than path-write. */
int odir = 0;
# ifdef O_DIRECTORY
if (S_ISDIR(file->mode))
odir = O_DIRECTORY;
# endif
xfd = secure_relative_open(NULL, fname,
xfd = vfs_resolve_open(NULL, fname,
O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_NOCTTY | O_CLOEXEC | odir, 0);
if (xfd < 0) {
rsyserr(FERROR_XFER, errno,
@@ -1567,20 +1566,18 @@ static int gen_entry_copy_xattrs(const char *src, const char *fname, struct file
* through the operator ownership walk. Refuse (don't path-read) when we are
* meant to confine but can't pin; a non-hardened receiver path-reads (sfd<0). */
#if defined AT_FDCWD && defined O_NOFOLLOW
if (secure_relpath_active() && src && *src && !symlink_optout_allowed()) {
if (vfs_relpath_active() && src && *src && !vfs_symlink_optout_allowed()) {
int odir = 0;
#ifdef O_DIRECTORY
if (S_ISDIR(file->mode)) /* secure_relative_open rejects a dir leaf without this */
if (S_ISDIR(file->mode)) /* vfs_resolve_open rejects a dir leaf without this */
odir = O_DIRECTORY;
#endif
if (src[0] != '/')
sfd = secure_relative_open(NULL, src, O_RDONLY | O_NOFOLLOW | odir, 0);
sfd = vfs_resolve_open(NULL, src, O_RDONLY | O_NOFOLLOW | odir, 0);
else {
int save = operator_path_resolve, sdfd, e;
int sdfd, e;
const char *leaf;
operator_path_resolve = 1;
sdfd = owner_walk_parent(src, &leaf);
operator_path_resolve = save;
sdfd = vfs_owner_walk_parent(src, &leaf, 1);
if (sdfd >= 0) {
sfd = openat(sdfd, leaf, O_RDONLY | O_NOFOLLOW | odir | O_NONBLOCK | O_NOCTTY | O_CLOEXEC);
e = errno; close(sdfd); errno = e;
@@ -1595,6 +1592,13 @@ static int gen_entry_copy_xattrs(const char *src, const char *fname, struct file
return -1;
}
}
#endif
#ifdef STRICT_CONFINEMENT
/* In the confined regime the dfd/re-pin paths above yield xfd >= 0 or already
* returned -1; reaching copy_xattrs with xfd < 0 while confined would let it
* path-write the dest xattrs (the copy-xattrs fallback class) -- abort. */
if (xfd < 0 && vfs_must_be_confined(fname, 0))
vfs_strict_confine_fail(fname, "gen_entry_copy_xattrs dest");
#endif
ret = copy_xattrs(src, sfd, fname, xfd);
if (sfd >= 0)
@@ -1716,10 +1720,10 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
}
}
if (relative_paths && !implied_dirs && file->mode != 0
&& do_stat_at(dn, &sx.st) < 0) {
&& vfs_stat(VFS_AT_FDCWD, dn, &sx.st, 0) < 0) {
if (dry_run)
goto parent_is_dry_missing;
if (make_path(fname, MKP_DROP_NAME | MKP_SKIP_SLASH) < 0) {
if (vfs_make_path(fname, MKP_DROP_NAME | MKP_SKIP_SLASH, 0) < 0) {
rsyserr(FERROR_XFER, errno,
"recv_generator: mkdir %s failed",
full_fname(dn));
@@ -1873,9 +1877,9 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
if (real_ret != 0 && gen_entry_mkdir(fname, file, file->mode|added_perms) < 0 && errno != EEXIST) {
/* The parent may have just been created by make_path(), so
* drop any cached (failed) dir fd before the retry. */
reset_dir_fd_cache();
vfs_dircache_reset();
if (!relative_paths || errno != ENOENT
|| make_path(fname, MKP_DROP_NAME | MKP_SKIP_SLASH) < 0
|| vfs_make_path(fname, MKP_DROP_NAME | MKP_SKIP_SLASH, 0) < 0
|| (gen_entry_mkdir(fname, file, file->mode|added_perms) < 0 && errno != EEXIST)) {
rsyserr(FERROR_XFER, errno,
"recv_generator: mkdir %s failed",
@@ -2225,7 +2229,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
if (write_devices && IS_DEVICE(sx.st.st_mode) && sx.st.st_size == 0) {
/* This early open into fd skips the regular open below. */
if ((fd = do_open_nofollow(fnamecmp, O_RDONLY)) >= 0)
if ((fd = vfs_open_nofollow(fnamecmp, O_RDONLY)) >= 0)
real_sx.st.st_size = sx.st.st_size = get_device_size(fd, fnamecmp);
}
@@ -2238,9 +2242,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
/* The --partial-dir basis is an operator/peer path: unlink it
* through the exclude-aware ownership walk so a symlinked
* partial-dir can't delete a file in an excluded subtree. */
operator_path_resolve = 1;
do_unlink_at(partialptr);
operator_path_resolve = 0;
vfs_unlink(VFS_AT_FDCWD, partialptr, VFS_OPERATOR_PATH);
handle_partial_dir(partialptr, PDIR_DELETE);
}
set_file_attrs(fname, file, &sx, NULL, maybe_ATTRS_REPORT | maybe_ATTRS_ACCURATE_TIME);
@@ -2280,25 +2282,20 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
if (read_batch || whole_file) {
if (inplace && make_backups > 0 && fnamecmp_type == FNAMECMP_FNAME) {
/* The --backup-dir (backupptr) is an operator path; this in-place
* backup bypasses make_backup(), so set operator_path_resolve here
* too -- get_backup_name() (make_path) and copy_file() then resolve
* it with the ownership walk instead of following any symlink. */
operator_path_resolve = 1;
* backup bypasses make_backup(), so get_backup_name() (make_path) and
* copy_file() below are passed VFS_OPERATOR_PATH to resolve it with the
* ownership walk instead of following any symlink. */
if (!(backupptr = get_backup_name(fname))) {
operator_path_resolve = 0;
goto cleanup;
}
if (!(back_file = make_file(fname, NULL, NULL, 0, NO_FILTERS))) {
operator_path_resolve = 0;
goto pretend_missing;
}
if (copy_file(fname, backupptr, -1, back_file->mode) < 0) {
operator_path_resolve = 0;
if (copy_file(fname, backupptr, -1, back_file->mode, VFS_OPERATOR_PATH) < 0) {
unmake_file(back_file);
back_file = NULL;
goto cleanup;
}
operator_path_resolve = 0;
}
goto notify_others;
}
@@ -2310,7 +2307,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
}
/* open the file */
if (fd < 0 && (fd = do_open_checklinks(fnamecmp)) < 0) {
if (fd < 0 && (fd = vfs_open_checklinks(fnamecmp)) < 0) {
rsyserr(FERROR, errno, "failed to open %s, continuing",
full_fname(fnamecmp));
pretend_missing:
@@ -2328,31 +2325,25 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
if (inplace && make_backups > 0 && fnamecmp_type == FNAMECMP_FNAME) {
/* Operator --backup-dir, bypassing make_backup(): resolve get_backup_name()
* (make_path), the unlink and the create with the ownership walk. */
operator_path_resolve = 1;
if (!(backupptr = get_backup_name(fname))) {
operator_path_resolve = 0;
goto cleanup;
}
if (!(back_file = make_file(fname, NULL, NULL, 0, NO_FILTERS))) {
operator_path_resolve = 0;
goto pretend_missing;
}
if (robust_unlink(backupptr) && errno != ENOENT) {
operator_path_resolve = 0;
if (robust_unlink(backupptr, VFS_OPERATOR_PATH) && errno != ENOENT) {
rsyserr(FERROR_XFER, errno, "unlink %s",
full_fname(backupptr));
unmake_file(back_file);
back_file = NULL;
goto cleanup;
}
if ((f_copy = do_open_at(backupptr, O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, 0600)) < 0) {
operator_path_resolve = 0;
if ((f_copy = vfs_open_at(backupptr, O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, 0600, VFS_OPERATOR_PATH)) < 0) {
rsyserr(FERROR_XFER, errno, "open %s", full_fname(backupptr));
unmake_file(back_file);
back_file = NULL;
goto cleanup;
}
operator_path_resolve = 0;
fnamecmp_type = FNAMECMP_BACKUP;
}
@@ -2436,7 +2427,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
if (f_copy >= 0)
close(f_copy);
/* backupptr's data/xattrs were written safely (confined create under
* operator_path_resolve, held-fd xattr copy above). This metadata set
* VFS_OPERATOR_PATH, held-fd xattr copy above). This metadata set
* re-resolves backupptr by path and is NOT wrapped in operator mode:
* set_file_attrs() also drives the path-based xattr set whose held-fd
* race-fix operator mode would defeat (cf. the copy-dest note in
@@ -2498,7 +2489,7 @@ int atomic_create(struct file_struct *file, char *fname, const char *slnk, const
#endif
} else if (hlnk) {
#ifdef SUPPORT_HARD_LINKS
if (!hard_link_one(file, create_name, hlnk, 0))
if (!hard_link_one(file, create_name, hlnk, 0, 0))
return 0;
#else
return 0;
+5 -2
View File
@@ -473,9 +473,12 @@ int hard_link_check(struct file_struct *file, int ndx, char *fname,
}
int hard_link_one(struct file_struct *file, const char *fname,
const char *oldname, int terse)
const char *oldname, int terse, int vfs_flags)
{
if (do_link_at(oldname, fname) < 0) {
/* oldname is the link source (vfs_flags carries its policy -- VFS_OPERATOR_PATH
* for an alt-dest basis on a non-daemon receiver, else 0); fname is the
* transfer destination, always under the secure receiver resolve. */
if (vfs_link_at(oldname, fname, vfs_flags, 0) < 0) {
enum logcode code;
if (terse) {
if (!INFO_GTE(NAME, 1))
+3 -4
View File
@@ -55,7 +55,6 @@ extern iconv_t ic_chck;
#ifdef ICONV_OPTION
extern iconv_t ic_recv;
#endif
extern char curr_dir[MAXPATHLEN];
extern char *full_module_path;
extern unsigned int module_dirlen;
extern char sender_file_sum[MAX_DIGEST_LEN];
@@ -166,8 +165,8 @@ static void logfile_open(void)
* attacker-writable dirs; a planted symlink could redirect root's log
* into e.g. /root/.ssh/authorized_keys. Refuse symlinks not owned by
* uid 0 or our euid. */
int fd = open_no_attacker_symlinks(logfile_name,
O_WRONLY | O_APPEND | O_CREAT, 0644);
int fd = vfs_open_owner_walk(logfile_name,
O_WRONLY | O_APPEND | O_CREAT, 0644, 0);
logfile_fp = fd >= 0 ? fdopen(fd, "a") : NULL;
if (!logfile_fp && fd >= 0)
close(fd);
@@ -648,7 +647,7 @@ static void log_formatted(enum logcode code, const char *format, const char *op,
n = buf2;
} else if (am_daemon && *c != '/') {
pathjoin(buf2, sizeof buf2,
curr_dir + module_dirlen, c);
vfs.curr_dir + module_dirlen, c);
clean_fname(buf2, 0);
if (fmt[1]) {
strlcpy(c, buf2, MAXPATHLEN);
+17 -17
View File
@@ -70,7 +70,6 @@ extern int protect_args;
extern int relative_paths;
extern int sanitize_paths;
extern int curr_dir_depth;
extern unsigned int curr_dir_len;
extern int module_id;
extern int rsync_port;
extern int whole_file;
@@ -106,7 +105,6 @@ extern char *password_file;
extern char *backup_dir;
extern char *copy_as;
extern char *tmpdir;
extern char curr_dir[MAXPATHLEN];
extern char backup_dir_buf[MAXPATHLEN];
extern char *basis_dir[MAX_BASIS_DIRS+1];
extern struct file_list *first_flist;
@@ -737,13 +735,13 @@ static char *get_local_name(struct file_list *flist, char *dest_path)
}
/* See what currently exists at the destination. */
statret = do_stat(dest_path, &st);
statret = vfs_stat(VFS_AT_FDCWD, dest_path, &st, VFS_ALLOW_SYMLINK);
cp = strrchr(dest_path, '/');
trailing_slash = cp && !cp[1];
if (mkpath_dest_arg && statret < 0 && (cp || file_total > 1)) {
int save_errno = errno;
int ret = make_path(dest_path, file_total > 1 && !trailing_slash ? 0 : MKP_DROP_NAME);
int ret = vfs_make_path(dest_path, file_total > 1 && !trailing_slash ? 0 : MKP_DROP_NAME, 0);
if (ret < 0)
goto mkdir_error;
if (ret && (INFO_GTE(NAME, 1) || stdout_format_has_i)) {
@@ -754,7 +752,7 @@ static char *get_local_name(struct file_list *flist, char *dest_path)
*cp = '/';
}
if (ret)
statret = do_stat(dest_path, &st);
statret = vfs_stat(VFS_AT_FDCWD, dest_path, &st, VFS_ALLOW_SYMLINK);
else
errno = save_errno;
}
@@ -801,7 +799,7 @@ static char *get_local_name(struct file_list *flist, char *dest_path)
exit_cleanup(RERR_SYNTAX);
}
if (do_mkdir(dest_path, ACCESSPERMS) != 0) {
if (vfs_mkdir(VFS_AT_FDCWD, dest_path, ACCESSPERMS, VFS_ALLOW_SYMLINK) != 0) {
mkdir_error:
rsyserr(FERROR, errno, "mkdir %s failed",
full_fname(dest_path));
@@ -840,7 +838,7 @@ static char *get_local_name(struct file_list *flist, char *dest_path)
dest_path = "/";
*cp = '\0';
if (dry_run && mkpath_dest_arg && do_stat(dest_path, &st) < 0) {
if (dry_run && mkpath_dest_arg && vfs_stat(VFS_AT_FDCWD, dest_path, &st, VFS_ALLOW_SYMLINK) < 0) {
/* --mkpath would have created this parent dir, but a dry run did
* not, so don't chdir into it; flag the destination as not yet
* present (as the dir-creation path above does) so the generator
@@ -862,12 +860,12 @@ static char *get_local_name(struct file_list *flist, char *dest_path)
/* This function checks on our alternate-basis directories. If we're in
* dry-run mode and the destination dir does not yet exist, we'll try to
* tweak any dest-relative paths to make them work for a dry-run (the
* destination dir must be in curr_dir[] when this function is called).
* destination dir must be in vfs.curr_dir[] when this function is called).
* We also warn about any arg that is non-existent or not a directory. */
static void check_alt_basis_dirs(void)
{
STRUCT_STAT st;
char *slash = strrchr(curr_dir, '/');
char *slash = strrchr(vfs.curr_dir, '/');
int j;
for (j = 0; j < basis_dir_cnt; j++) {
@@ -877,13 +875,13 @@ static void check_alt_basis_dirs(void)
if (bd_len > 1 && bdir[bd_len-1] == '/')
bdir[--bd_len] = '\0';
/* Make a relative --link-dest/--copy-dest/--compare-dest absolute
* (vs the destination curr_dir). These are operator-trusted roots, so
* (vs the destination vfs.curr_dir). These are operator-trusted roots, so
* an absolute path makes the do_*_at() wrappers use plain resolution
* rather than reject an operator '..' outside the dest tree (e.g.
* --copy-dest=../to). Skipped when sanitize_paths already confined
* them; the dry_run>1 case keeps its leading-"../"-strip. */
if (*bdir != '/' && (dry_run > 1 || !sanitize_paths)) {
int len = curr_dir_len + 1 + bd_len + 1;
int len = vfs.curr_dir_len + 1 + bd_len + 1;
char *new = new_array(char, len);
if (dry_run > 1 && slash && strncmp(bdir, "../", 3) == 0) {
/* We want to remove only one leading "../" prefix for
@@ -891,13 +889,13 @@ static void check_alt_basis_dirs(void)
* this ensures that any other ".." references get
* evaluated the same as they would for a live copy. */
*slash = '\0';
pathjoin(new, len, curr_dir, bdir + 3);
pathjoin(new, len, vfs.curr_dir, bdir + 3);
*slash = '/';
} else
pathjoin(new, len, curr_dir, bdir);
pathjoin(new, len, vfs.curr_dir, bdir);
basis_dir[j] = bdir = new;
}
if (do_stat(bdir, &st) < 0)
if (vfs_stat(VFS_AT_FDCWD, bdir, &st, VFS_ALLOW_SYMLINK) < 0)
rprintf(FWARNING, "%s arg does not exist: %s\n", alt_dest_opt(0), bdir);
else if (!S_ISDIR(st.st_mode))
rprintf(FWARNING, "%s arg is not a dir: %s\n", alt_dest_opt(0), bdir);
@@ -1025,7 +1023,7 @@ static int do_recv(int f_in, int f_out, char *local_name)
int ret;
if (backup_dir_len > 1)
backup_dir_buf[backup_dir_len-1] = '\0';
ret = do_stat(backup_dir_buf, &st);
ret = vfs_stat(VFS_AT_FDCWD, backup_dir_buf, &st, VFS_ALLOW_SYMLINK);
if (ret != 0 || !S_ISDIR(st.st_mode)) {
if (ret == 0) {
rprintf(FERROR, "The backup-dir is not a directory: %s\n", backup_dir_buf);
@@ -1045,7 +1043,7 @@ static int do_recv(int f_in, int f_out, char *local_name)
if (tmpdir) {
STRUCT_STAT st;
int ret = do_stat(tmpdir, &st);
int ret = vfs_stat(VFS_AT_FDCWD, tmpdir, &st, VFS_ALLOW_SYMLINK);
if (ret < 0 || !S_ISDIR(st.st_mode)) {
if (ret == 0) {
rprintf(FERROR, "The temp-dir is not a directory: %s\n", tmpdir);
@@ -1782,7 +1780,7 @@ static void unset_env_var(const char *var)
}
/* The symlink-race-safe path resolver (secure_relative_open) holds one open
/* The symlink-race-safe path resolver (vfs_resolve_open) holds one open
* dirfd per path component while it walks a path, plus an ancestor-dirfd cache
* -- far more descriptors than legacy rsync's single open(). On a host with a
* low default soft limit (e.g. OpenBSD's 128) a deep tree can hit EMFILE.
@@ -1814,6 +1812,8 @@ int main(int argc,char *argv[])
raw_argc = argc;
raw_argv = argv;
vfs_init();
raise_fd_limit();
#ifdef HAVE_SIGACTION
+12 -16
View File
@@ -27,7 +27,6 @@
extern int module_id;
extern int local_server;
extern int sanitize_paths;
extern int operator_path_resolve;
extern int trust_sender_args;
extern int trust_sender_filter;
extern unsigned int module_dirlen;
@@ -61,7 +60,7 @@ int preserve_executability = 0;
int preserve_devices = 0;
int preserve_specials = 0;
int drop_devices = 0;
char *confine_root = NULL; /* --confine-root: see syscall.c */
char *confine_root = NULL; /* --confine-root: see vfs/dirstack.c */
unsigned int confine_rootlen = 0;
int preserve_uid = 0;
int preserve_gid = 0;
@@ -123,7 +122,7 @@ int am_daemon = 0;
* clientserver.c. NOT set for the daemon-level "daemon chroot = /X"
* chroot: that confines path resolution to /X, but module paths
* /X/modA, /X/modB, etc. are not chroot boundaries, so the per-module
* symlink-race defenses (secure_relative_open() / do_*_at() in
* symlink-race defenses (vfs_resolve_open() / do_*_at() in
* syscall.c, gated by `am_daemon && !am_chrooted`) must still fire
* even when the daemon is inside a daemon chroot. */
int am_chrooted = 0;
@@ -331,7 +330,7 @@ static struct output_struct debug_words[COUNT_DEBUG+1] = {
};
static int verbose = 0;
static int do_stats = 0;
static int vfs_stats = 0;
static int do_progress = 0;
static int daemon_opt; /* sets am_daemon after option error-reporting */
static int F_option_cnt = 0;
@@ -621,7 +620,7 @@ static struct poptOption long_options[] = {
{"quiet", 'q', POPT_ARG_NONE, 0, 'q', 0, 0 },
{"motd", 0, POPT_ARG_VAL, &output_motd, 1, 0, 0 },
{"no-motd", 0, POPT_ARG_VAL, &output_motd, 0, 0, 0 },
{"stats", 0, POPT_ARG_NONE, &do_stats, 0, 0, 0 },
{"stats", 0, POPT_ARG_NONE, &vfs_stats, 0, 0, 0 },
{"human-readable", 'h', POPT_ARG_NONE, 0, 'h', 0, 0},
{"no-human-readable",0, POPT_ARG_VAL, &human_readable, 0, 0, 0},
{"no-h", 0, POPT_ARG_VAL, &human_readable, 0, 0, 0},
@@ -2178,7 +2177,7 @@ int parse_arguments(int *argc_p, const char ***argv_p)
set_output_verbosity(verbose, DEFAULT_PRIORITY);
if (do_stats) {
if (vfs_stats) {
parse_output_words(info_words, info_levels,
verbose > 1 ? "stats3" : "stats2", DEFAULT_PRIORITY);
}
@@ -2372,7 +2371,7 @@ int parse_arguments(int *argc_p, const char ***argv_p)
STRUCT_STAT st;
char prefix[SYMLINK_PREFIX_LEN]; /* NOT +1 ! */
strlcpy(prefix, SYMLINK_PREFIX, sizeof prefix); /* trim the trailing slash */
if (do_stat(prefix, &st) == 0 && S_ISDIR(st.st_mode)) {
if (vfs_stat(VFS_AT_FDCWD, prefix, &st, VFS_ALLOW_SYMLINK) == 0 && S_ISDIR(st.st_mode)) {
rprintf(FERROR, "Symlink munging is unsafe when a %s directory exists.\n",
prefix);
exit_cleanup(RERR_UNSUPPORTED);
@@ -2645,14 +2644,11 @@ int parse_arguments(int *argc_p, const char ***argv_p)
* as for --exclude-from/--include-from/--filter in exclude.c.
* A daemon reads this list from a CLIENT-requested path
* (--files-from=:LIST) and it must stay inside the module:
* operator_path_resolve makes the ownership walk also refuse a
* (trusted-owned) symlink that redirects the list outside the
* module root -- e.g. a root-owned backup symlink. No-op off a
* daemon (the module-root check only fires when am_daemon). */
int save_opr = operator_path_resolve;
operator_path_resolve = 1;
filesfrom_fd = open_no_attacker_symlinks(files_from, O_RDONLY|O_BINARY, 0);
operator_path_resolve = save_opr;
* the is_operator walk also refuses a (trusted-owned) symlink
* that redirects the list outside the module root -- e.g. a
* root-owned backup symlink. No-op off a daemon (the module-root
* check only fires when am_daemon). */
filesfrom_fd = vfs_open_owner_walk(files_from, O_RDONLY|O_BINARY, 0, 1);
if (filesfrom_fd < 0) {
snprintf(err_buf, sizeof err_buf,
"failed to open files-from file %s: %s\n",
@@ -3019,7 +3015,7 @@ void server_options(char **args, int *argc_p)
args[ac++] = "--super";
if (size_only)
args[ac++] = "--size-only";
if (do_stats)
if (vfs_stats)
args[ac++] = "--stats";
} else {
if (skip_compress)
+2 -2
View File
@@ -416,7 +416,7 @@ static int include_config(char *include, int manage_globals)
char *match = manage_globals ? "*.conf" : "*.inc";
int ret;
if (do_stat(include, &sb) < 0) {
if (vfs_stat(VFS_AT_FDCWD, include, &sb, VFS_ALLOW_SYMLINK) < 0) {
rsyserr(FLOG, errno, "unable to stat config file \"%s\"", include);
return 0;
}
@@ -583,7 +583,7 @@ static FILE *OpenConfFile( char *FileName )
/* rsyncd.conf path (--config or default): a planted symlink could redirect
* the daemon's config read. Refuse symlinks not owned by uid 0 or euid. */
{
int cfg_fd = open_no_attacker_symlinks( FileName, O_RDONLY, 0 );
int cfg_fd = vfs_open_owner_walk( FileName, O_RDONLY, 0 , 0);
OpenedFile = cfg_fd >= 0 ? fdopen( cfg_fd, "r" ) : NULL;
if( !OpenedFile && cfg_fd >= 0 )
close( cfg_fd );
+60 -71
View File
@@ -72,7 +72,6 @@ extern int fuzzy_basis;
extern struct name_num_item *xfer_sum_nni;
extern int xfer_sum_len;
extern int use_secure_symlinks;
extern int operator_path_resolve;
static struct bitbag *delayed_bits = NULL;
static int phase = 0, redoing = 0;
@@ -87,7 +86,7 @@ static int updating_basis_or_equiv;
/* Open a basis/output path that may legitimately be an operator-trusted
* ABSOLUTE path -- e.g. an absolute --partial-dir ("a directory reserved for
* partial-dir work") or --backup-dir. secure_relative_open() deliberately
* partial-dir work") or --backup-dir. vfs_resolve_open() deliberately
* rejects an absolute relpath, so feeding it the whole absolute partialptr
* (with a NULL basedir) returns EINVAL: the basis fd is then -1, no basis is
* mapped, and receive_data() omits every matched block from the whole-file
@@ -99,7 +98,7 @@ static int updating_basis_or_equiv;
* (trusted) and leaf and confine just the leaf -- exactly how secure_relative_
* open already trusts an absolute basedir while O_NOFOLLOW-confining the leaf.
* Anything else is a straight pass-through that preserves the strict contract. */
static int secure_basis_open(const char *basedir, const char *relpath, int flags, mode_t mode)
static int secure_basis_open(const char *basedir, const char *relpath, int flags, mode_t mode, int is_operator)
{
extern int am_daemon, am_chrooted;
extern unsigned int module_dirlen;
@@ -107,25 +106,25 @@ static int secure_basis_open(const char *basedir, const char *relpath, int flags
/* "insecure links = yes": restore the 3.2.7 plain open so an operator/peer
* alt-dest basis follows symlinks like legacy rsync, the same opt-out the
* other daemon symlink sites honour. */
if (symlink_optout_allowed()) {
if (vfs_symlink_optout_allowed()) {
if (basedir) {
char fullpath[MAXPATHLEN];
if (pathjoin(fullpath, sizeof fullpath, basedir, relpath) >= sizeof fullpath) {
errno = ENAMETOOLONG;
return -1;
}
return do_open(fullpath, flags, mode);
return vfs_open(fullpath, flags, mode);
}
return do_open(relpath, flags, mode);
return vfs_open(relpath, flags, mode);
}
/* A peer-supplied --partial-dir basis/staging path (operator_path_resolve set
* by recv_files) may be absolute (module_dir-prefixed on a non-chroot daemon)
* and traverse a symlink the secure_relative_open path can't confine: resolve
* it with the ownership walk, which follows a uid0/euid-owned symlink but
* refuses a foreign one AND (via abspath_excluded_by_module) refuses a target
* the module's exclude hides -- closing the partial-dir exclude bypass. */
if (operator_path_resolve) {
/* A peer-supplied --partial-dir basis/staging path (is_operator, set by the
* recv_files caller) may be absolute (module_dir-prefixed on a non-chroot
* daemon) and traverse a symlink the vfs_resolve_open path can't confine:
* resolve it with the ownership walk, which follows a uid0/euid-owned symlink
* but refuses a foreign one AND (via abspath_outside_confinement) refuses a
* target the module's exclude hides -- closing the partial-dir exclude bypass. */
if (is_operator) {
char fullpath[MAXPATHLEN];
const char *p = relpath;
if (basedir) {
@@ -135,7 +134,7 @@ static int secure_basis_open(const char *basedir, const char *relpath, int flags
}
p = fullpath;
}
return open_no_attacker_symlinks(p, flags, mode);
return vfs_open_owner_walk(p, flags, mode, is_operator);
}
/* The confined resolver is needed for the sanitizing daemon
@@ -147,7 +146,7 @@ static int secure_basis_open(const char *basedir, const char *relpath, int flags
* "use chroot = yes" makes the kernel root the boundary, so there an alt-dest
* basis like --link-dest=../01 must resolve against the cwd as a bare open did
* before the hardening (confining it would reject the legitimate sibling
* "..", #915). The re-anchoring in secure_relative_open() covers the
* "..", #915). The re-anchoring in vfs_resolve_open() covers the
* in-module ".." climb for the inner-module case too. */
if (!am_daemon || (am_chrooted && !module_dirlen)) {
if (basedir) {
@@ -156,9 +155,9 @@ static int secure_basis_open(const char *basedir, const char *relpath, int flags
errno = ENAMETOOLONG;
return -1;
}
return do_open(fullpath, flags, mode);
return vfs_open(fullpath, flags, mode);
}
return do_open(relpath, flags, mode);
return vfs_open(relpath, flags, mode);
}
if (!basedir && relpath && *relpath == '/') {
@@ -178,9 +177,9 @@ static int secure_basis_open(const char *basedir, const char *relpath, int flags
dirbuf[dlen] = '\0';
dir = dirbuf;
}
return secure_relative_open(dir, leaf, flags, mode);
return vfs_resolve_open(dir, leaf, flags, mode);
}
return secure_relative_open(basedir, relpath, flags, mode);
return vfs_resolve_open(basedir, relpath, flags, mode);
}
/* Keep the ownership policy for every attempt to open a one-inplace partial
@@ -188,13 +187,8 @@ static int secure_basis_open(const char *basedir, const char *relpath, int flags
* must not downgrade an operator-path open to the ordinary path resolver. */
static int secure_recv_open(const char *path, int flags, mode_t mode, int owner_walk)
{
int fd, save = operator_path_resolve;
if (owner_walk)
operator_path_resolve = 1;
fd = secure_basis_open(NULL, path, flags, mode);
operator_path_resolve = save;
return fd;
return secure_basis_open(NULL, path, flags, mode,
owner_walk ? VFS_OPERATOR_PATH : 0);
}
/* Open a read-only regular file for an in-place update without leaving its
@@ -216,7 +210,7 @@ static int open_readonly_inplace(const char *fname, int one_inplace)
cfd = secure_recv_open(fname, O_RDONLY|O_NOFOLLOW, 0, one_inplace);
if (cfd < 0)
goto failed;
if (do_fstat(cfd, &cst) < 0 || !S_ISREG(cst.st_mode)) {
if (vfs_fstat(cfd, &cst) < 0 || !S_ISREG(cst.st_mode)) {
errno = EACCES; /* refused: not the read-only regular file we recover */
goto failed;
}
@@ -252,10 +246,10 @@ static int open_readonly_inplace(const char *fname, int one_inplace)
/* Local and chrooted transfers retain the existing pathname semantics.
* Note the S_ISREG test here is a type check on a stable path, NOT race
* protection: do_stat() follows a leaf symlink and each call below
* protection: vfs_stat() follows a leaf symlink and each call below
* re-resolves the name. The fd-based branch above is the one that
* pins an inode; a chroot is what confines this one. */
if (do_stat(fname, &cst) < 0) {
if (vfs_stat(VFS_AT_FDCWD, fname, &cst, VFS_ALLOW_SYMLINK) < 0) {
errno = EACCES;
return -1;
}
@@ -264,11 +258,11 @@ static int open_readonly_inplace(const char *fname, int one_inplace)
return -1;
}
prior_mode = cst.st_mode & CHMOD_BITS;
if (do_chmod_at(fname, prior_mode | S_IWUSR) < 0)
if (vfs_chmod(VFS_AT_FDCWD, fname, prior_mode | S_IWUSR, 0) < 0)
return -1;
fd = do_open(fname, O_WRONLY, 0600);
fd = vfs_open(fname, O_WRONLY, 0600);
open_errno = errno;
if (do_chmod_at(fname, prior_mode) < 0) {
if (vfs_chmod(VFS_AT_FDCWD, fname, prior_mode, 0) < 0) {
restore_errno = errno;
if (fd >= 0)
close(fd);
@@ -418,34 +412,34 @@ int open_tmpfile(char *fnametmp, const char *fname, struct file_struct *file)
* access to ensure that there is no race condition. They will be
* correctly updated after the right owner and group info is set.
* (Thanks to snabb@epipe.fi for pointing this out.) */
/* For any non-chrooted receiver (secure_relpath_active()), create the
/* For any non-chrooted receiver (vfs_relpath_active()), create the
* temp file securely so a parent-symlink race can't redirect it. When
* the temp lives in the entry's own dir (the common case, no --temp-dir)
* use the cached held dir fd; otherwise fall back to secure_mkstemp. An
* use the cached held dir fd; otherwise fall back to vfs_secure_mkstemp. An
* operator-supplied --temp-dir (tmpdir) gets the ownership-walk resolver
* (it may legitimately point outside the tree); the deep-entry-dir fallback,
* when the held-dirfd cache declines, gets the strict transfer-path one. */
if (secure_relpath_active()) {
int dfd = held_dfd_for(fnametmp, file);
if (vfs_relpath_active()) {
int dfd = vfs_cached_dirfd(fnametmp, file);
if (dfd >= 0) {
char *slash = strrchr(fnametmp, '/');
fd = do_mkstemp_atfd(dfd, slash ? slash + 1 : fnametmp,
fd = vfs_mkstemp_atfd(dfd, slash ? slash + 1 : fnametmp,
(file->mode|added_perms) & INITACCESSPERMS);
} else
fd = secure_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS,
fd = vfs_secure_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS,
tmpdir != NULL);
} else
fd = do_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS);
fd = vfs_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS);
#if 0
/* In most cases parent directories will already exist because their
* information should have been previously transferred, but that may
* not be the case with -R */
if (fd == -1 && relative_paths && errno == ENOENT
&& make_path(fnametmp, MKP_SKIP_SLASH | MKP_DROP_NAME) == 0) {
&& vfs_make_path(fnametmp, MKP_SKIP_SLASH | MKP_DROP_NAME, 0) == 0) {
/* Get back to name with XXXXXX in it. */
get_tmpname(fnametmp, fname, False);
fd = do_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS);
fd = vfs_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS);
}
#endif
@@ -476,14 +470,14 @@ static int receive_data(int f_in, char *fname_r, int fd_r, OFF_T size_r,
if (preallocate_files && fd != -1 && total_size > 0 && (!inplace_sizing || total_size > size_r)) {
/* Try to preallocate enough space for file's eventual length. Can
* reduce fragmentation on filesystems like ext4, xfs, and NTFS. */
if ((preallocated_len = do_fallocate(fd, 0, total_size)) < 0)
rsyserr(FWARNING, errno, "do_fallocate %s", full_fname(fname));
if ((preallocated_len = vfs_fallocate(fd, 0, total_size)) < 0)
rsyserr(FWARNING, errno, "vfs_fallocate %s", full_fname(fname));
} else
#endif
if (inplace_sizing) {
#ifdef HAVE_FTRUNCATE
/* The most compatible way to create a sparse file is to start with no length. */
if (sparse_files > 0 && whole_file && fd >= 0 && do_ftruncate(fd, 0) == 0)
if (sparse_files > 0 && whole_file && fd >= 0 && vfs_ftruncate(fd, 0) == 0)
preallocated_len = 0;
else
#endif
@@ -526,7 +520,7 @@ static int receive_data(int f_in, char *fname_r, int fd_r, OFF_T size_r,
}
}
offset = sum.flength;
if (fd != -1 && (j = do_lseek(fd, offset, SEEK_SET)) != offset) {
if (fd != -1 && (j = vfs_lseek(fd, offset, SEEK_SET)) != offset) {
rsyserr(FERROR_XFER, errno, "lseek of %s returned %s, not %s",
full_fname(fname), big_num(j), big_num(offset));
exit_cleanup(RERR_FILEIO);
@@ -650,7 +644,7 @@ static int receive_data(int f_in, char *fname_r, int fd_r, OFF_T size_r,
* preallocate_files: total_size could have been an overestimate.
* Cut off any extra preallocated zeros from dest file. */
if ((inplace_sizing || preallocated_len > offset) && fd != -1 && !IS_DEVICE(file->mode)) {
if (do_ftruncate(fd, offset) < 0)
if (vfs_ftruncate(fd, offset) < 0)
rsyserr(FERROR_XFER, errno, "ftruncate failed on %s", full_fname(fname));
}
#endif
@@ -703,9 +697,9 @@ static void handle_delayed_updates(char *local_name)
* walk so a symlinked partial-dir can't move a file out of
* an excluded subtree. */
int rret;
operator_path_resolve = 1;
rret = do_rename_at(partialptr, fname);
operator_path_resolve = 0;
/* partialptr is the operator-supplied --partial-dir source (owner
* walk); fname is the transfer destination (secure receiver resolve). */
rret = vfs_rename_at(partialptr, fname, VFS_OPERATOR_PATH, 0);
if (rret < 0) {
rsyserr(FERROR_XFER, errno,
"rename failed for %s (from %s)",
@@ -1068,12 +1062,12 @@ int recv_files(int f_in, int f_out, char *local_name)
&& fnamecmp && *fnamecmp != '/') {
/* The relative partial path contains peer-derived directory
* components. It is not an operator-trusted path as a whole. */
fd1 = secure_relative_open(NULL, fnamecmp, O_RDONLY, 0);
} else if (!basedir && (bdfd = held_dfd_for(fnamecmp, file)) >= 0) {
fd1 = vfs_resolve_open(NULL, fnamecmp, O_RDONLY, 0);
} else if (!basedir && (bdfd = vfs_cached_dirfd(fnamecmp, file)) >= 0) {
const char *slash;
assert(fnamecmp != NULL); /* set on every path above */
slash = strrchr(fnamecmp, '/');
fd1 = do_open_atfd(bdfd, slash ? slash + 1 : fnamecmp, O_RDONLY, 0);
fd1 = vfs_open_atfd(bdfd, slash ? slash + 1 : fnamecmp, O_RDONLY, 0);
} else {
/* An operator-supplied basis -- a --partial-dir, or an
* alt-dest basedir (--copy-dest/--compare-dest/--link-dest) --
@@ -1084,10 +1078,9 @@ int recv_files(int f_in, int f_out, char *local_name)
* and the operator's own uid0/euid symlinks. A daemon keeps its
* stronger confinement branch in secure_basis_open(), so only
* route the alt-dest basedir read through the walk off-daemon. */
if ((basedir && !am_daemon) || fnamecmp_type == FNAMECMP_PARTIAL_DIR)
operator_path_resolve = 1;
fd1 = secure_basis_open(basedir, fnamecmp, O_RDONLY, 0);
operator_path_resolve = 0;
fd1 = secure_basis_open(basedir, fnamecmp, O_RDONLY, 0,
((basedir && !am_daemon) || fnamecmp_type == FNAMECMP_PARTIAL_DIR) ? VFS_OPERATOR_PATH : 0);
}
}
if (fnamecmp_type == FNAMECMP_PARTIAL_DIR && fd1 == -1) {
@@ -1110,7 +1103,7 @@ int recv_files(int f_in, int f_out, char *local_name)
if (fnamecmp != fname) {
fnamecmp = fname;
fnamecmp_type = FNAMECMP_FNAME;
fd1 = do_open_nofollow(fnamecmp, O_RDONLY);
fd1 = vfs_open_nofollow(fnamecmp, O_RDONLY);
}
if (fd1 == -1 && basis_dir[0]) {
@@ -1118,10 +1111,8 @@ int recv_files(int f_in, int f_out, char *local_name)
basedir = basis_dir[0];
fnamecmp = fname;
fnamecmp_type = FNAMECMP_BASIS_DIR_LOW;
if (!am_daemon)
operator_path_resolve = 1;
fd1 = secure_basis_open(basedir, fnamecmp, O_RDONLY, 0);
operator_path_resolve = 0;
fd1 = secure_basis_open(basedir, fnamecmp, O_RDONLY, 0,
!am_daemon ? VFS_OPERATOR_PATH : 0);
}
}
@@ -1142,7 +1133,7 @@ int recv_files(int f_in, int f_out, char *local_name)
if (fd1 == -1) {
st.st_mode = 0;
st.st_size = 0;
} else if (do_fstat(fd1,&st) != 0) {
} else if (vfs_fstat(fd1,&st) != 0) {
rsyserr(FERROR_XFER, errno, "fstat %s failed",
full_fname(fnamecmp));
discard_receive_data(f_in, file);
@@ -1194,18 +1185,18 @@ int recv_files(int f_in, int f_out, char *local_name)
/* We now check to see if we are writing the file "inplace" */
if (inplace || one_inplace) {
fnametmp = one_inplace ? partialptr : fname;
/* For any non-chrooted receiver (secure_relpath_active()),
/* For any non-chrooted receiver (vfs_relpath_active()),
* use secure open to prevent symlink race attacks where an
* attacker could switch a directory to a symlink between
* path validation and file open. */
/* one_inplace stages into the operator/peer --partial-dir path:
* resolve it with the ownership walk (exclude-aware) so it can't be
* redirected through a symlink into an excluded subtree. */
if (secure_relpath_active())
if (vfs_relpath_active())
fd2 = secure_recv_open(fnametmp, O_WRONLY|O_CREAT, 0600,
one_inplace);
else
fd2 = do_open(fnametmp, O_WRONLY|O_CREAT, 0600);
fd2 = vfs_open(fnametmp, O_WRONLY|O_CREAT, 0600);
#ifdef linux
if (fd2 == -1 && errno == EACCES) {
/* Maybe the error was due to protected_regular setting? */
@@ -1213,7 +1204,7 @@ int recv_files(int f_in, int f_out, char *local_name)
fd2 = secure_recv_open(fnametmp, O_WRONLY, 0600,
one_inplace);
else
fd2 = do_open(fnametmp, O_WRONLY, 0600);
fd2 = vfs_open(fnametmp, O_WRONLY, 0600);
}
#endif
if (fd2 == -1 && errno == EACCES) {
@@ -1292,9 +1283,7 @@ int recv_files(int f_in, int f_out, char *local_name)
/* Unlink the consumed --partial-dir basis through the
* exclude-aware ownership walk (a symlinked partial-dir
* must not delete a file in an excluded subtree). */
operator_path_resolve = 1;
do_unlink_at(partialptr);
operator_path_resolve = 0;
vfs_unlink(VFS_AT_FDCWD, partialptr, VFS_OPERATOR_PATH);
}
handle_partial_dir(partialptr, PDIR_DELETE);
}
@@ -1304,7 +1293,7 @@ int recv_files(int f_in, int f_out, char *local_name)
"Unable to create partial-dir for %s -- discarding %s.\n",
local_name ? local_name : f_name(file, NULL),
recv_ok ? "completed file" : "partial file");
do_unlink_at(fnametmp);
vfs_unlink(VFS_AT_FDCWD, fnametmp, 0);
recv_ok = -1;
} else if (!finish_transfer(partialptr, fnametmp, fnamecmp, NULL,
file, recv_ok, !partial_dir))
@@ -1315,7 +1304,7 @@ int recv_files(int f_in, int f_out, char *local_name)
} else
partialptr = NULL;
} else if (!one_inplace)
do_unlink_at(fnametmp);
vfs_unlink(VFS_AT_FDCWD, fnametmp, 0);
cleanup_disable();
+64 -39
View File
@@ -37,7 +37,6 @@ extern int omit_dir_times;
extern int omit_link_times;
extern int am_root;
extern int am_server;
extern int operator_path_resolve;
extern int am_daemon;
extern int am_sender;
extern int am_receiver;
@@ -514,6 +513,13 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp,
int op_leaf_fd = -1; /* O_NOFOLLOW fd pinning a cross-tree operator leaf */
int op_pin = 0; /* drive chmod/chown off op_leaf_fd for a cross-tree leaf */
int op_refuse = 0; /* pin open hit the symlink-race signal: refuse, don't redirect */
/* The ownership walk for the path-based chmod/chown fallbacks below. op_pin
* covers a reg/dir/fifo leaf with a pinned fd, but a symlink or device leaf
* never enters it, and a non-root operator can fail the pin open with a plain
* EACCES and fall through -- both must still resolve the operator path via the
* walk rather than a bare lchown()/chmod(). (vfs_chmod's operator branch skips
* S_ISLNK itself, so a symlink-as-object keeps the lchmod/setattrlist path.) */
int op_vfs = (flags & ATTRS_OPERATOR_PATH) ? VFS_OPERATOR_PATH : 0;
#if defined SUPPORT_XATTRS || defined SUPPORT_ACLS
int held_fd = -1; /* held O_NOFOLLOW fd for fd-based xattr/ACL ops, or -1 */
int xattr_refuse = 0; /* no confined fd for a slashed path: skip path-based xattr/ACL */
@@ -526,7 +532,7 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp,
/* Stat through the entry's held dir fd (like gen_entry_stat) so we
* don't re-walk the full path here; link_stat_at folds in no
* fake-super xattr, so only when am_root >= 0. */
if (am_root >= 0 && (sdfd = held_dfd_for(fname, file)) >= 0) {
if (am_root >= 0 && (sdfd = vfs_cached_dirfd(fname, file)) >= 0) {
const char *sl = strrchr(fname, '/');
sret = link_stat_at(sdfd, sl ? sl + 1 : fname, &sx2.st, 0);
} else
@@ -546,7 +552,7 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp,
* issue single-component *at() calls against it instead of re-resolving
* the full path each time. -1 => fall back to the full-path wrappers
* (cross-tree path such as --temp-dir/--backup-dir, or gated off). */
dfd = held_dfd_for(fname, file);
dfd = vfs_cached_dirfd(fname, file);
if (dfd >= 0) {
const char *slash = strrchr(fname, '/');
leaf = slash ? slash + 1 : fname;
@@ -569,30 +575,26 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp,
))
held_fd = openat(dfd, leaf, O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_NOCTTY | O_CLOEXEC);
/* If the held-fd pin above missed (no cached dir fd -- a path deeper than the
* dirfd cache, or a raced leaf) but we are a confined receiver on a
* non-operator path, re-pin the leaf through the secure resolver so the
* xattr/ACL ops below drive fsetxattr off a confined fd -- NOT a raw path-based
* lsetxattr, which re-resolves the parent and lets a flipped dest/sub symlink
* land the xattr OUTSIDE the tree (copy-xattrs-symlink-race). If the secure
* re-pin also fails (a genuinely raced parent/leaf symlink), held_fd stays -1
* and xattr_refuse skips the path-based ops rather than redirecting them.
* (chmod/chown/times stay safe via their secure path wrappers; operator paths
* use op_pin/op_refuse below.) */
if (held_fd < 0 && !operator_path_resolve && secure_relpath_active()
/* If the held-fd pin above missed (no cached dir fd, or a raced leaf) but we
* are a confined receiver on a non-operator path, re-pin the leaf through the
* secure resolver so the xattr/ACL ops below drive fsetxattr off a confined fd
* -- NOT a raw path-based lsetxattr, which re-resolves the parent and lets a
* flipped dest/sub symlink land the xattr OUTSIDE the tree (copy-xattrs-
* symlink-race). A confined receiver normally always has the cached pin; a
* miss here is a raced parent/leaf. If the secure re-pin also fails (the
* parent/leaf is a symlink), held_fd stays -1 and xattr_refuse below skips the
* path-based ops rather than redirecting them. (chmod/chown/times stay safe
* via their secure path wrappers; operator paths use op_pin/op_refuse.) */
if (held_fd < 0 && !(flags & ATTRS_OPERATOR_PATH) && vfs_relpath_active()
&& (S_ISREG(sxp->st.st_mode) || S_ISDIR(sxp->st.st_mode) || S_ISFIFO(sxp->st.st_mode))
&& (preserve_xattrs || am_root < 0
# ifdef SUPPORT_ACLS
|| (preserve_acls && am_root >= 0)
# endif
)) {
int odir = 0;
# ifdef O_DIRECTORY
if (S_ISDIR(sxp->st.st_mode))
odir = O_DIRECTORY;
# endif
held_fd = secure_relative_open(NULL, fname,
O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_NOCTTY | O_CLOEXEC | odir, 0);
held_fd = vfs_resolve_open(NULL, fname,
O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_NOCTTY | O_CLOEXEC
| (S_ISDIR(sxp->st.st_mode) ? O_DIRECTORY : 0), 0);
if (held_fd < 0 && strchr(fname, '/'))
xattr_refuse = 1;
}
@@ -607,16 +609,16 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp,
* operator owner-walk resolver and drive fchmod/fchown off that fd. A raced
* symlink leaf makes the open fail, leaving op_leaf_fd == -1: the metadata op
* is then refused, never redirected. --insecure-links opts back out (the
* resolver in do_open_at honours it), and a genuine symlink leaf (a symlink
* resolver in vfs_open_at honours it), and a genuine symlink leaf (a symlink
* backup) keeps the existing l-variant path. */
/* Gate on the INTENDED type (new_mode), not the on-disk type (sxp->st): the
* attacker controls the latter via the flip, and a dir component that has
* just been flipped to a symlink must still take the pinned path so the
* O_NOFOLLOW open refuses it -- otherwise the lchown would launder it. */
op_pin = operator_path_resolve && dfd < 0 && !symlink_optout_allowed()
op_pin = (flags & ATTRS_OPERATOR_PATH) && dfd < 0 && !vfs_symlink_optout_allowed()
&& (S_ISREG(new_mode) || S_ISDIR(new_mode) || S_ISFIFO(new_mode));
if (op_pin) {
op_leaf_fd = do_open_at(fname, O_RDONLY | O_NONBLOCK | O_NOCTTY | O_CLOEXEC, 0);
op_leaf_fd = vfs_open_at(fname, O_RDONLY | O_NONBLOCK | O_NOCTTY | O_CLOEXEC, 0, VFS_OPERATOR_PATH);
/* When running as root (the uid-0 trust-laundering case) an O_RDONLY open
* of a real owned reg/dir/fifo leaf never fails for permission reasons, so
* ANY failure here means the leaf is being raced (a symlink refused by
@@ -647,6 +649,29 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp,
if (daemon_chmod_modes && !S_ISLNK(new_mode))
new_mode = tweak_mode(new_mode, daemon_chmod_modes);
#if (defined SUPPORT_XATTRS || defined SUPPORT_ACLS) && defined STRICT_CONFINEMENT
/* Enforce the pin/re-pin invariant: for a confined, pinnable, non-operator
* leaf with metadata work pending, the held-fd pin/re-pin above must have
* produced a confined fd (held_fd >= 0) or set xattr_refuse. Reaching here
* with neither means the xattr/ACL setters would take their raw path-based
* branch (the copy-xattrs fallback class) -- abort so the suite catches the
* regression. The clause mirrors the pin condition (excluding no-metadata-
* work, symlink/operator/opt-out, etc.); it is intentionally a touch broader
* than "the next setter definitely path-writes" (set_xattr is skipped when
* fnamecmp == NULL; a native ACL may still take a dirfd+leaf route), but a
* confined slashed path only reaches here once the invariant is already
* broken, so it cannot false-abort a legitimate transfer. */
if (held_fd < 0 && !op_refuse && !xattr_refuse && !(flags & ATTRS_OPERATOR_PATH)
&& (S_ISREG(sxp->st.st_mode) || S_ISDIR(sxp->st.st_mode) || S_ISFIFO(sxp->st.st_mode))
&& (preserve_xattrs || am_root < 0
# ifdef SUPPORT_ACLS
|| (preserve_acls && am_root >= 0)
# endif
)
&& vfs_must_be_confined(fname, 0))
vfs_strict_confine_fail(fname, "xattr/ACL set");
#endif
#ifdef SUPPORT_ACLS
if (preserve_acls && !S_ISLNK(file->mode) && !ACL_READY(*sxp) && !op_refuse && !xattr_refuse)
get_acl_fdat(held_fd, dfd, leaf, fname, sxp);
@@ -676,10 +701,10 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp,
if (am_root >= 0) {
uid_t uid = change_uid ? (uid_t)F_OWNER(file) : sxp->st.st_uid;
gid_t gid = change_gid ? (gid_t)F_GROUP(file) : sxp->st.st_gid;
if ((op_leaf_fd >= 0 ? do_fchown(op_leaf_fd, uid, gid)
if ((op_leaf_fd >= 0 ? vfs_fchown(op_leaf_fd, uid, gid)
: op_refuse ? (errno = ELOOP, -1)
: dfd >= 0 ? do_lchown_atfd(dfd, leaf, uid, gid)
: do_lchown_at(fname, uid, gid)) != 0) {
: dfd >= 0 ? vfs_lchown(dfd, leaf, uid, gid, 0)
: vfs_lchown(VFS_AT_FDCWD, fname, uid, gid, op_vfs)) != 0) {
/* We shouldn't have attempted to change uid
* or gid unless have the privilege. */
rsyserr(FERROR_XFER, errno, "%s %s failed",
@@ -751,13 +776,13 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp,
if (crtimes_ndx && !(flags & ATTRS_SKIP_CRTIME)) {
time_t file_crtime = F_CRTIME(file);
if (sxp->crtime == 0)
sxp->crtime = get_create_time(fname, &sxp->st);
sxp->crtime = vfs_get_create_time(fname, &sxp->st);
if (!same_time(sxp->crtime, 0L, file_crtime, 0L)) {
if (
#ifdef HAVE_GETATTRLIST
do_setattrlist_crtime(fname, file_crtime) == 0
vfs_setattrlist_crtime(fname, file_crtime) == 0
#elif defined __CYGWIN__
do_SetFileTime(fname, file_crtime) == 0
vfs_SetFileTime(fname, file_crtime) == 0
#else
#error Unknown crtimes implementation
#endif
@@ -770,7 +795,7 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp,
int ret;
#ifdef HAVE_FUTIMENS
if (op_leaf_fd >= 0)
ret = do_futimens(op_leaf_fd, &sx2.st);
ret = vfs_futimens(op_leaf_fd, &sx2.st);
else
#endif
if (op_refuse)
@@ -806,10 +831,10 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp,
#ifdef HAVE_CHMOD
if (!BITS_EQUAL(sxp->st.st_mode, new_mode, CHMOD_BITS)) {
int ret = am_root < 0 ? 0
: op_leaf_fd >= 0 ? do_fchmod(op_leaf_fd, new_mode)
: op_leaf_fd >= 0 ? vfs_fchmod(op_leaf_fd, new_mode)
: op_refuse ? (errno = ELOOP, -1)
: dfd >= 0 && !S_ISLNK(new_mode) ? do_chmod_atfd(dfd, leaf, new_mode)
: do_chmod_at(fname, new_mode);
: dfd >= 0 && !S_ISLNK(new_mode) ? vfs_chmod(dfd, leaf, new_mode, 0)
: vfs_chmod(VFS_AT_FDCWD, fname, new_mode, op_vfs);
if (ret < 0) {
rsyserr(FERROR_XFER, errno,
"failed to set permissions on %s",
@@ -907,10 +932,8 @@ int finish_transfer(const char *fname, const char *fnametmp,
* dirfd, so resolve its metadata through the ownership walk (op_pin); a
* flipped temp-dir parent then can't redirect the chmod/chown/times/etc.
* (in-tree temps keep their held dirfd, so op_pin stays off there). */
operator_path_resolve = 1;
set_file_attrs(fnametmp, file, NULL, fnamecmp,
ok_to_set_time ? ATTRS_ACCURATE_TIME : ATTRS_SKIP_MTIME | ATTRS_SKIP_ATIME | ATTRS_SKIP_CRTIME);
operator_path_resolve = 0;
ATTRS_OPERATOR_PATH | (ok_to_set_time ? ATTRS_ACCURATE_TIME : ATTRS_SKIP_MTIME | ATTRS_SKIP_ATIME | ATTRS_SKIP_CRTIME));
/* move tmp file over real file */
if (DEBUG_GTE(RECV, 1))
@@ -922,7 +945,7 @@ int finish_transfer(const char *fname, const char *fnametmp,
full_fname(fnametmp), fname);
if (!partialptr || (ret == -2 && temp_copy_name)
|| robust_rename(fnametmp, partialptr, NULL, file->mode, file) < 0)
do_unlink_at(fnametmp);
vfs_unlink(VFS_AT_FDCWD, fnametmp, 0);
return 0;
}
if (ret == 0) {
@@ -938,7 +961,9 @@ int finish_transfer(const char *fname, const char *fnametmp,
ok_to_set_time ? ATTRS_ACCURATE_TIME : ATTRS_SKIP_MTIME | ATTRS_SKIP_ATIME | ATTRS_SKIP_CRTIME);
if (temp_copy_name) {
if (do_rename_at(fnametmp, fname) < 0) {
/* temp_copy_name and fname both live in the dest tree here; flag 0 lets
* vfs_twopath_side confine each side (absolute=owner-walk, relative=secure). */
if (vfs_rename_at(fnametmp, fname, 0, 0) < 0) {
rsyserr(FERROR_XFER, errno, "rename %s -> \"%s\"",
full_fname(fnametmp), fname);
return 0;
+6 -4
View File
@@ -223,6 +223,7 @@
#define ATTRS_SKIP_MTIME (1<<1)
#define ATTRS_ACCURATE_TIME (1<<2)
#define ATTRS_SKIP_ATIME (1<<3)
#define ATTRS_OPERATOR_PATH (1<<4) /* fname is a cross-tree operator path: pin its leaf (op_pin) */
#define ATTRS_SKIP_CRTIME (1<<5)
#define MSG_FLUSH 2
@@ -1237,12 +1238,13 @@ struct name_num_obj {
#ifndef __cplusplus
#include "proto.h"
#include "vfs/vfs.h"
#endif
#ifndef SUPPORT_XATTRS
#define x_stat(fn,fst,xst) do_stat(fn,fst)
#define x_lstat(fn,fst,xst) do_lstat(fn,fst)
#define x_fstat(fd,fst,xst) do_fstat(fd,fst)
#define x_stat(fn,fst,xst,vfsflags) vfs_stat(VFS_AT_FDCWD, fn, fst, vfsflags)
#define x_lstat(fn,fst,xst,vfsflags) vfs_lstat(VFS_AT_FDCWD, fn, fst, vfsflags)
#define x_fstat(fd,fst,xst) vfs_fstat(fd,fst)
#endif
/* We have replacement versions of these if they're missing. */
@@ -1281,7 +1283,7 @@ extern int errno;
#ifdef HAVE_READLINK
#define SUPPORT_LINKS 1
#if !defined NO_SYMLINK_XATTRS && !defined NO_SYMLINK_USER_XATTRS
#define do_readlink(path, buf, bufsiz) readlink(path, buf, bufsiz)
#define vfs_readlink(path, buf, bufsiz) readlink(path, buf, bufsiz)
#endif
#endif
#ifdef HAVE_LINK
+32 -32
View File
@@ -85,7 +85,7 @@ static int secure_sender_parent_fd(struct file_struct *file, const char *fname,
/* "insecure links = yes" / --insecure-links: restore the 3.2.7 plain re-stat
* by declining the confined parent (errno=0 makes the caller use do_lstat). */
if (symlink_optout_allowed()) {
if (vfs_symlink_optout_allowed()) {
errno = 0;
return -1;
}
@@ -127,21 +127,21 @@ static int secure_sender_parent_fd(struct file_struct *file, const char *fname,
#endif
while (*rel == '/')
rel++;
return secure_relative_open("/", rel,
return vfs_resolve_open("/", rel,
O_RDONLY | O_DIRECTORY, 0);
}
/* held_dir_path_fd returns a cache-OWNED fd; the caller closes
/* vfs_path_dirfd returns a cache-OWNED fd; the caller closes
* what we return, so hand back an owned dup and leave the cache's
* dirfd intact. An uncacheable (very deep) dir declines with
* errno 0 -- fall back to the full confined walk (an owned fd,
* matching the sender's content open) so deep paths stay confined
* too; a real error propagates. */
dfd = held_dir_path_fd(NULL, dir);
dfd = vfs_path_dirfd(NULL, dir);
if (dfd >= 0)
return dup(dfd);
if (errno != 0)
return -1;
return secure_relative_open(NULL, dir, O_RDONLY | O_DIRECTORY, 0);
return vfs_resolve_open(NULL, dir, O_RDONLY | O_DIRECTORY, 0);
}
errno = 0; /* top-level file: no parent component to confine */
return -1;
@@ -176,9 +176,9 @@ static int secure_sender_parent_fd(struct file_struct *file, const char *fname,
}
memcpy(dir, relp, dlen);
dir[dlen] = '\0';
dfd = secure_relative_open(module_dir, dir, O_RDONLY | O_DIRECTORY, 0);
dfd = vfs_resolve_open(module_dir, dir, O_RDONLY | O_DIRECTORY, 0);
} else
dfd = secure_relative_open(module_dir, "", O_RDONLY | O_DIRECTORY, 0);
dfd = vfs_resolve_open(module_dir, "", O_RDONLY | O_DIRECTORY, 0);
/* The leaf is the same last component either way; take it from the caller's
* persistent fname buffer, not the local secure_path. */
@@ -195,20 +195,20 @@ static int secure_sender_parent_fd(struct file_struct *file, const char *fname,
#endif
}
/* Go through the do_*() wrapper rather than a raw unlinkat(): it carries the
* dry_run no-op and the read-only/list-only refusal that do_unlink() applies
* on the non-fd path, plus the missing-AT_FDCWD fallback. */
/* Go through the VFS wrapper rather than a raw unlinkat(): it carries the
* dry_run no-op and the read-only/list-only refusal that the plain unlink path
* applies, plus the missing-AT_FDCWD fallback. */
static int secure_remove_source_file(int dfd, const char *bname)
{
return do_unlink_atfd(dfd, bname, 0);
return vfs_unlink(dfd, bname, 0);
}
/* Open `relpath` (relative to `anchor`: NULL=cwd, else an absolute trusted root)
* with `flags`, opening the leaf via the shared held ancestor-dirfd stack
* (held_dir_path_fd) so a directory is walked once, not once per file. The leaf
* semantics are identical to secure_relative_open() -- it always O_NOFOLLOWs a
* (vfs_path_dirfd) so a directory is walked once, not once per file. The leaf
* semantics are identical to vfs_resolve_open() -- it always O_NOFOLLOWs a
* file leaf and folds in O_NOATIME, both preserved here. An uncacheable path
* (held_dir_path_fd returns -1) falls back to the full confined walk. */
* (vfs_path_dirfd returns -1) falls back to the full confined walk. */
static int sender_open_confined(const char *anchor, const char *relpath, int flags)
{
#ifdef AT_FDCWD
@@ -237,14 +237,14 @@ static int sender_open_confined(const char *anchor, const char *relpath, int fla
if (open_noatime)
flags |= O_NOATIME;
#endif
dfd = held_dir_path_fd(anchor, dir);
dfd = vfs_path_dirfd(anchor, dir);
if (dfd < 0)
return secure_relative_open(anchor, relpath, flags | O_NOFOLLOW, 0);
return vfs_resolve_open(anchor, relpath, flags | O_NOFOLLOW, 0);
return openat(dfd, bname, flags | O_NOFOLLOW, 0);
#else
/* No *at() support: secure_relative_open is a plain open() here (no walk,
/* No *at() support: vfs_resolve_open is a plain open() here (no walk,
* so nothing to amortise); use it directly to keep the anchor semantics. */
return secure_relative_open(anchor, relpath, flags | O_NOFOLLOW, 0);
return vfs_resolve_open(anchor, relpath, flags | O_NOFOLLOW, 0);
#endif
}
@@ -253,7 +253,7 @@ static int sender_open_confined(const char *anchor, const char *relpath, int fla
* O_NOFOLLOW that sender_open_confined() applies refuses an in-tree symlink the
* operator explicitly asked to follow, so resolve the link ourselves: read it,
* refuse an absolute or "../"-escaping target (a module escape), and re-resolve
* the relative target through secure_relative_open() -- which follows in-tree
* the relative target through vfs_resolve_open() -- which follows in-tree
* links and rejects an escape above the anchor -- looping for a symlink chain.
* The final open is still O_NOFOLLOW, so a raced flip at the resolved leaf is
* refused. This keeps the module boundary while honouring --copy-links. */
@@ -292,14 +292,14 @@ static int sender_open_copylinks_confined(const char *anchor, const char *relpat
* only this branch would hand it to strcmp(). */
if (am_daemon && module_dirfd >= 0 && module_dir && anchor
&& strcmp(anchor, module_dir) == 0)
pdfd = secure_relative_open_at_beneath(module_dirfd, dir,
pdfd = vfs_resolve_open_at_beneath(module_dirfd, dir,
O_RDONLY | O_DIRECTORY, 0);
else
pdfd = secure_relative_open(anchor, dir,
pdfd = vfs_resolve_open(anchor, dir,
O_RDONLY | O_DIRECTORY, 0);
if (pdfd < 0)
return -1;
n = do_readlink_atfd(pdfd, bname, tgt, sizeof tgt - 1);
n = vfs_readlink_atfd(pdfd, bname, tgt, sizeof tgt - 1);
e = errno;
if (n < 0) {
/* EINVAL: not a symlink -> the resolved target file. Open it
@@ -327,7 +327,7 @@ static int sender_open_copylinks_confined(const char *anchor, const char *relpat
errno = ELOOP;
return -1;
#else
return secure_relative_open(anchor, relpath, O_RDONLY | O_NOFOLLOW, 0);
return vfs_resolve_open(anchor, relpath, O_RDONLY | O_NOFOLLOW, 0);
#endif
}
@@ -424,8 +424,8 @@ void successful_send(int ndx)
}
if (dfd >= 0
? (copy_links ? do_stat_atfd(dfd, bname, &st) : do_lstat_atfd(dfd, bname, &st)) < 0
: (copy_links ? do_stat(fname, &st) : do_lstat(fname, &st)) < 0) {
? (copy_links ? vfs_stat(dfd, bname, &st, 0) : vfs_lstat(dfd, bname, &st, 0)) < 0
: (copy_links ? vfs_stat(VFS_AT_FDCWD, fname, &st, VFS_ALLOW_SYMLINK) : vfs_lstat(VFS_AT_FDCWD, fname, &st, VFS_ALLOW_SYMLINK)) < 0) {
failed_op = "re-lstat";
goto failed;
}
@@ -450,7 +450,7 @@ void successful_send(int ndx)
return;
}
if (dfd >= 0 ? secure_remove_source_file(dfd, bname) < 0 : do_unlink(fname) < 0) {
if (dfd >= 0 ? secure_remove_source_file(dfd, bname) < 0 : vfs_unlink(VFS_AT_FDCWD, fname, VFS_ALLOW_SYMLINK) < 0) {
failed_op = "remove";
failed:
if (errno == ENOENT)
@@ -647,13 +647,13 @@ void send_files(int f_in, int f_out)
exit_cleanup(RERR_PROTOCOL);
}
if (symlink_optout_allowed()) {
if (vfs_symlink_optout_allowed()) {
/* Module opted out of symlink confinement ("insecure links =
* yes", admin-only) -- or a non-daemon --insecure-links: legacy
* unconfined open, restoring the pre-hardening content read
* (re-opening the escape for that module; documented). */
fd = do_open_checklinks(fname);
} else if (secure_relpath_active()) {
fd = vfs_open_checklinks(fname);
} else if (vfs_relpath_active()) {
/* Open from module root to prevent TOCTOU race where
* change_pathname's chdir follows a directory symlink.
* Reconstruct the full path relative to module_dir
@@ -671,7 +671,7 @@ void send_files(int f_in, int f_out)
}
/* A module with `path = /` makes F_PATHNAME absolute, so the
* joined path starts with '/'; strip leading slashes to a
* module-relative path that secure_relative_open accepts (#897). */
* module-relative path that vfs_resolve_open accepts (#897). */
relp = secure_path;
while (*relp == '/')
relp++;
@@ -704,7 +704,7 @@ void send_files(int f_in, int f_out)
} else
fd = sender_open_confined(NULL, fname, O_RDONLY);
} else {
fd = do_open_checklinks(fname);
fd = vfs_open_checklinks(fname);
}
if (fd == -1) {
if (errno == ENOENT) {
@@ -725,7 +725,7 @@ void send_files(int f_in, int f_out)
}
/* map the local file */
if (do_fstat(fd, &st) != 0) {
if (vfs_fstat(fd, &st) != 0) {
io_error |= IOERR_GENERAL;
rsyserr(FERROR_XFER, errno, "fstat failed");
free_sums(s);
-3975
View File
File diff suppressed because it is too large. Load diff
+12 -12
View File
@@ -1,7 +1,7 @@
/*
* Test harness for do_chmod_at(). Confirms the symlink-TOCTOU
* Test harness for vfs_chmod(). Confirms the symlink-TOCTOU
* primitive used by CVE-2026-29518 (and its incomplete-fix follow-up
* for chmod) is closed by do_chmod_at(): a parent directory component
* for chmod) is closed by vfs_chmod(): a parent directory component
* being a symlink that escapes the receiver's confinement must be
* rejected, while a parent symlink that resolves *within* the tree
* must still work (so legitimate dir-symlinks are not regressed).
@@ -31,7 +31,7 @@ short info_levels[COUNT_INFO], debug_levels[COUNT_DEBUG];
static int errs = 0;
/* Does do_chmod_at()'s leaf handling refuse to follow a symlink at the final
/* Does vfs_chmod()'s leaf handling refuse to follow a symlink at the final
* component? Yes wherever AT_SYMLINK_NOFOLLOW exists; otherwise the wrapper
* falls back to a following fchmodat() (documented limitation). Mirrors the
* #ifdef ladder in do_fchmodat_nofollow. */
@@ -85,9 +85,9 @@ int main(int argc, char **argv)
return 2;
}
/* Simulate the daemon-without-chroot deployment that do_chmod_at()
/* Simulate the daemon-without-chroot deployment that vfs_chmod()
* defends. With am_daemon=0 or am_chrooted=1 the wrapper falls
* through to plain do_chmod() and the symlink-race test would be
* through to plain vfs_chmod() and the symlink-race test would be
* meaningless. */
am_daemon = 1;
am_chrooted = 0;
@@ -112,26 +112,26 @@ int main(int argc, char **argv)
* Solaris, older Cygwin, HPE NonStop, pre-5.6 Linux) -- which now follows
* an in-tree directory symlink whose target is relative and ".."-free.
* Escapes are still rejected on both paths (Scenario B). */
int rc = do_chmod_at("inside_link/sentinel", 0640);
int rc = vfs_chmod(VFS_AT_FDCWD, "inside_link/sentinel", 0640, 0);
check("A: legit dir-symlink within tree (followed)",
rc, 1, "realdir/sentinel", 0640);
/* Scenario B: parent symlink escapes the tree -- chmod must be
* rejected and the outside file's mode must be unchanged. */
rc = do_chmod_at("escape_link/sentinel", 0666);
rc = vfs_chmod(VFS_AT_FDCWD, "escape_link/sentinel", 0666, 0);
check("B: parent symlink escapes tree (the attack)",
rc, 0, "../trap/sentinel", 0600);
/* Scenario C: plain relative path with no symlink components,
* regression check that the safe wrapper doesn't break the
* normal case. */
rc = do_chmod_at("realdir/sentinel", 0644);
rc = vfs_chmod(VFS_AT_FDCWD, "realdir/sentinel", 0644, 0);
check("C: plain relative path (regression check)",
rc, 1, "realdir/sentinel", 0644);
/* Scenario D: top-level file, no parent directory component.
* Falls back to do_chmod(); should succeed. */
rc = do_chmod_at("topfile", 0640);
* Falls back to vfs_chmod(); should succeed. */
rc = vfs_chmod(VFS_AT_FDCWD, "topfile", 0640, 0);
check("D: top-level file, no parent component",
rc, 1, "topfile", 0640);
@@ -141,12 +141,12 @@ int main(int argc, char **argv)
* (refused on Linux, lchmod-the-symlink on *BSD/macOS), so assert only that
* the outside target's mode is unchanged. */
if (leaf_chmod_nofollow_supported()) {
rc = do_chmod_at("realdir/leaflink", 0666);
rc = vfs_chmod(VFS_AT_FDCWD, "realdir/leaflink", 0666, 0);
check("E: leaf component is an escaping symlink (must not be followed)",
rc, -1, "../trap/sentinel", 0600);
} else {
fprintf(stderr, "INFO: leaf-nofollow chmod unsupported here; "
"do_chmod_at follows a leaf symlink (documented limitation), "
"vfs_chmod follows a leaf symlink (documented limitation), "
"skipping scenario E\n");
}
+63 -7
View File
@@ -1,6 +1,6 @@
/*
* Test harness for do_rename_at(): a mixed top-level/slashed rename must still
* resolve the slashed side's parent under secure_relative_open() rather than
* Test harness for vfs_rename_at(): a mixed top-level/slashed rename must still
* resolve the slashed side's parent under vfs_resolve_open() rather than
* fall back to plain rename(). Not linked into rsync. GPL version 2.
*/
@@ -23,21 +23,21 @@ static int errs = 0;
#ifdef AT_FDCWD
/* The 3.4.3 bug: if either side has no slash the whole op fell back to plain
* rename(), leaving the slashed side's parent outside secure_relative_open(). */
* rename(), leaving the slashed side's parent outside vfs_resolve_open(). */
static int vulnerable_mixed_rename_at(const char *old_path, const char *new_path)
{
const char *old_slash, *new_slash;
if (!old_path || !*old_path || *old_path == '/'
|| !new_path || !*new_path || *new_path == '/')
return do_rename(old_path, new_path);
return vfs_rename(old_path, new_path);
old_slash = strrchr(old_path, '/');
new_slash = strrchr(new_path, '/');
if (!old_slash || !new_slash)
return do_rename(old_path, new_path);
return vfs_rename(old_path, new_path);
return do_rename_at(old_path, new_path);
return vfs_rename_at(old_path, new_path, 0, 0);
}
#endif
@@ -64,7 +64,7 @@ static void check_rename(const char *label, const char *old_path,
int saved_errno;
errno = 0;
rc = do_rename_at(old_path, new_path);
rc = vfs_rename_at(old_path, new_path, 0, 0);
saved_errno = errno;
got_ok = rc == 0;
@@ -79,6 +79,30 @@ static void check_rename(const char *label, const char *old_path,
label, old_path, new_path, expect_ok ? "succeeded" : "rejected");
}
/* Like check_rename() but with explicit per-operand policy flags, for the
* two-path per-side split (PR #30). */
static void check_rename_flags(const char *label, const char *old_path,
const char *new_path, int old_flags, int new_flags,
int expect_ok)
{
int rc, got_ok, saved_errno;
errno = 0;
rc = vfs_rename_at(old_path, new_path, old_flags, new_flags);
saved_errno = errno;
got_ok = rc == 0;
if (got_ok != expect_ok) {
fprintf(stderr, "FAIL [%s]: rename %s -> %s (of=%d nf=%d) rc=%d errno=%d (%s), expected %s\n",
label, old_path, new_path, old_flags, new_flags, rc, saved_errno,
strerror(saved_errno), expect_ok ? "success" : "rejection");
errs++;
return;
}
fprintf(stderr, "OK [%s]: rename %s -> %s %s\n",
label, old_path, new_path, expect_ok ? "succeeded" : "rejected");
}
static void check_vulnerable_rename(const char *label, const char *old_path,
const char *new_path)
{
@@ -199,6 +223,38 @@ int main(int argc, char **argv)
check_exists("F source consumed", "top-old", 0);
check_exists("F destination created", "top-new", 1);
/* Per-operand policy split (PR #30): the NEW side's policy must be independent
* of the OLD side's. oplink is a caller-owned (uid0/euid) symlink that ESCAPES
* the tree (-> ../trap): the ownership walk (operator policy) follows the
* operator's own symlink, but the secure receiver resolve (transfer, flag 0)
* refuses it because it leaves the cwd anchor. PS-refuse and PS-follow rename
* to the SAME oplink/ path with the SAME operator old side, differing ONLY in
* the new-side flag -- so the per-side flag, not a whole-call flag, decides.
* The old single-flag API applied VFS_OPERATOR_PATH to both operands, so it
* would have followed oplink in PS-refuse too (PS-refuse is RED on that code).
* Run non-daemon (the regime where an operator basis/backup path legitimately
* carries the operator's own uid0/euid symlinks). */
{
struct stat lst;
if (lstat("oplink", &lst) == 0 && S_ISLNK(lst.st_mode)) {
int save_daemon = am_daemon;
am_daemon = 0;
check_rename_flags("PS-refuse: new side transfer refuses an escaping owned symlink",
"realdir/perside-src2", "oplink/tr-out",
VFS_OPERATOR_PATH, 0, 0);
check_exists("PS-refuse out-of-tree dest absent", "../trap/tr-out", 0);
check_exists("PS-refuse source preserved", "realdir/perside-src2", 1);
check_rename_flags("PS-follow: new side operator follows the same owned symlink",
"realdir/perside-src3", "oplink/op-out",
VFS_OPERATOR_PATH, VFS_OPERATOR_PATH, 1);
check_exists("PS-follow out-of-tree dest created (operator's own symlink)", "../trap/op-out", 1);
am_daemon = save_daemon;
}
}
if (errs)
fprintf(stderr, "%d failure(s)\n", errs);
return errs ? 1 : 0;
+9 -9
View File
@@ -1,5 +1,5 @@
/*
* Test harness for secure_relative_open()'s front-door input
* Test harness for vfs_resolve_open()'s front-door input
* validation. Codex audit Finding 5 noted that the existing check
*
* if (strncmp(relpath, "../", 3) == 0 || strstr(relpath, "/../"))
@@ -14,7 +14,7 @@
* pre-5.6 Linux does not, so the validation must happen at the
* front door.
*
* This helper invokes secure_relative_open() with each suspect
* This helper invokes vfs_resolve_open() with each suspect
* input and checks both the failure (rc < 0) and the errno
* (EINVAL means "rejected at the front door"). Pre-fix, the kernel
* may reject with a different errno (EXDEV from RESOLVE_BENEATH);
@@ -47,7 +47,7 @@ static void check_relpath(const char *relpath)
int saved_errno;
errno = 0;
fd = secure_relative_open(NULL, relpath, O_RDONLY | O_DIRECTORY, 0);
fd = vfs_resolve_open(NULL, relpath, O_RDONLY | O_DIRECTORY, 0);
saved_errno = errno;
if (fd >= 0) {
@@ -76,7 +76,7 @@ static void check_basedir(const char *basedir)
int saved_errno;
errno = 0;
fd = secure_relative_open(basedir, "ok", O_RDONLY | O_DIRECTORY, 0);
fd = vfs_resolve_open(basedir, "ok", O_RDONLY | O_DIRECTORY, 0);
saved_errno = errno;
if (fd >= 0) {
@@ -111,7 +111,7 @@ static void check_beneath_dotdot(void)
return;
}
fd = secure_relative_open_at_beneath(anchor, "alias/../subdir",
fd = vfs_resolve_open_at_beneath(anchor, "alias/../subdir",
O_RDONLY | O_DIRECTORY, 0);
if (fd < 0 || fstat(fd, &fst) < 0 || fst.st_dev != ast.st_dev
|| fst.st_ino == ast.st_ino) {
@@ -138,7 +138,7 @@ static void check_beneath_dotdot(void)
for (ci = 0; ci < sizeof dotdot_cases / sizeof *dotdot_cases; ci++) {
int dfd;
errno = 0;
dfd = secure_relative_open_at_beneath(anchor, "..",
dfd = vfs_resolve_open_at_beneath(anchor, "..",
dotdot_cases[ci].flags, 0);
if (dfd >= 0) {
STRUCT_STAT dst;
@@ -159,7 +159,7 @@ static void check_beneath_dotdot(void)
}
errno = 0;
fd = secure_relative_open_at_beneath(anchor, "../outside",
fd = vfs_resolve_open_at_beneath(anchor, "../outside",
O_RDONLY | O_DIRECTORY, 0);
if (fd >= 0 || errno != ELOOP) {
fprintf(stderr, "FAIL [beneath escape]: rc=%d errno=%d, expected -1/ELOOP\n",
@@ -184,7 +184,7 @@ int main(int argc, char **argv)
return 2;
}
/* secure_relative_open's daemon-only confinement protections only
/* vfs_resolve_open's daemon-only confinement protections only
* fire when am_daemon && !am_chrooted (the threat model is the
* daemon-no-chroot deployment), but the front-door input
* validation runs unconditionally. We set am_daemon anyway so the
@@ -196,7 +196,7 @@ int main(int argc, char **argv)
symlink("subdir", "alias");
/* Each of these relpaths must be rejected with EINVAL at the
* secure_relative_open() front door. ".." is the actual one-level
* vfs_resolve_open() front door. ".." is the actual one-level
* escape; the others ("subdir/..", "subdir/../subdir") resolve
* back to the start dir on systems that allow them, but we still
* reject them as defence-in-depth: a path containing a ".." token
+2 -2
View File
@@ -42,14 +42,14 @@ size_t max_alloc = (size_t)-1; /* test helpers are not memory-constrained;
* 0 here makes every my_alloc()/my_strdup() in
* util2.c trip the "exceeded --max-alloc=0"
* check, which any helper exercising the
* per-component fallback of secure_relative_open()
* per-component fallback of vfs_resolve_open()
* hits at its first my_strdup() call. */
char *partial_dir;
char *module_dir;
int module_dirfd = -1;
char *confine_root;
unsigned int confine_rootlen = 0;
/* curr_dir[]/curr_dir_len (read by secure_relative_open) are defined in
/* vfs.curr_dir[]/vfs.curr_dir_len (read by vfs_resolve_open) are defined in
* syscall.c, which every helper links -- no stub needed here. */
filter_rule_list daemon_filter_list;
+22 -22
View File
@@ -1,8 +1,8 @@
/*
* Test harness for the fake-super branches of do_symlink_at()/do_mknod_at().
* Test harness for the fake-super branches of vfs_symlink_at()/vfs_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
* vfs_symlink()/vfs_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.
*/
@@ -12,7 +12,7 @@
#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
* xattrs are unavailable -- the same guard vfs_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
@@ -89,8 +89,8 @@ int main(int argc, char **argv)
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
/* Nothing left to assert: the vfs_mknod() checks need mknodat(), and
* the vfs_symlink() ones are not compiled here. Skip rather than
* pass vacuously. */
(void)argc; (void)argv;
fprintf(stderr, "SKIP: no mknodat() and no symlink placeholders -- "
@@ -118,39 +118,39 @@ int main(int argc, char **argv)
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(),
/* Pre-fix fallback: a no-slash path went to vfs_symlink()/vfs_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",
vfs_symlink("VULN_SYM_PAYLOAD", VFS_AT_FDCWD, "sympath", VFS_ALLOW_SYMLINK);
check_clobbered("poc vfs_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", "");
vfs_mknod(VFS_AT_FDCWD, "nodpath", S_IFCHR | 0600, 0, VFS_ALLOW_SYMLINK);
check_clobbered("poc vfs_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");
vfs_symlink("FIXED_SYM_PAYLOAD", VFS_AT_FDCWD, "sympath", 0);
check_preserved("vfs_symlink 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");
vfs_symlink("FIXED_SYM_PAYLOAD", VFS_AT_FDCWD, "sub/sympath2", 0);
check_preserved("vfs_symlink 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");
/* Without mknodat() the secure vfs_mknod() IS the plain mknod(): the
* confinement is compiled out by design (SECURITY.md), so these would
* assert a property the build deliberately does not have. The
* vfs_symlink() checks above do not depend on it and still run. */
vfs_mknod(VFS_AT_FDCWD, "nodpath", S_IFCHR | 0600, 0, 0);
check_preserved("vfs_mknod 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");
vfs_mknod(VFS_AT_FDCWD, "sub/nodpath2", S_IFCHR | 0600, 0, 0);
check_preserved("vfs_mknod slashed", "../outside/secret_nod2", "VICTIM_NOD2");
# endif
if (errs)
+3 -1
View File
@@ -19,7 +19,9 @@ from rsyncfns import (
SCRATCHDIR, rmtree, rsync_argv, start_test_daemon, test_fail, write_daemon_conf,
)
DAEMON_PORT = 13010
# Not 13000-13060: ASUS Armoury Crate on the Cygwin CI host parks localhost
# listeners there (13010 among them).
DAEMON_PORT = 12931
# (module name, exclude pattern, expect the pushed file to land)
CASES = [
+3 -2
View File
@@ -227,9 +227,10 @@
"scratchbase": "/Volumes/RsyncHFS",
"expect_skip_omit": [
"backup-crossdev-copy",
"chmod-temp-dir"
"chmod-temp-dir",
"operator-path-backup-chown"
],
"_skip_comment": "The two omitted entries are macOS-wide expected-skips that this target RUNS: the separate volume supplies the cross-device conditions mac2 lacks. (itemize used to XFAIL here, before --link-dest learned to fall back when the filesystem cannot hard-link a symlink.)"
"_skip_comment": "The three omitted entries are macOS-wide expected-skips that this target RUNS: the separate volume supplies the cross-device conditions mac2 lacks. (itemize used to XFAIL here, before --link-dest learned to fall back when the filesystem cannot hard-link a symlink.)"
},
{
"_comment": "The x86-64 Mac (macOS 10.13). The ONLY target that can build the x86-64 md5 assembly -- mac2 is arm64, where configure refuses --enable-md5-asm outright. MacPorts supplies autotools, python3 and the crypto/hash libs the stock 10.13 image lacks, and is not on the non-interactive ssh PATH, so put it there for the whole run. This target keeps the STOCK Apple compiler (clang 10, the 10.13 ceiling), which is what caught #161; it cannot build --enable-roll-simd, because a clang that old rejects configure's target(\"default\") multiversioning probe. That is a compiler-VERSION limit, not a Mach-O one -- see mac-x86-asm below, which builds the same source with MacPorts clang 19 and all three optimizations on.",
@@ -0,0 +1,184 @@
#!/usr/bin/env python3
# --backup-dir parent-component symlink-race confinement for the OWNERSHIP set,
# not the create. operator-path-backup-symlink covers the create side (a backup
# symlink must not be written outside the backup tree); this covers what
# set_file_attrs() does to the item afterwards.
#
# make_backup() recreates the item at the backup name and then calls
# set_file_attrs(buf, ..., ATTRS_OPERATOR_PATH). A regular/dir/fifo leaf is
# pinned by op_pin and its metadata driven off that fd, but a SYMLINK leaf never
# enters op_pin (there is no O_NOFOLLOW open of a symlink), so the chown falls
# through to the path-based wrapper on the full operator path. Unless that
# wrapper resolves through the ownership walk, a parent component flipped to an
# attacker-owned symlink redirects the lchown out of the backup tree and retags
# a victim inode as the attacker's -- an ownership-transfer primitive, and the
# trust laundering that then defeats the walk on any later pass.
#
# Reaching the recreate path at all needs the backup dir on ANOTHER filesystem:
# on one filesystem make_backup() hard-links or renames the item across and
# never calls set_file_attrs(). So the whole fixture lives on tmpfs.
#
# A statically planted symlink is not enough either -- rsync's own backup-dir
# validation deletes a non-directory component before using it -- so the plant
# has to be a live flip, as in the sibling test.
import os
import subprocess
import time
from rsyncfns import (
SCRATCHDIR, race_budget, find_attacker_uid, rmtree, makepath,
start_c_flipper, stop_flipper, test_fail, test_skipped,
)
if os.geteuid() != 0:
test_skipped("requires root to own a symlink by a foreign uid and to chown backups")
ATT_UID = find_attacker_uid()
if ATT_UID is None:
test_skipped("no untrusted-uid user available for cross-uid plant")
# The backup dir must be on a different st_dev from the destination, or
# make_backup() renames into it and the set_file_attrs() path never runs.
dest_dev = os.stat(SCRATCHDIR).st_dev
TMPFS = None
for cand in ('/dev/shm', '/run/shm', os.environ.get('TMPDIR', '/tmp')):
try:
if os.stat(cand).st_dev != dest_dev and os.access(cand, os.W_OK):
TMPFS = cand
break
except OSError:
continue
if TMPFS is None:
test_skipped("no writable cross-device dir (tmpfs) for the --backup-dir EXDEV path")
# Many files widen the per-file backup window so the flipper has more chances to
# land the swap between the recreate and the chown.
NFILES = 95
base = SCRATCHDIR / 'bdir-chown-race'
src = base / 'src'
dest = base / 'dest'
bakroot = os.path.join(TMPFS, 'rsync-bakchown-race')
backup = os.path.join(bakroot, 'backup')
outside = os.path.join(bakroot, 'outside')
sub = os.path.join(backup, 'sub')
sublink = os.path.join(backup, '.sublink')
def build():
"""Reset the workspace. Call only while the flipper is stopped."""
rmtree(base)
subprocess.run(['rm', '-rf', bakroot], check=False)
makepath(src / 'sub', dest / 'sub')
os.makedirs(outside, exist_ok=True)
os.makedirs(backup, exist_ok=True)
# Distinct source and destination symlink values so each transfer replaces
# the destination symlink and thus backs the old one up. The destination
# symlinks are attacker-owned, so restoring their ownership onto the backup
# copy REQUIRES an lchown -- without that there is no chown to redirect and
# the test would pass vacuously.
for i in range(NFILES):
(src / 'sub' / f'f{i}').symlink_to('test')
d = dest / 'sub' / f'f{i}'
d.symlink_to('test2')
os.lchown(d, ATT_UID, ATT_UID)
# Victims: root-owned regular files carrying the names the backup would use
# if a flipped `sub` redirected the operator path into outside/.
for i in range(NFILES):
v = os.path.join(outside, f'f{i}')
with open(v, 'w') as fh:
fh.write('victim\n')
os.chown(v, 0, 0)
# The attacker-owned parent-swap target.
os.symlink(outside, sublink)
os.lchown(sublink, ATT_UID, ATT_UID)
os.makedirs(sub, exist_ok=True)
def push():
"""Blocking local rsync push that backs up the old destination symlinks."""
return subprocess.run(
['./rsync', '-a', '-b', f'--backup-dir={backup}', f'{src}/', f'{dest}/'],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
)
def retagged():
"""Name of a victim in outside/ that stopped being root-owned, or ''.
A swap killed mid-rename can leave outside/ momentarily odd; anything we
cannot stat is simply not evidence of a win."""
try:
with os.scandir(outside) as it:
for e in it:
try:
st = e.stat(follow_symlinks=False)
except OSError:
continue
if st.st_uid != 0 or st.st_gid != 0:
return e.name
except (FileNotFoundError, NotADirectoryError):
return ''
return ''
# ---- POSITIVE CONTROL ------------------------------------------------------
# A clean run must (a) back the old destination symlink into the backup tree via
# the cross-device recreate path and (b) carry the attacker ownership onto that
# backup copy -- which is the lchown this test is about. Without both, the race
# below would be asserting on a code path that never executes.
build()
proc = push()
if proc.returncode != 0:
test_fail(f"positive control: clean --backup-dir run failed (rc={proc.returncode}):\n{proc.stdout or ''}")
bak0 = os.path.join(sub, 'f0')
if not os.path.islink(bak0) or os.readlink(bak0) != 'test2':
test_fail(f"positive control: the old destination symlink was not backed up into {sub}; "
"the cross-device recreate path was not exercised")
st = os.lstat(bak0)
if st.st_uid != ATT_UID:
test_fail(f"positive control: backup copy {bak0} is uid {st.st_uid}, expected the "
f"attacker uid {ATT_UID}; set_file_attrs() did not lchown the backup, so "
"this test would pass vacuously")
if retagged():
test_fail("positive control: a victim in outside/ changed ownership during a no-flipper run")
# ---- THE LIVE RACE ---------------------------------------------------------
# Flip backup/sub between the real backup directory and the attacker-owned
# symlink to outside/ under a live transfer. The ownership walk must refuse the
# foreign-owned component, so no victim in outside/ is ever retagged.
deadline = time.monotonic() + race_budget(10.0)
flip = None
try:
while time.monotonic() < deadline:
# Reset only while the flipper is quiet, so build()'s rmtree/mkdir
# cannot race the swapper and drop artifacts in outside/.
if flip is not None:
stop_flipper(flip)
flip = None
build()
flip = start_c_flipper(sub, sublink)
push()
victim = retagged()
if victim:
test_fail(
"--backup-dir parent symlink race: victim "
f"{os.path.join(outside, victim)} was retagged away from root; rsync "
"chowned through the flipped attacker-owned backup/sub component "
"instead of refusing it."
)
finally:
if flip is not None:
stop_flipper(flip)
subprocess.run(['rm', '-rf', bakroot], check=False)
print("operator-path-backup-chown: backup ownership confined under parent-swap race")
@@ -31,6 +31,14 @@ trap_outside.mkdir(parents=True)
(mod / 'top-old').write_text("top-old\n")
os.symlink('../trap', mod / 'escape_link')
# Per-operand policy split (PR #30): a caller-owned symlink that ESCAPES the tree
# (-> ../trap). The ownership walk (operator) follows the operator's own symlink;
# the secure receiver resolve (transfer, flag 0) refuses it. The harness
# PS-refuse/PS-follow checks rename to oplink/ under each new-side policy.
os.symlink('../trap', mod / 'oplink')
for n in ('perside-src2', 'perside-src3'):
(mod / 'realdir' / n).write_text(n + "\n")
proc = subprocess.run([str(TOOLDIR / 't_rename_secure'), str(mod)])
if proc.returncode == 77:
test_skipped("t_rename_secure skipped")
+7 -1
View File
@@ -35,6 +35,7 @@ import subprocess
import sys
import tempfile
import time
import zlib
from pathlib import Path
from exitcodes import Exit # re-exported: tests may `from rsyncfns import Exit`
@@ -2193,7 +2194,12 @@ def setup_chroot_inner(name):
('mod', {'path': str(outer) + '/./inner', 'read only': 'no',
'use chroot': 'yes', 'munge symlinks': 'no'}),
], name=f'{name}.conf')
url = start_test_daemon(conf, 12940 + (abs(hash(name)) % 200))
# crc32, not hash(): str hash is per-process randomized (PYTHONHASHSEED),
# so the port would wander run to run -- and the old 12940+200 span reached
# into 13000+, where desktop bloatware (e.g. ASUS Armoury Crate on the
# Cygwin CI host) parks localhost listeners. 12800-12859 is otherwise
# unused by the suite.
url = start_test_daemon(conf, 12800 + (zlib.crc32(name.encode()) % 60))
return base, inner, outside, src, url
+1
View File
@@ -45,6 +45,7 @@ msg-io-timeout-overflow
nondaemon-symlink-race
nonroot-restrictive-perms
open-noatime
operator-path-backup-chown
operator-path-backup-rmdir
operator-path-backup-symlink
operator-path-insecure-links-daemon
+1
View File
@@ -19,6 +19,7 @@ dir-sgid
fake-super-acl-xattr
link-dest-symlink-enotsup # the ENOTSUP hard-link hook is an LD_PRELOAD, Linux-only
open-noatime
operator-path-backup-chown
partial-protected-regular-retry-linux
preallocate
protected-regular
+5 -5
View File
@@ -160,11 +160,11 @@ static void list_file(const char *fname)
char linkbuf[4096];
int nsecs;
if (do_lstat(fname, &buf) < 0)
if (vfs_lstat(VFS_AT_FDCWD, fname, &buf, VFS_ALLOW_SYMLINK) < 0)
failed("stat", fname);
#ifdef SUPPORT_CRTIMES
if (display_crtimes && (crtime = get_create_time(fname, &buf)) == 0)
failed("get_create_time", fname);
if (display_crtimes && (crtime = vfs_get_create_time(fname, &buf)) == 0)
failed("vfs_get_create_time", fname);
#endif
#ifdef SUPPORT_XATTRS
if (am_root < 0)
@@ -188,9 +188,9 @@ static void list_file(const char *fname)
buf.st_uid = buf.st_gid = 0;
strlcpy(linkbuf, " -> ", sizeof linkbuf);
/* const-cast required for silly UNICOS headers */
len = do_readlink((char*)fname, linkbuf+4, sizeof linkbuf - 4);
len = vfs_readlink((char*)fname, linkbuf+4, sizeof linkbuf - 4);
if (len == -1)
failed("do_readlink", fname);
failed("vfs_readlink", fname);
else
/* it's not nul-terminated */
linkbuf[4+len] = 0;
+48 -487
View File
@@ -34,7 +34,6 @@ extern int relative_paths;
extern int preserve_xattrs;
extern int omit_link_times;
extern int preallocate_files;
extern int operator_path_resolve;
extern char *module_dir;
extern unsigned int module_dirlen;
extern char *partial_dir;
@@ -42,8 +41,6 @@ extern filter_rule_list daemon_filter_list;
int sanitize_paths = 0;
extern char curr_dir[MAXPATHLEN]; /* defined in syscall.c */
extern unsigned int curr_dir_len;
int curr_dir_depth; /* This is only set for a sanitizing daemon. */
/* Set a fd into nonblocking mode. */
@@ -133,7 +130,7 @@ int set_times(const char *fname, STRUCT_STAT *stp)
switch (switch_step) {
#ifdef HAVE_SETATTRLIST
#include "case_N.h"
if (do_setattrlist_times(fname, stp) == 0)
if (vfs_setattrlist_times(fname, stp) == 0)
break;
if (errno != ENOSYS)
return -1;
@@ -142,7 +139,7 @@ int set_times(const char *fname, STRUCT_STAT *stp)
#ifdef HAVE_UTIMENSAT
#include "case_N.h"
if (do_utimensat_at(fname, stp) == 0)
if (vfs_utimensat_at(fname, stp) == 0)
break;
if (errno != ENOSYS)
return -1;
@@ -151,7 +148,7 @@ int set_times(const char *fname, STRUCT_STAT *stp)
#ifdef HAVE_LUTIMES
#include "case_N.h"
if (do_lutimes(fname, stp) == 0)
if (vfs_lutimes(fname, stp) == 0)
break;
if (errno != ENOSYS)
return -1;
@@ -168,10 +165,10 @@ int set_times(const char *fname, STRUCT_STAT *stp)
#include "case_N.h"
#ifdef HAVE_UTIMES
if (do_utimes(fname, stp) == 0)
if (vfs_utimes(fname, stp) == 0)
break;
#else
if (do_utime(fname, stp) == 0)
if (vfs_utime(fname, stp) == 0)
break;
#endif
@@ -189,7 +186,7 @@ int set_times(const char *fname, STRUCT_STAT *stp)
int set_times_at(int dfd, const char *name, STRUCT_STAT *stp)
{
#if defined HAVE_UTIMENSAT && !defined HAVE_SETATTRLIST
int r = do_utimensat_atfd(dfd, name, stp);
int r = vfs_utimensat_atfd(dfd, name, stp);
if (r == 0)
return 0;
if (errno == ENOSYS)
@@ -201,91 +198,6 @@ int set_times_at(int dfd, const char *name, STRUCT_STAT *stp)
#endif
}
/* Create any necessary directories in fname. Any missing directories are
* created with default permissions. Returns < 0 on error, or the number
* of directories created. */
int make_path(char *fname, int flags)
{
char *end, *p;
int ret = 0;
if (flags & MKP_SKIP_SLASH) {
while (*fname == '/')
fname++;
}
while (*fname == '.' && fname[1] == '/')
fname += 2;
if (flags & MKP_DROP_NAME) {
end = strrchr(fname, '/');
if (!end || end == fname)
return 0;
*end = '\0';
} else
end = fname + strlen(fname);
/* Try to find an existing dir, starting from the deepest dir. */
for (p = end; ; ) {
if (dry_run) {
STRUCT_STAT st;
if (do_stat(fname, &st) == 0) {
if (S_ISDIR(st.st_mode))
errno = EEXIST;
else
errno = ENOTDIR;
}
} else if (do_mkdir_at(fname, ACCESSPERMS) == 0) {
ret++;
break;
}
if (errno != ENOENT) {
STRUCT_STAT st;
if (errno != EEXIST || (do_stat(fname, &st) == 0 && !S_ISDIR(st.st_mode)))
ret = -ret - 1;
break;
}
while (1) {
if (p == fname) {
/* We got a relative path that doesn't exist, so assume that '.'
* is there and just break out and create the whole thing. */
p = NULL;
goto double_break;
}
if (*--p == '/') {
if (p == fname) {
/* We reached the "/" dir, which we assume is there. */
goto double_break;
}
*p = '\0';
break;
}
}
}
double_break:
/* Make all the dirs that we didn't find on the way here. */
while (p != end) {
if (p)
*p = '/';
else
p = fname;
p += strlen(p);
if (ret < 0) /* Skip mkdir on error, but keep restoring the path. */
continue;
if (do_mkdir_at(fname, ACCESSPERMS) < 0)
ret = -ret - 1;
else
ret++;
}
if (flags & MKP_DROP_NAME)
*end = '/';
return ret;
}
/**
* Write @p len bytes at @p ptr to descriptor @p desc, retrying if
* interrupted.
@@ -315,353 +227,6 @@ int full_write(int desc, const char *ptr, size_t len)
return total_written;
}
/**
* Read @p len bytes at @p ptr from descriptor @p desc, retrying if
* interrupted.
*
* @retval >0 the actual number of bytes read
*
* @retval 0 for EOF
*
* @retval <0 for an error.
*
* Derived from GNU C's cccp.c. */
static int safe_read(int desc, char *ptr, size_t len)
{
int n_chars;
if (len == 0)
return len;
do {
n_chars = read(desc, ptr, len);
} while (n_chars < 0 && errno == EINTR);
return n_chars;
}
/* Remove existing file @dest and reopen, creating a new file with @mode */
static int unlink_and_reopen(const char *dest, mode_t mode)
{
int ofd;
if (robust_unlink(dest) && errno != ENOENT) {
int save_errno = errno;
rsyserr(FERROR_XFER, errno, "unlink %s", full_fname(dest));
errno = save_errno;
return -1;
}
#ifdef SUPPORT_XATTRS
if (preserve_xattrs)
mode |= S_IWUSR;
#endif
mode &= INITACCESSPERMS;
/* Use do_open_at so the create/truncate goes through a secure
* parent dirfd in the daemon-no-chroot deployment. Otherwise
* an attacker could swap a parent component with a symlink in
* the window between robust_unlink (which uses do_unlink_at,
* already secure) and the create here, and redirect the new
* file outside the module. */
if ((ofd = do_open_at(dest, O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, mode)) < 0) {
int save_errno = errno;
rsyserr(FERROR_XFER, save_errno, "open %s", full_fname(dest));
errno = save_errno;
return -1;
}
return ofd;
}
/* Copy contents of file @source to file @dest with mode @mode.
*
* If @tmpfilefd is < 0, copy_file unlinks @dest and then opens a new
* file with name @dest.
*
* Otherwise, copy_file writes to and closes the provided file
* descriptor.
*
* In either case, if --xattrs are being preserved, the dest file will
* have its xattrs set from the source file.
*
* This is used in conjunction with the --temp-dir, --backup, and
* --copy-dest options. */
int copy_file(const char *source, const char *dest, int tmpfilefd, mode_t mode)
{
int ifd, ofd;
char buf[1024 * 8];
int len; /* Number of bytes read into `buf'. */
OFF_T prealloc_len = 0, offset = 0;
/* For any hardened (non-chrooted) receiver, route the source open through
* secure_relative_open so a parent-symlink on the source path (e.g.
* --copy-dest=cd where cd is a symlink to an outside directory) cannot
* redirect the read to a file the attacker should not see. Plain
* do_open_nofollow only refuses a final-component symlink; parents are
* still followed. An ABSOLUTE source is an operator basis (e.g. an absolute
* --copy-dest): confine its parents via the ownership walk -- a foreign-owned
* parent symlink is refused, the operator's own dirs/uid0/euid symlinks
* followed -- so a flipped parent can't redirect the basis read out of tree.
* operator_path_resolve is set only across the walk (so module-exclude is
* enforced) and restored, leaving the caller's value for the dest side -- this
* is why confining the source here does not re-open the copy_xattrs dest
* race the way wrapping the whole copy_altdest_file would. */
if (secure_relpath_active() && source && *source && source[0] != '/')
ifd = secure_relative_open(NULL, source, O_RDONLY | O_NOFOLLOW, 0);
#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY
else if (secure_relpath_active() && source && source[0] == '/'
&& !symlink_optout_allowed()) {
int save = operator_path_resolve, dfd, e;
const char *leaf;
operator_path_resolve = 1;
dfd = owner_walk_parent(source, &leaf);
operator_path_resolve = save;
if (dfd < 0)
ifd = -1;
else {
ifd = openat(dfd, leaf, O_RDONLY | O_NOFOLLOW);
e = errno;
close(dfd);
errno = e;
}
}
#endif
else
ifd = do_open_nofollow(source, O_RDONLY);
if (ifd < 0) {
int save_errno = errno;
rsyserr(FERROR_XFER, errno, "open %s", full_fname(source));
errno = save_errno;
return -1;
}
if (tmpfilefd >= 0) {
ofd = tmpfilefd;
} else {
ofd = unlink_and_reopen(dest, mode);
if (ofd < 0) {
int save_errno = errno;
close(ifd);
errno = save_errno;
return -1;
}
}
#ifdef SUPPORT_PREALLOCATION
if (preallocate_files) {
STRUCT_STAT srcst;
/* Try to preallocate enough space for file's eventual length. Can
* reduce fragmentation on filesystems like ext4, xfs, and NTFS. */
if (do_fstat(ifd, &srcst) < 0)
rsyserr(FWARNING, errno, "fstat %s", full_fname(source));
else if (srcst.st_size > 0) {
prealloc_len = do_fallocate(ofd, 0, srcst.st_size);
if (prealloc_len < 0)
rsyserr(FWARNING, errno, "do_fallocate %s", full_fname(dest));
}
}
#endif
while ((len = safe_read(ifd, buf, sizeof buf)) > 0) {
if (full_write(ofd, buf, len) < 0) {
int save_errno = errno;
rsyserr(FERROR_XFER, errno, "write %s", full_fname(dest));
close(ifd);
close(ofd);
errno = save_errno;
return -1;
}
offset += len;
}
if (len < 0) {
int save_errno = errno;
rsyserr(FERROR_XFER, errno, "read %s", full_fname(source));
close(ifd);
close(ofd);
errno = save_errno;
return -1;
}
/* Source file might have shrunk since we fstatted it.
* Cut off any extra preallocated zeros from dest file. */
if (offset < prealloc_len) {
#ifdef HAVE_FTRUNCATE
/* If we fail to truncate, the dest file may be wrong, so we
* must trigger the "partial transfer" error. */
if (do_ftruncate(ofd, offset) < 0)
rsyserr(FERROR_XFER, errno, "ftruncate %s", full_fname(dest));
#else
rprintf(FERROR_XFER, "no ftruncate for over-long pre-alloc: %s", full_fname(dest));
#endif
}
if (do_fsync && fsync(ofd) < 0) {
int save_errno = errno;
rsyserr(FERROR, errno, "fsync failed on %s", full_fname(dest));
close(ofd);
close(ifd); /* ifd is held open until after the xattr copy below */
errno = save_errno;
return -1;
}
#ifdef SUPPORT_XATTRS
/* Read the source xattrs through the held source fd (ifd) and set them
* through ofd while both are still held, so a parent-symlink race can't
* redirect the read out of tree or the write onto a file outside it. */
if (preserve_xattrs)
copy_xattrs(source, ifd, dest, ofd);
#endif
if (close(ifd) < 0) {
rsyserr(FWARNING, errno, "close failed on %s",
full_fname(source));
}
if (close(ofd) < 0) {
int save_errno = errno;
rsyserr(FERROR_XFER, errno, "close failed on %s", full_fname(dest));
errno = save_errno;
return -1;
}
return 0;
}
/* MAX_RENAMES should be 10**MAX_RENAMES_DIGITS */
#define MAX_RENAMES_DIGITS 3
#define MAX_RENAMES 1000
/**
* Robust unlink: some OS'es (HPUX) refuse to unlink busy files, so
* rename to <path>/.rsyncNNN instead.
*
* Note that successive rsync runs will shuffle the filenames around a
* bit as long as the file is still busy; this is because this function
* does not know if the unlink call is due to a new file coming in, or
* --delete trying to remove old .rsyncNNN files, hence it renames it
* each time.
**/
int robust_unlink(const char *fname)
{
#ifndef ETXTBSY
return do_unlink_at(fname);
#else
static int counter = 1;
int rc, pos, start;
char path[MAXPATHLEN];
rc = do_unlink_at(fname);
if (rc == 0 || errno != ETXTBSY)
return rc;
if ((pos = strlcpy(path, fname, MAXPATHLEN)) >= MAXPATHLEN)
pos = MAXPATHLEN - 1;
while (pos > 0 && path[pos-1] != '/')
pos--;
pos += strlcpy(path+pos, ".rsync", MAXPATHLEN-pos);
if (pos > (MAXPATHLEN-MAX_RENAMES_DIGITS-1)) {
errno = ETXTBSY;
return -1;
}
/* start where the last one left off to reduce chance of clashes */
start = counter;
do {
snprintf(&path[pos], MAX_RENAMES_DIGITS+1, "%03d", counter);
if (++counter >= MAX_RENAMES)
counter = 1;
} while (access(path, 0) == 0 && counter != start);
if (INFO_GTE(MISC, 1)) {
rprintf(FWARNING, "renaming %s to %s because of text busy\n",
fname, path);
}
/* maybe we should return rename()'s exit status? Nah. */
if (do_rename_at(fname, path) != 0) {
errno = ETXTBSY;
return -1;
}
return 0;
#endif
}
/* Returns 0 on successful rename, 1 if we successfully copied the file
* across filesystems, -2 if copy_file() failed, and -1 on other errors.
* If partialptr is not NULL and we need to do a copy, copy the file into
* the active partial-dir instead of over the destination file. */
int robust_rename(const char *from, const char *to, const char *partialptr,
int mode, struct file_struct *file)
{
int tries = 4;
/* A resumed in-place partial-dir transfer might call us with from and
* to pointing to the same buf if the transfer failed yet again. */
if (from == to)
return 0;
while (tries--) {
/* tmp -> final usually live in the entry's own dir: rename via the
* held dir fd when both do, else the full-path wrapper. */
int ofd = held_dfd_for(from, file);
int nfd = held_dfd_for(to, file);
int rr;
if (ofd >= 0 && nfd >= 0) {
const char *os = strrchr(from, '/');
const char *ns = strrchr(to, '/');
rr = do_rename_atfd(ofd, os ? os + 1 : from, nfd, ns ? ns + 1 : to);
} else
rr = do_rename_at(from, to);
if (rr == 0)
return 0;
switch (errno) {
#ifdef ETXTBSY
case ETXTBSY:
if (robust_unlink(to) != 0) {
errno = ETXTBSY;
return -1;
}
errno = ETXTBSY;
break;
#endif
case EXDEV: {
int save = operator_path_resolve, rc;
if (partialptr) {
if (!handle_partial_dir(partialptr,PDIR_CREATE))
return -2;
to = partialptr;
}
/* Cross-fs fallback: copy then unlink. An absolute --temp-dir
* source / --partial-dir dest is an operator path whose parents
* do_open_at()/do_unlink_at() would otherwise follow via plain libc
* -- confine them through the ownership walk so a raced parent
* symlink can't redirect the dest-write or the source-unlink out of
* the module. copy_file already confines the source READ; a
* relative in-module path stays on the secure_relative_open arm, so
* only flip the flag for an absolute (operator) path. */
if (*to == '/')
operator_path_resolve = 1;
rc = copy_file(from, to, -1, mode);
operator_path_resolve = save;
if (rc != 0)
return -2;
if (*from == '/')
operator_path_resolve = 1;
do_unlink_at(from);
operator_path_resolve = save;
return 1;
}
default:
return -1;
}
}
return -1;
}
static pid_t all_pids[10];
static int num_pids;
@@ -801,7 +366,7 @@ static inline void call_glob_match(const char *name, int len, int from_glob,
STRUCT_STAT st;
int is_dir;
if (do_stat(glob.arg_buf, &st) != 0)
if (vfs_stat(VFS_AT_FDCWD, glob.arg_buf, &st, VFS_ALLOW_SYMLINK) != 0)
return;
is_dir = S_ISDIR(st.st_mode) != 0;
if (arg && !is_dir)
@@ -1211,7 +776,7 @@ char *sanitize_path(char *dest, const char *p, const char *rootdir, int depth, i
}
/* Like chdir(), but it keeps track of the current directory (in the
* global "curr_dir"), and ensures that the path size doesn't overflow.
* global "vfs.curr_dir"), and ensures that the path size doesn't overflow.
* Also cleans the path using the clean_fname() function. */
int change_dir(const char *dir, int set_path_only)
{
@@ -1221,11 +786,11 @@ int change_dir(const char *dir, int set_path_only)
if (!initialised) {
initialised = 1;
if (getcwd(curr_dir, sizeof curr_dir - 1) == NULL) {
if (getcwd(vfs.curr_dir, sizeof vfs.curr_dir - 1) == NULL) {
rsyserr(FERROR, errno, "getcwd()");
exit_cleanup(RERR_FILESELECT);
}
curr_dir_len = strlen(curr_dir);
vfs.curr_dir_len = strlen(vfs.curr_dir);
}
if (!dir) /* this call was probably just to initialize */
@@ -1236,13 +801,13 @@ int change_dir(const char *dir, int set_path_only)
return 1;
if (*dir == '/') {
if (len >= sizeof curr_dir) {
if (len >= sizeof vfs.curr_dir) {
errno = ENAMETOOLONG;
return 0;
}
if (!set_path_only) {
/* The destination is operator-supplied (like --log-file et al.), so
* resolve it with open_no_attacker_symlinks: walk each component
* resolve it with vfs_open_owner_walk: walk each component
* refusing a symlink not owned by uid 0 or our euid, then fchdir to
* the result. This still follows the operator's/root's own symlinked
* dest -- the `/backup -> /mnt/disk` / `/var/www -> /srv/www` admin
@@ -1252,7 +817,7 @@ int change_dir(const char *dir, int set_path_only)
* non-daemon receiver can opt back into the legacy plain chdir with
* --insecure-links. */
if (am_daemon && !am_chrooted) {
int dfd = open_no_attacker_symlinks(dir, O_RDONLY | O_DIRECTORY, 0);
int dfd = vfs_open_owner_walk(dir, O_RDONLY | O_DIRECTORY, 0, 0);
if (dfd < 0)
return 0;
if (fchdir(dfd) != 0) {
@@ -1283,7 +848,7 @@ int change_dir(const char *dir, int set_path_only)
* another uid. A real dir is opened directly. This closes the
* destination chdir TOCTOU; --insecure-links keeps the plain
* chdir for an operator whose dest is a foreign-owned symlink. */
dfd = open_no_attacker_symlinks(nf, O_RDONLY | O_DIRECTORY, 0);
dfd = vfs_open_owner_walk(nf, O_RDONLY | O_DIRECTORY, 0, 0);
if (dfd < 0)
return 0;
if (fchdir(dfd) != 0) {
@@ -1299,16 +864,16 @@ int change_dir(const char *dir, int set_path_only)
}
}
skipped_chdir = set_path_only;
memcpy(curr_dir, dir, len + 1);
memcpy(vfs.curr_dir, dir, len + 1);
} else {
unsigned int save_dir_len = curr_dir_len;
if (curr_dir_len + 1 + len >= sizeof curr_dir) {
unsigned int save_dir_len = vfs.curr_dir_len;
if (vfs.curr_dir_len + 1 + len >= sizeof vfs.curr_dir) {
errno = ENAMETOOLONG;
return 0;
}
if (!(curr_dir_len && curr_dir[curr_dir_len-1] == '/'))
curr_dir[curr_dir_len++] = '/';
memcpy(curr_dir + curr_dir_len, dir, len + 1);
if (!(vfs.curr_dir_len && vfs.curr_dir[vfs.curr_dir_len-1] == '/'))
vfs.curr_dir[vfs.curr_dir_len++] = '/';
memcpy(vfs.curr_dir + vfs.curr_dir_len, dir, len + 1);
if (!set_path_only) {
int chdir_failed;
@@ -1317,20 +882,20 @@ int change_dir(const char *dir, int set_path_only)
* target -- otherwise CWD escapes the module and
* every subsequent path-relative syscall (open,
* chmod, lchown, ...) inherits the escape, which
* defeats secure_relative_open's RESOLVE_BENEATH
* defeats vfs_resolve_open's RESOLVE_BENEATH
* anchor and re-opens the CVE-2026-29518 class of
* symlink TOCTOU attacks. Use the secure resolver
* to get a confined dirfd, then fchdir() to it.
*
* If skipped_chdir is set, a previous CD_SKIP_CHDIR
* call buffered an absolute prefix in curr_dir
* call buffered an absolute prefix in vfs.curr_dir
* (e.g. change_pathname's CD_SKIP_CHDIR to orig_dir)
* without syncing the kernel's CWD. Resolve `dir`
* relative to that prefix as basedir so the secure
* branch still anchors at the operator-trusted
* directory rather than wherever the kernel CWD
* happens to be. */
if (am_daemon && (!am_chrooted || module_dirlen) && !symlink_optout_allowed()) {
if (am_daemon && (!am_chrooted || module_dirlen) && !vfs_symlink_optout_allowed()) {
const char *basedir = NULL;
char prefix[MAXPATHLEN];
int dfd;
@@ -1340,11 +905,11 @@ int change_dir(const char *dir, int set_path_only)
chdir_failed = 1;
goto chdir_cleanup;
}
memcpy(prefix, curr_dir, save_dir_len);
memcpy(prefix, vfs.curr_dir, save_dir_len);
prefix[save_dir_len] = '\0';
basedir = prefix;
}
dfd = secure_relative_open(basedir, dir,
dfd = vfs_resolve_open(basedir, dir,
O_RDONLY | O_DIRECTORY, 0);
if (dfd < 0) {
chdir_failed = 1;
@@ -1352,19 +917,19 @@ int change_dir(const char *dir, int set_path_only)
chdir_failed = fchdir(dfd) != 0;
close(dfd);
}
} else if (am_daemon && symlink_optout_allowed()) {
} else if (am_daemon && vfs_symlink_optout_allowed()) {
/* "insecure links = yes": restore the 3.2.7 follow-any-symlink
* traversal with a plain chdir to the accumulated path, the same
* legacy behaviour the per-operation sites grant under the opt-out. */
chdir_failed = chdir(curr_dir) != 0;
chdir_failed = chdir(vfs.curr_dir) != 0;
} else if (!am_chrooted && !am_sender && !insecure_links) {
/* Non-daemon receiver: confine the operator-named relative
* destination like the absolute case above -- refuse a component
* symlink not owned by uid 0 or our euid, closing the
* relative-dest chdir TOCTOU while still following the operator's
* own symlinks. --insecure-links keeps the plain chdir. */
int dfd = open_no_attacker_symlinks(curr_dir,
O_RDONLY | O_DIRECTORY, 0);
int dfd = vfs_open_owner_walk(vfs.curr_dir,
O_RDONLY | O_DIRECTORY, 0, 0);
if (dfd < 0)
chdir_failed = 1;
else {
@@ -1372,30 +937,30 @@ int change_dir(const char *dir, int set_path_only)
close(dfd);
}
} else {
chdir_failed = chdir(curr_dir) != 0;
chdir_failed = chdir(vfs.curr_dir) != 0;
}
chdir_cleanup:
if (chdir_failed) {
curr_dir_len = save_dir_len;
curr_dir[curr_dir_len] = '\0';
vfs.curr_dir_len = save_dir_len;
vfs.curr_dir[vfs.curr_dir_len] = '\0';
return 0;
}
}
skipped_chdir = set_path_only;
}
curr_dir_len = clean_fname(curr_dir, CFN_COLLAPSE_DOT_DOT_DIRS | CFN_DROP_TRAILING_DOT_DIR);
vfs.curr_dir_len = clean_fname(vfs.curr_dir, CFN_COLLAPSE_DOT_DOT_DIRS | CFN_DROP_TRAILING_DOT_DIR);
if (sanitize_paths) {
if (module_dirlen > curr_dir_len)
module_dirlen = curr_dir_len;
curr_dir_depth = count_dir_elements(curr_dir + module_dirlen);
if (module_dirlen > vfs.curr_dir_len)
module_dirlen = vfs.curr_dir_len;
curr_dir_depth = count_dir_elements(vfs.curr_dir + module_dirlen);
}
if (!set_path_only) /* a real chdir invalidates the cwd-relative dir-fd stack */
reset_dir_fd_cache();
vfs_dircache_reset();
if (DEBUG_GTE(CHDIR, 1) && !set_path_only)
rprintf(FINFO, "[%s] change_dir(%s)\n", who_am_i(), curr_dir);
rprintf(FINFO, "[%s] change_dir(%s)\n", who_am_i(), vfs.curr_dir);
return 1;
}
@@ -1408,12 +973,12 @@ char *normalize_path(char *path, BOOL force_newbuf, unsigned int *len_ptr)
if (*path != '/') { /* Make path absolute. */
int len = strlen(path);
if (curr_dir_len + 1 + len >= sizeof curr_dir)
if (vfs.curr_dir_len + 1 + len >= sizeof vfs.curr_dir)
return NULL;
curr_dir[curr_dir_len] = '/';
memcpy(curr_dir + curr_dir_len + 1, path, len + 1);
path = strdup(curr_dir);
curr_dir[curr_dir_len] = '\0';
vfs.curr_dir[vfs.curr_dir_len] = '/';
memcpy(vfs.curr_dir + vfs.curr_dir_len + 1, path, len + 1);
path = strdup(vfs.curr_dir);
vfs.curr_dir[vfs.curr_dir_len] = '\0';
} else if (force_newbuf)
path = strdup(path);
@@ -1445,7 +1010,7 @@ char *full_fname(const char *fn)
if (*fn == '/')
p1 = p2 = "";
else {
p1 = curr_dir + module_dirlen;
p1 = vfs.curr_dir + module_dirlen;
for (p2 = p1; *p2 == '/'; p2++) {}
if (*p2)
p2 = "/";
@@ -1515,26 +1080,22 @@ int handle_partial_dir(const char *fname, int create)
* outside the tree): resolve it with the ownership walk -- follow a
* uid0/euid-owned symlink, refuse a foreign one, absolute and relative alike.
* --insecure-links (or a daemon module's "insecure links =") opts out. */
operator_path_resolve = 1;
if (create) {
STRUCT_STAT st;
int statret = do_lstat_at(dir, &st);
int statret = vfs_lstat(VFS_AT_FDCWD, dir, &st, VFS_OPERATOR_PATH);
if (statret == 0 && !S_ISDIR(st.st_mode)) {
if (do_unlink_at(dir) < 0) {
operator_path_resolve = 0;
if (vfs_unlink(VFS_AT_FDCWD, dir, VFS_OPERATOR_PATH) < 0) {
*fn = '/';
return 0;
}
statret = -1;
}
if (statret < 0 && do_mkdir_at(dir, 0700) < 0) {
operator_path_resolve = 0;
if (statret < 0 && vfs_mkdir(VFS_AT_FDCWD, dir, 0700, VFS_OPERATOR_PATH) < 0) {
*fn = '/';
return 0;
}
} else
do_rmdir_at(dir);
operator_path_resolve = 0;
vfs_unlink(VFS_AT_FDCWD, dir, VFS_REMOVEDIR | VFS_OPERATOR_PATH);
*fn = '/';
return 1;
+347
View File
@@ -0,0 +1,347 @@
/*
* vfs/chmod.c - chmod wrappers (path, parent-resolved, held-dirfd).
*
* Includes the platform-specific lchmod/setattrlist/SYS_fchmodat2 handling and
* the leaf-safe do_fchmodat_nofollow helper.
*
* 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"
#ifdef HAVE_SYS_ATTR_H
#include <sys/attr.h> /* for the macOS setattrlist() chmod path */
#endif
#ifdef __linux__
#include <sys/syscall.h> /* SYS_fchmodat2 raw-syscall wrapper */
#endif
#ifdef HAVE_CHMOD
static int vfs__chmod_plain(const char *path, mode_t mode)
{
static int switch_step = 0;
int code;
if (dry_run) return 0;
RETURN_ERROR_IF_RO_OR_LO;
RETURN_ERROR_IF_NULL(path);
switch (switch_step) {
#ifdef HAVE_LCHMOD
case 0:
if ((code = lchmod(path, mode & CHMOD_BITS)) == 0)
break;
if (errno == ENOSYS)
switch_step++;
else if (errno != ENOTSUP)
break;
#endif
/* FALLTHROUGH */
default:
if (S_ISLNK(mode)) {
# if defined HAVE_SETATTRLIST
struct attrlist attrList;
uint32_t m = mode & CHMOD_BITS; /* manpage is wrong: not mode_t! */
memset(&attrList, 0, sizeof attrList);
attrList.bitmapcount = ATTR_BIT_MAP_COUNT;
attrList.commonattr = ATTR_CMN_ACCESSMASK;
if ((code = setattrlist(path, &attrList, &m, sizeof m, FSOPT_NOFOLLOW)) == 0)
break;
if (errno == ENOTSUP)
code = 1;
# else
code = 1;
# endif
} else
code = chmod(path, mode & CHMOD_BITS); /* DISCOURAGED FUNCTION */
break;
}
if (code != 0 && (preserve_perms || preserve_executability))
return code;
return 0;
}
/* chmod `name` relative to dfd without following a final-component symlink.
* The held parent fd confines the ancestors; this closes the leaf race (an
* attacker swapping the leaf to a symlink that fchmodat(...,0) would follow out
* of the tree).
*
* Never follows the leaf: a regular file or dir is pinned via
* openat(O_NOFOLLOW) and chmod'd with fchmod() (leaf-safe, every kernel, and
* fakeroot-wrappable unlike the raw fchmodat2() syscall); a symlink leaf is
* refused (ELOOP, or EMLINK/EFTYPE on the BSDs). Other types or an open
* failure fall to fchmodat(AT_SYMLINK_NOFOLLOW) (a real no-follow chmod on
* glibc>=2.32 / Linux>=6.6), then the raw fchmodat2() syscall. If no
* no-follow primitive exists we skip with a warning rather than follow the
* leaf.
*
* A FIFO takes the fd path on Linux and the pathname path elsewhere -- see the
* S_ISFIFO arm below for why, and for what that costs. Note the type used to
* choose between them comes from the lstat above, so a leaf swapped between
* that and the open is classified by what it WAS: an observed regular file or
* dir that becomes a FIFO is still opened. O_NOFOLLOW rejects symlinks, not
* type changes. Constraining the open to the observed type would close that;
* it is not done here. */
static int do_fchmodat_nofollow(int dfd, const char *name, mode_t mode)
{
#if defined AT_FDCWD && defined AT_SYMLINK_NOFOLLOW
mode &= CHMOD_BITS;
# ifdef O_NOFOLLOW
{
STRUCT_STAT st;
int oflags = O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_NOCTTY;
if (vfs_lstat(dfd, name, &st, 0) < 0)
return -1;
if (S_ISLNK(st.st_mode)) {
errno = ELOOP; /* refuse to chmod through a symlink leaf */
return -1;
}
if (S_ISREG(st.st_mode) || S_ISDIR(st.st_mode) || S_ISFIFO(st.st_mode)) {
int fd;
# ifndef __linux__
/* Never open a FIFO here. Opening one -- even O_NONBLOCK --
* makes this process a reader for as long as the descriptor
* lives, which wakes a writer blocked in open(O_WRONLY) and
* can cost it a SIGPIPE or the bytes it writes before we
* close. The pathname call reaches the same end state
* without that: it succeeds outright when the mode is
* grantable, and when macOS refuses an ungrantable setgid
* with EPERM (having applied nothing), asking again without
* that bit gives exactly what fchmod() would have -- it drops
* the bit it cannot grant and applies the ordinary ones.
* Measured on macOS: fchmodat(2750) EPERM leaving 0600,
* fchmodat(0750) ok giving 0750, for a FIFO and a directory
* alike.
*
* This is a pathname call, so unlike the descriptor path it
* does not pin the inode; a leaf swapped for another object
* of the same name is chmod'd instead. AT_SYMLINK_NOFOLLOW
* still keeps it off a symlink's target. That trade buys
* away the reader hazard, and only for FIFOs.
*
* Only S_ISGID is retried. An ungrantable S_ISUID would
* still fail where fchmod() would have cleared it, but
* setuid is meaningless on a FIFO and the behaviour is
* undemonstrated, so it is not coded for.
*
* Linux keeps the fd-first order it has always had. */
if (S_ISFIFO(st.st_mode)) {
if (fchmodat(dfd, name, mode, AT_SYMLINK_NOFOLLOW) == 0)
return 0;
if (errno == EPERM && (mode & S_ISGID)
&& fchmodat(dfd, name, mode & ~S_ISGID,
AT_SYMLINK_NOFOLLOW) == 0)
return 0;
return -1;
}
# endif
# ifdef O_CLOEXEC
oflags |= O_CLOEXEC;
# endif
fd = openat(dfd, name, oflags);
if (fd >= 0) {
int r = fchmod(fd, mode), e = errno;
close(fd);
errno = e;
return r;
}
/* A leaf swapped for a symlink between the lstat above and
* this open: refuse rather than fall through. The errno is
* not the same everywhere -- Linux/Solaris ELOOP, FreeBSD
* EMLINK, NetBSD EFTYPE. */
if (errno == ELOOP
# ifdef EMLINK
|| errno == EMLINK
# endif
# ifdef EFTYPE
|| errno == EFTYPE
# endif
)
return -1; /* raced to a symlink: refuse */
/* otherwise (e.g. EACCES on an unreadable file) fall through */
}
}
# endif
# if defined __linux__
{
int r = fchmodat(dfd, name, mode, AT_SYMLINK_NOFOLLOW);
if (r == 0)
return 0;
if (errno != ENOTSUP && errno != EOPNOTSUPP && errno != ENOSYS)
return r; /* a real error (EPERM, ENOENT, ...) */
}
# ifdef SYS_fchmodat2
{
int r = syscall(SYS_fchmodat2, dfd, name, (unsigned int)mode, AT_SYMLINK_NOFOLLOW);
if (r == 0)
return 0;
if (errno != ENOSYS && errno != EPERM && errno != EOPNOTSUPP)
return r;
}
# endif
/* No symlink-safe chmod primitive here: skip rather than follow the leaf. */
rprintf(FWARNING, "vfs_chmod: no symlink-safe chmod for \"%s\"; mode not set\n", name);
return 1;
# else
return fchmodat(dfd, name, mode, AT_SYMLINK_NOFOLLOW);
# endif
#else
(void)dfd;
(void)mode;
/* No symlink-safe chmod primitive here: skip rather than follow the leaf. */
rprintf(FWARNING, "vfs_chmod: no symlink-safe chmod for \"%s\"; mode not set\n", name);
return 1;
#endif
}
/*
Symlink-race-safe variant of vfs_chmod() for receiver-side use.
Threat model: on a daemon running with "use chroot = no" (the prerequisite
for CVE-2026-29518), a local attacker can race a symlink swap of one of
the parent directory components of a path the receiver is about to chmod.
Because chmod() resolves symlinks at every component, the swap redirects
the chmod outside the receiver's confinement.
Defence: open the *parent* directory of fname under vfs_resolve_open()
(a portable per-component O_NOFOLLOW walk on held parent dirfds) and do
fchmodat() against that dirfd. A symlink substituted into one of the parent
components is then either followed within the tree (legitimate dir-symlinks
still work) or rejected (escape attempts fail).
Final-component handling matches vfs_chmod(): fchmodat() with flag 0
follows a symlink at the final component, which is the same behaviour as
chmod() and matches every current call site (the file being chmod'd is
one the receiver itself just created or transferred). For the rare case
where the caller wants to chmod a symlink-as-an-object (S_ISLNK in the
mode bits), we fall through to vfs_chmod() which has portability code for
that case.
Falls back to vfs_chmod() for absolute paths and for paths with no parent
component, where there is nothing to protect against.
*/
static int vfs__chmod_secure(const char *fname, mode_t mode, int flags)
{
#ifdef AT_FDCWD
char dirpath[MAXPATHLEN];
const char *bname;
const char *slash;
int dfd, ret, e;
size_t dlen;
if (dry_run) return 0;
RETURN_ERROR_IF_RO_OR_LO;
#if defined O_NOFOLLOW && defined O_DIRECTORY
/* Operator-supplied path: resolve the parent via the ownership walk, as
* the other VFS wrappers do. Without this the caller's VFS_OPERATOR_PATH
* has no effect here, and an absolute name would fall straight through to
* the unconfined full-path chmod. S_ISLNK(mode) still needs the plain
* lchmod()/setattrlist() handling. */
if ((flags & VFS_OPERATOR_PATH) && fname && *fname && !S_ISLNK(mode)) {
if (vfs_symlink_optout_allowed())
return vfs__chmod_plain(fname, mode);
dfd = vfs_owner_walk_parent(fname, &bname, 1);
if (dfd < 0)
return -1;
ret = do_fchmodat_nofollow(dfd, bname, mode);
e = errno;
close(dfd);
errno = e;
return ret;
}
#endif
/* Only the daemon-without-chroot case is exposed to the symlink-
* race attack: a chroot already confines the receiver, and a
* non-daemon rsync runs with the user's own authority so a
* symlink they planted can only redirect to files they could
* already access. Everywhere else, fall through to plain
* vfs_chmod() to avoid the dirfd-open overhead on every call. */
if (!vfs_relpath_active())
return vfs__chmod_plain(fname, mode);
if (!fname || !*fname || *fname == '/' || S_ISLNK(mode))
return vfs__chmod_plain(fname, mode);
slash = strrchr(fname, '/');
if (!slash)
return vfs__chmod_plain(fname, mode);
dlen = slash - fname;
if (dlen >= sizeof dirpath) {
errno = ENAMETOOLONG;
return -1;
}
memcpy(dirpath, fname, dlen);
dirpath[dlen] = '\0';
bname = slash + 1;
dfd = vfs_resolve_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0);
if (dfd < 0)
return -1;
ret = do_fchmodat_nofollow(dfd, bname, mode);
e = errno;
close(dfd);
errno = e;
return ret;
#else
(void)flags;
return vfs__chmod_plain(fname, mode);
#endif
}
#endif
/* Unified chmod. dirfd == VFS_AT_FDCWD resolves `path`; a real held dirfd makes
* `path` a single component chmod'd (no-follow leaf, via do_fchmodat_nofollow)
* under it. flags: VFS_ALLOW_SYMLINK (trusted, plain chmod), default 0 (secure
* receiver resolve). A symlink-as-object (S_ISLNK(mode)) goes through the plain
* lchmod/setattrlist path. */
#ifdef HAVE_CHMOD
int vfs_chmod(int dirfd, const 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: reject empty, multi-component and ".." (writing the
* parent of the pinned dir); "." (chmod the dir itself) is a
* legitimate single-component op. */
if (!*path || strchr(path, '/')
|| (path[0] == '.' && path[1] == '.' && path[2] == '\0')) {
errno = EINVAL;
return -1;
}
return do_fchmodat_nofollow(dirfd, path, mode);
#else
(void)dirfd; (void)mode;
errno = ENOSYS;
return -1;
#endif
}
if (flags & VFS_ALLOW_SYMLINK)
return vfs__chmod_plain(path, mode);
return vfs__chmod_secure(path, mode, flags);
}
/* Mode on an already-open fd (no path, no symlink to follow): the race-free
* counterpart for a pinned cross-tree operator leaf -- see set_file_attrs(). */
int vfs_fchmod(int fd, mode_t mode)
{
if (dry_run) return 0;
RETURN_ERROR_IF_RO_OR_LO;
return fchmod(fd, mode);
}
#endif
+126
View File
@@ -0,0 +1,126 @@
/*
* vfs/chown.c - lchown 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"
#ifndef HAVE_LCHOWN
#define lchown chown
#endif
static int vfs__lchown_plain(const char *path, uid_t owner, gid_t group)
{
return lchown(path, owner, group);
}
/* Secure receiver-side resolve: open the parent under vfs_resolve_open() and
* fchownat(..., AT_SYMLINK_NOFOLLOW) so a parent-component symlink swap can't
* redirect the chown outside the module. VFS_OPERATOR_PATH takes the ownership
* walk instead, as the other VFS wrappers do. Falls through to the plain
* lchown in non-daemon/sender, chrooted, no-parent and absolute-path cases. */
static int vfs__lchown_secure(const char *path, uid_t owner, gid_t group, int flags)
{
#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY && defined AT_SYMLINK_NOFOLLOW
char dirpath[MAXPATHLEN];
const char *bname, *slash;
int dfd, ret, e;
size_t dlen;
/* Operator-supplied path: without this branch the caller's
* VFS_OPERATOR_PATH has no effect here and an absolute name would fall
* straight through to the unconfined full-path lchown. */
if ((flags & VFS_OPERATOR_PATH) && path && *path) {
if (vfs_symlink_optout_allowed())
return vfs__lchown_plain(path, owner, group);
dfd = vfs_owner_walk_parent(path, &bname, 1);
if (dfd < 0)
return -1;
ret = fchownat(dfd, bname, owner, group, AT_SYMLINK_NOFOLLOW);
e = errno;
close(dfd);
errno = e;
return ret;
}
if (!vfs_relpath_active() || !*path || *path == '/')
return vfs__lchown_plain(path, owner, group);
slash = strrchr(path, '/');
if (!slash)
return vfs__lchown_plain(path, owner, group);
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 = fchownat(dfd, bname, owner, group, AT_SYMLINK_NOFOLLOW);
e = errno;
close(dfd);
errno = e;
return ret;
#else
(void)flags;
return vfs__lchown_plain(path, owner, group);
#endif
}
/* Unified lchown. dirfd == VFS_AT_FDCWD resolves `path`; a real held dirfd
* makes `path` a single component chowned (no-follow) under it. flags:
* VFS_ALLOW_SYMLINK (trusted, plain lchown), default 0 (secure receiver
* resolve). */
int vfs_lchown(int dirfd, const char *path, uid_t owner, gid_t group, int flags)
{
if (dry_run) return 0;
RETURN_ERROR_IF_RO_OR_LO;
RETURN_ERROR_IF_NULL(path);
if (dirfd != VFS_AT_FDCWD) {
#if defined AT_FDCWD && defined AT_SYMLINK_NOFOLLOW
/* Held-fd: reject empty, multi-component and ".." (writing the
* parent of the pinned dir); "." (chown the dir itself) is a
* legitimate single-component op. */
if (!*path || strchr(path, '/')
|| (path[0] == '.' && path[1] == '.' && path[2] == '\0')) {
errno = EINVAL;
return -1;
}
return fchownat(dirfd, path, owner, group, AT_SYMLINK_NOFOLLOW);
#else
(void)dirfd; (void)owner; (void)group;
errno = ENOSYS;
return -1;
#endif
}
if (flags & VFS_ALLOW_SYMLINK)
return vfs__lchown_plain(path, owner, group);
return vfs__lchown_secure(path, owner, group, flags);
}
/* Mode/owner on an already-open fd (no path, no symlink to follow): the
* race-free way to set metadata on a cross-tree operator-path leaf that was
* pinned with O_NOFOLLOW. See set_file_attrs(). */
int vfs_fchown(int fd, uid_t owner, gid_t group)
{
if (dry_run) return 0;
RETURN_ERROR_IF_RO_OR_LO;
return fchown(fd, owner, group);
}
+228
View File
@@ -0,0 +1,228 @@
/*
* vfs/copy_file.c - compound VFS op: copy a file's contents (and, for
* --xattrs, its xattrs) to a new destination. Layered on the vfs_* open/
* read/write primitives; calls out to the metadata layer (copy_xattrs) for
* the held-fd xattr copy.
*
* Moved out of util1.c as part of the VFS compound layer.
*
* Copyright (C) 1996-2022 Andrew Tridgell, Paul Mackerras, 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"
extern int do_fsync;
extern int preallocate_files;
extern int preserve_xattrs;
/* Read @p len bytes at @p ptr from descriptor @p desc, retrying if interrupted.
* Returns the number of bytes read (0 = EOF), or <0 on error. */
static int safe_read(int desc, char *ptr, size_t len)
{
int n_chars;
if (len == 0)
return len;
do {
n_chars = read(desc, ptr, len);
} while (n_chars < 0 && errno == EINTR);
return n_chars;
}
/* Remove existing file @dest and reopen, creating a new file with @mode.
* vfs_flags carries the resolution policy (VFS_OPERATOR_PATH for an operator
* dest) to both the robust_unlink and the create. */
static int unlink_and_reopen(const char *dest, mode_t mode, int vfs_flags)
{
int ofd;
if (robust_unlink(dest, vfs_flags) && errno != ENOENT) {
int save_errno = errno;
rsyserr(FERROR_XFER, errno, "unlink %s", full_fname(dest));
errno = save_errno;
return -1;
}
#ifdef SUPPORT_XATTRS
if (preserve_xattrs)
mode |= S_IWUSR;
#endif
mode &= INITACCESSPERMS;
/* Use vfs_open_at so the create/truncate goes through a secure
* parent dirfd in the daemon-no-chroot deployment. Otherwise
* an attacker could swap a parent component with a symlink in
* the window between robust_unlink (which uses vfs_unlink,
* already secure) and the create here, and redirect the new
* file outside the module. */
if ((ofd = vfs_open_at(dest, O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, mode, vfs_flags)) < 0) {
int save_errno = errno;
rsyserr(FERROR_XFER, save_errno, "open %s", full_fname(dest));
errno = save_errno;
return -1;
}
return ofd;
}
/* Copy contents of file @source to file @dest with mode @mode.
*
* If @tmpfilefd is < 0, copy_file unlinks @dest and then opens a new
* file with name @dest.
*
* Otherwise, copy_file writes to and closes the provided file
* descriptor.
*
* In either case, if --xattrs are being preserved, the dest file will
* have its xattrs set from the source file.
*
* This is used in conjunction with the --temp-dir, --backup, and
* --copy-dest options. */
int copy_file(const char *source, const char *dest, int tmpfilefd, mode_t mode, int vfs_flags)
{
int ifd, ofd;
char buf[1024 * 8];
int len; /* Number of bytes read into `buf'. */
OFF_T prealloc_len = 0, offset = 0;
/* For any hardened (non-chrooted) receiver, route the source open through
* vfs_resolve_open so a parent-symlink on the source path (e.g.
* --copy-dest=cd where cd is a symlink to an outside directory) cannot
* redirect the read to a file the attacker should not see. Plain
* vfs_open_nofollow only refuses a final-component symlink; parents are
* still followed. An ABSOLUTE source is an operator basis (e.g. an absolute
* --copy-dest): confine its parents via the ownership walk -- a foreign-owned
* parent symlink is refused, the operator's own dirs/uid0/euid symlinks
* followed -- so a flipped parent can't redirect the basis read out of tree.
* The walk runs with is_operator=1 (module-exclude enforced) and pins only
* the source side, leaving the dest open untouched -- this is why confining
* the source here does not re-open the copy_xattrs dest race the way wrapping
* the whole copy_altdest_file would. */
if (vfs_relpath_active() && source && *source && source[0] != '/')
ifd = vfs_resolve_open(NULL, source, O_RDONLY | O_NOFOLLOW, 0);
#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY
else if (vfs_relpath_active() && source && source[0] == '/'
&& !vfs_symlink_optout_allowed()) {
int dfd, e;
const char *leaf;
dfd = vfs_owner_walk_parent(source, &leaf, 1);
if (dfd < 0)
ifd = -1;
else {
ifd = openat(dfd, leaf, O_RDONLY | O_NOFOLLOW);
e = errno;
close(dfd);
errno = e;
}
}
#endif
else
ifd = vfs_open_nofollow(source, O_RDONLY);
if (ifd < 0) {
int save_errno = errno;
rsyserr(FERROR_XFER, errno, "open %s", full_fname(source));
errno = save_errno;
return -1;
}
if (tmpfilefd >= 0) {
ofd = tmpfilefd;
} else {
ofd = unlink_and_reopen(dest, mode, vfs_flags);
if (ofd < 0) {
int save_errno = errno;
close(ifd);
errno = save_errno;
return -1;
}
}
#ifdef SUPPORT_PREALLOCATION
if (preallocate_files) {
STRUCT_STAT srcst;
/* Try to preallocate enough space for file's eventual length. Can
* reduce fragmentation on filesystems like ext4, xfs, and NTFS. */
if (vfs_fstat(ifd, &srcst) < 0)
rsyserr(FWARNING, errno, "fstat %s", full_fname(source));
else if (srcst.st_size > 0) {
prealloc_len = vfs_fallocate(ofd, 0, srcst.st_size);
if (prealloc_len < 0)
rsyserr(FWARNING, errno, "vfs_fallocate %s", full_fname(dest));
}
}
#endif
while ((len = safe_read(ifd, buf, sizeof buf)) > 0) {
if (full_write(ofd, buf, len) < 0) {
int save_errno = errno;
rsyserr(FERROR_XFER, errno, "write %s", full_fname(dest));
close(ifd);
close(ofd);
errno = save_errno;
return -1;
}
offset += len;
}
if (len < 0) {
int save_errno = errno;
rsyserr(FERROR_XFER, errno, "read %s", full_fname(source));
close(ifd);
close(ofd);
errno = save_errno;
return -1;
}
/* Source file might have shrunk since we fstatted it.
* Cut off any extra preallocated zeros from dest file. */
if (offset < prealloc_len) {
#ifdef HAVE_FTRUNCATE
/* If we fail to truncate, the dest file may be wrong, so we
* must trigger the "partial transfer" error. */
if (vfs_ftruncate(ofd, offset) < 0)
rsyserr(FERROR_XFER, errno, "ftruncate %s", full_fname(dest));
#else
rprintf(FERROR_XFER, "no ftruncate for over-long pre-alloc: %s", full_fname(dest));
#endif
}
if (do_fsync && fsync(ofd) < 0) {
int save_errno = errno;
rsyserr(FERROR, errno, "fsync failed on %s", full_fname(dest));
close(ofd);
close(ifd); /* ifd is held open until after the xattr copy below */
errno = save_errno;
return -1;
}
#ifdef SUPPORT_XATTRS
/* Read the source xattrs through the held source fd (ifd) and set them
* through ofd while both are still held, so a parent-symlink race can't
* redirect the read out of tree or the write onto a file outside it. */
if (preserve_xattrs)
copy_xattrs(source, ifd, dest, ofd);
#endif
if (close(ifd) < 0) {
rsyserr(FWARNING, errno, "close failed on %s",
full_fname(source));
}
if (close(ofd) < 0) {
int save_errno = errno;
rsyserr(FERROR_XFER, errno, "close failed on %s", full_fname(dest));
errno = save_errno;
return -1;
}
return 0;
}
+244
View File
@@ -0,0 +1,244 @@
/*
* vfs/dircache.c - persistent ancestor-dirfd cache for held-directory traversal.
*
* The file list is path-sorted, so consecutive directory resolutions share a
* long leading prefix. Rather than re-resolve a full path from the anchor per
* file, we keep the whole current ancestor chain open as pinned, race-safe
* dirfds and reuse the longest common component prefix on the next resolution.
* vfs_opendir() hands out a held dirfd (or -1 to fall back); vfs_dircache_reset()
* drops the chain (called by change_dir() on a real chdir). 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"
/* Held-directory-fd traversal.
*
* Rather than re-resolve a full path on every syscall (do_*_at() re-opens the
* parent via vfs_resolve_open() each call), the generator and receiver
* open each directory ONCE via vfs_opendir() and issue single-component
* *at() ops against that held dirfd with the do_*_atfd() wrappers below. The
* parent is a pinned fd, not re-resolved, so the per-entry symlink-race window
* is closed and the re-resolution overhead is gone.
*
* vfs_opendir() owns both the authority gate and the resolver choice: it
* returns a held dirfd only when hardened resolution is in effect, else -1
* with errno==0 so the caller falls back to the do_*_at() wrappers
* (behaviour-neutral). The do_*_atfd() wrappers are thin shims with the same
* leaf semantics as do_*_at() (dry-run/read-only guards, AT_SYMLINK_NOFOLLOW,
* fake-super placeholder files); they never re-check the gate or re-resolve a
* parent. */
int vfs_opendir(const char *dirname)
{
#ifdef AT_FDCWD
int dfd;
/* Authority gate, identical to the do_*_at() wrappers. When hardened
* resolution isn't in effect, return -1 with errno cleared so the caller
* uses the full-path wrappers. */
if (!vfs_relpath_active()) {
errno = 0;
return -1;
}
if (!dirname || !*dirname) {
/* The transfer root itself (file->dirname == NULL): the cwd. */
dfd = openat(AT_FDCWD, ".", O_RDONLY | O_DIRECTORY);
} else if (dirname[0] == '/') {
/* An absolute dirname is not expected for an in-transfer entry;
* leave it to the legacy path. */
errno = 0;
return -1;
} else {
dfd = vfs_resolve_open(NULL, dirname, O_RDONLY | O_DIRECTORY, 0);
}
if (dfd >= 0) {
/* O_CLOEXEC on every tier (the per-component walk fallback
* doesn't thread our flags onto the returned dirfd). */
int fl = fcntl(dfd, F_GETFD);
if (fl >= 0)
fcntl(dfd, F_SETFD, fl | FD_CLOEXEC);
}
return dfd;
#else
(void)dirname;
errno = 0;
return -1;
#endif
}
/* Persistent ancestor-dirfd stack for held-directory traversal.
*
* The transfer's file list is path-sorted, so iterating it walks the tree in
* DFS order and consecutive directory resolutions share a long leading prefix.
* Rather than re-resolve a full path from the anchor each time (re-opening
* every ancestor dir per file), we keep the whole current ancestor chain open
* as pinned, race-safe dirfds and, on the next resolution, reuse the longest
* common component prefix -- popping only the divergent tail and descending the
* new tail. Each directory is then opened once while we are inside its subtree.
*
* The chain is relative to the process cwd (for a NULL anchor), so change_dir()
* drops it on any real chdir; it otherwise persists across flist chunks (the
* pinned fds stay valid, and a raced/replaced ancestor resolves to the original
* inode the fd holds -- the held-dirfd race-safety property, not a hazard).
* Each component is resolved with ds_descend(), which follows in-tree directory
* symlinks exactly as vfs_resolve_open() does; only the resolved dir fd is
* kept (intermediate symlink-target fds are closed -- sound, since an open
* dirfd needs no live parent). */
#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY
void vfs_dircache_reset(void)
{
while (vfs.dpc.depth > 0)
close(vfs.dpc.fd[--vfs.dpc.depth]);
if (vfs.dpc.base >= 0)
close(vfs.dpc.base);
vfs.dpc.base = -1;
vfs.dpc.anchor = VFS_DPC_ANCHOR_NONE;
}
/* Resolve directory `dirpath` beneath `anchor` (NULL = cwd, else an absolute
* trusted root), reusing the held ancestor stack. Returns a BORROWED dirfd
* owned by the cache (do NOT close), or -1 (errno preserved for a real open
* error, errno==0 for an uncacheable path -- "..", too deep/long, or a relative
* non-cwd anchor) so the caller can fall back to vfs_resolve_open(). */
static int dpc_dir_fd(const char *anchor, const char *dirpath)
{
char copy[MAXPATHLEN];
char *comps[VFS_DPC_MAXDEPTH];
char *sv = NULL;
int nc = 0, p, i;
if (anchor && anchor[0] != '/') { errno = 0; return -1; }
if (!dirpath)
dirpath = "";
if (dirpath[0] == '/') { errno = 0; return -1; }
if (anchor != vfs.dpc.anchor || vfs.dpc.base < 0) {
int fl;
vfs_dircache_reset();
vfs.dpc.base = open_anchor_dirfd(anchor ? anchor : ".");
if (vfs.dpc.base < 0)
return -1;
if ((fl = fcntl(vfs.dpc.base, F_GETFD)) >= 0)
fcntl(vfs.dpc.base, F_SETFD, fl | FD_CLOEXEC);
vfs.dpc.anchor = anchor;
}
if (strlcpy(copy, dirpath, sizeof copy) >= sizeof copy) { errno = ENAMETOOLONG; return -1; }
for (char *c = strtok_r(copy, "/", &sv); c; c = strtok_r(NULL, "/", &sv)) {
if (c[0] == '.' && c[1] == '\0')
continue; /* "." */
if (c[0] == '.' && c[1] == '.' && c[2] == '\0') { errno = 0; return -1; }
if (nc >= VFS_DPC_MAXDEPTH || strlen(c) >= sizeof vfs.dpc.name[0]) {
/* Too deep / a too-long component to cache. Release the held
* ancestor fds first so the caller's full-path fallback walk does
* not stack on top of them: a deep tree plus a low RLIMIT_NOFILE
* (e.g. OpenBSD's default 128) would otherwise exhaust descriptors
* (cache depth + walk depth). */
vfs_dircache_reset();
errno = 0;
return -1;
}
comps[nc++] = c;
}
/* Reuse the longest common prefix; drop the divergent tail. */
for (p = 0; p < vfs.dpc.depth && p < nc && strcmp(vfs.dpc.name[p], comps[p]) == 0; p++)
;
while (vfs.dpc.depth > p)
close(vfs.dpc.fd[--vfs.dpc.depth]);
/* Descend the new tail, holding each resolved component. */
for (i = p; i < nc; i++) {
int afd = vfs.dpc.depth > 0 ? vfs.dpc.fd[vfs.dpc.depth-1] : vfs.dpc.base;
struct dirstack ds;
int hops = SECURE_OPEN_MAXSYMLINKS;
int fd, fl;
if (ds_init(&ds, afd) < 0)
return -1;
if (ds_descend(&ds, comps[i], &hops) < 0) {
int e = errno;
ds_free(&ds);
errno = e;
return -1;
}
fd = ds_take(&ds);
ds_free(&ds); /* closes intermediate symlink fds, not afd */
if (fd < 0)
return -1;
if ((fl = fcntl(fd, F_GETFD)) >= 0)
fcntl(fd, F_SETFD, fl | FD_CLOEXEC);
strlcpy(vfs.dpc.name[vfs.dpc.depth], comps[i], sizeof vfs.dpc.name[0]);
vfs.dpc.fd[vfs.dpc.depth++] = fd;
}
return nc > 0 ? vfs.dpc.fd[vfs.dpc.depth-1] : vfs.dpc.base;
}
/* Public entry for the sender (no vfs_relpath_active gate: its send paths
* confine unconditionally). Borrowed fd; -1 => caller uses the full walk. */
int vfs_path_dirfd(const char *anchor, const char *dirpath)
{
return dpc_dir_fd(anchor, dirpath);
}
int vfs_get_dirfd(const char *dirname)
{
if (!vfs_relpath_active()) { errno = 0; return -1; }
return dpc_dir_fd(NULL, dirname);
}
#else
void vfs_dircache_reset(void)
{
}
int vfs_path_dirfd(const char *anchor, const char *dirpath)
{
(void)anchor;
(void)dirpath;
errno = 0;
return -1;
}
int vfs_get_dirfd(const char *dirname)
{
(void)dirname;
errno = 0;
return -1;
}
#endif
/* Return the cached current-directory fd iff `path` lives directly in the
* entry's own directory (file->dirname) -- the common case for held-dirfd
* traversal. Returns -1 (caller falls back to the do_*_at() wrappers) for
* anything elsewhere: --temp-dir/--partial-dir/--backup-dir, an absolute path,
* a differently-nested dir, or when vfs_opendir() is gated off. The dirfd
* is opened once and cached.
*
* file->basename is NOT assumed to equal `path`'s leaf (a temp file has a
* different basename), so the caller derives the leaf from `path`. */
int vfs_cached_dirfd(const char *path, const struct file_struct *file)
{
const char *slash, *dn;
size_t plen;
if (!path || *path == '/')
return -1;
dn = file && file->dirname ? file->dirname : "";
slash = strrchr(path, '/');
plen = slash ? (size_t)(slash - path) : 0;
if (strlen(dn) != plen || memcmp(path, dn, plen) != 0)
return -1;
return vfs_get_dirfd(file ? file->dirname : NULL);
}
+341
View File
@@ -0,0 +1,341 @@
/*
* vfs/dirstack.c - race-safe component-walk primitives for rsync's VFS.
*
* The dirstack walks a relative path one component at a time, keeping an open
* dirfd for every ancestor from the anchor down, so a parent renamed mid-walk
* cannot redirect the climb (TOCTOU). Plus the module-confinement helpers that
* decide whether a resolved absolute path has escaped the served module root.
* 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 "vfs/vfs_internal.h"
/* Returns 1 if path has any "/"-separated component that is exactly
* "..", 0 otherwise. Used by vfs_resolve_open's front-door
* validation to reject ".." inputs (bare "..", "foo/..", "subdir/..")
* for non-re-anchored paths; the walk itself resolves an in-tree ".."
* safely (ds_descend pops to the held parent) for a re-anchored path. */
int path_has_dotdot_component(const char *path)
{
const char *p = path;
while (*p) {
const char *q;
if (*p == '/') { p++; continue; }
q = p;
while (*q && *q != '/')
q++;
if (q - p == 2 && p[0] == '.' && p[1] == '.')
return 1;
p = q;
}
return 0;
}
/* True if `path` lies within directory `root` (path == root, or path begins
* with root followed by '/'). `rootlen` is strlen(root). */
static int path_within(const char *root, size_t rootlen, const char *path)
{
return strncmp(path, root, rootlen) == 0
&& (path[rootlen] == '\0' || path[rootlen] == '/');
}
/* Refuse (return 1) when the ABSOLUTE resolved path `abspath` lands OUTSIDE the
* serving module's root, for an operator/peer-supplied path that must stay in the
* module (--partial-dir/--backup-dir/alt-basis: is_operator). An
* in-tree symlink owned by uid 0 / the euid is followed by design, so it can
* redirect the resolved target outside the module; this catches that escape.
*
* This is module-ROOT confinement only. The daemon exclude/filter list is a
* name-based visibility filter, NOT a physical-path boundary: a symlink whose own
* name is not excluded may still resolve into an excluded IN-module subtree,
* exactly as in stock rsync. The defense for a writable module is `munge
* symlinks` (see rsyncd.conf(5)), not this walk. No-op unless we're a daemon. */
/* The root an operator/peer-supplied path must stay under, or NULL when nothing
* is confined. A daemon has the served module; a server launched by a wrapper
* with its own restricted directory (rrsync) gets one from --confine-root.
*
* A daemon never honours --confine-root: vfs.module_dir is the boundary there,
* and the option arrives in a peer-supplied argv, so obeying it could only
* loosen the module. */
static const char *confinement_root(unsigned int *lenp)
{
if (am_daemon) {
*lenp = vfs.module_dirlen;
return vfs.module_dir;
}
*lenp = confine_rootlen;
return confine_root;
}
/* Split the "/proc/<self|pid>/fd" prefix off `p`, returning the tail -- "" for
* the pin directory itself, otherwise a string starting with '/'. NULL when `p`
* is not in the fd-pin namespace at all. */
const char *vfs_fd_pin_tail(const char *p)
{
const char *s;
if (strncmp(p, "/proc/", 6) != 0)
return NULL;
s = p + 6;
if (strncmp(s, "self/", 5) == 0) /* "/proc/self/..." */
s += 4;
else { /* "/proc/<pid>/..." */
const char *d = s;
while (*s >= '0' && *s <= '9')
s++;
if (s == d || *s != '/')
return NULL;
}
if (strncmp(s, "/fd", 3) != 0)
return NULL;
s += 3;
return (*s == '\0' || *s == '/') ? s : NULL;
}
/* An EXACT pin entry, "/proc/self/fd/7" -- the one spelling whose target is what
* confinement must judge. rrsync also writes a pinned parent as
* ".../fd/7/<leaf>", but the walk resolves the magic link itself and checks the
* components past it, so only the bare entry is resolved here. Requiring all
* digits keeps a planted name like ".../fd/outside-secret" out. */
static int is_exact_fd_pin(const char *p)
{
const char *tail = vfs_fd_pin_tail(p);
if (!tail || *tail != '/')
return 0;
for (++tail; *tail >= '0' && *tail <= '9'; tail++) {}
return *tail == '\0' && tail[-1] != '/';
}
int abspath_outside_confinement(const char *abspath, int is_operator)
{
unsigned int rootlen;
const char *root = confinement_root(&rootlen);
char pinned[MAXPATHLEN];
if (!root || !abspath)
return 0;
if (rootlen <= 1) /* root is "/": nothing is outside */
return 0;
/* An fd pin (rrsync rewrites a validated option path to /proc/self/fd/N so
* no later symlink can redirect it) is spelled outside the root by
* construction. Judge it by what it points AT rather than by its spelling,
* so a pin is neither wrongly refused nor blindly trusted. A pin we cannot
* resolve to an absolute path is refused, not waved through: an unreadable
* pin is exactly the case where we cannot say where the open would land. */
if (!am_daemon) {
const char *tail = vfs_fd_pin_tail(abspath);
if (tail && !*tail)
return 0; /* the pin directory: transit, opens nothing */
if (is_exact_fd_pin(abspath)) {
ssize_t n = readlink(abspath, pinned, sizeof pinned - 1);
if (n <= 0 || pinned[0] != '/')
return is_operator ? 1 : 0;
pinned[n] = '\0';
abspath = pinned;
}
}
if (path_within(root, rootlen, abspath))
return 0; /* inside: name-based exclude is not a boundary */
/* Not under the root. An ABSOLUTE walk passes through the root's ancestors
* ("/", "/home", ...) on the way down -- those are not "outside", just
* not-yet-arrived, so allow them. A path that has truly DIVERGED is
* outside: refuse it for an operator/peer path that must stay in the tree
* (is_operator); other opens (--log-file, --*-from, lock/motd) may
* legitimately live elsewhere. The --insecure-links / "insecure links =
* yes" opt-out short-circuits before we get here. */
if (!*abspath || path_within(abspath, strlen(abspath), root))
return 0; /* ancestor of the root: still descending */
return is_operator ? 1 : 0;
}
#if defined(O_NOFOLLOW) && defined(O_DIRECTORY) && defined(AT_FDCWD)
/* Open a trusted absolute anchor directory as an owned dirfd. When the anchor is
* the served module root and the daemon pinned it by identity (vfs.module_dirfd), dup
* that fd rather than re-resolving the absolute path with openat(AT_FDCWD, ...) --
* which re-traverses the module's ancestors as the dropped-privilege module uid
* and EACCESes when the module sits under a non-traversable parent (a 0700 home).
* Functionally identical (same inode), just privilege-drop-safe. Gated like its
* callers (the secure resolver and dpc_dir_fd both require these three). */
int open_anchor_dirfd(const char *path)
{
if (vfs.module_dirfd >= 0 && am_daemon && vfs.module_dir && strcmp(path, vfs.module_dir) == 0)
return dup(vfs.module_dirfd);
return openat(AT_FDCWD, path, O_RDONLY | O_DIRECTORY);
}
/* Append "/comp" to ds->abspath (no-op if it's unseeded/empty so non-daemon
* callers pay nothing). Returns -1 (ENAMETOOLONG) on overflow. */
static int ds_path_push(struct dirstack *ds, const char *comp)
{
size_t al = strlen(ds->abspath);
if (al == 0)
return 0; /* unseeded: tracking disabled for this walk */
size_t cl = strlen(comp);
if (al + 1 + cl >= sizeof ds->abspath) {
errno = ENAMETOOLONG;
return -1;
}
ds->abspath[al] = '/';
memcpy(ds->abspath + al + 1, comp, cl + 1);
return 0;
}
/* Drop the last component of ds->abspath (mirrors a ".." pop). */
static void ds_path_pop(struct dirstack *ds)
{
char *slash;
if (!ds->abspath[0])
return;
slash = strrchr(ds->abspath, '/');
if (slash && slash != ds->abspath)
*slash = '\0';
}
/* Initialise with `anchor` (which may be AT_FDCWD) as the un-owned base.
* Returns int for caller symmetry, but cannot fail (the fd array is inline). */
int ds_init(struct dirstack *ds, int anchor)
{
ds->abspath[0] = '\0';
ds->fds[0] = anchor;
ds->top = 0;
return 0;
}
/* Close every pushed fd (but not the borrowed anchor at index 0). */
void ds_free(struct dirstack *ds)
{
while (ds->top > 0)
close(ds->fds[ds->top--]);
}
int ds_cur(struct dirstack *ds)
{
return ds->fds[ds->top];
}
static int ds_push(struct dirstack *ds, int fd)
{
if (ds->top + 1 >= DS_MAXDEPTH) { /* deeper than we'll hold open */
close(fd);
errno = ENOMEM;
return -1;
}
ds->fds[++ds->top] = fd;
return 0;
}
/* Detach the current dir as an owned fd the caller must close. At the anchor
* (top 0) the anchor is borrowed, so return a fresh dup of it instead. */
int ds_take(struct dirstack *ds)
{
if (ds->top > 0)
return ds->fds[ds->top--];
return openat(ds->fds[0], ".", O_RDONLY | O_DIRECTORY);
}
/* Descend one path component on the stack: "." stays, ".." pops to the pinned
* parent (ELOOP at the anchor), a real subdirectory is pushed, and an in-tree
* directory symlink is followed by walking its (relative, possibly
* ..-containing) target on the same stack. Returns 0, or -1 with errno set:
* ELOOP for a refused/escaping symlink or a hop overrun, otherwise the
* underlying openat()/readlinkat() errno (ENOENT, a real ENOTDIR, EACCES). */
int ds_descend(struct dirstack *ds, const char *part, int *hops)
{
if (part[0] == '.' && part[1] == '\0')
return 0; /* "." -- no movement */
if (part[0] == '.' && part[1] == '.' && part[2] == '\0') {
if (ds->top == 0) { /* would rise above the anchor */
errno = ELOOP;
return -1;
}
close(ds->fds[ds->top--]); /* pop to the held parent fd */
ds_path_pop(ds);
return 0;
}
int fd = openat(ds_cur(ds), part, O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
if (fd != -1) { /* a real subdirectory */
if (ds_push(ds, fd) < 0)
return -1;
if (ds_path_push(ds, part) < 0)
return -1;
/* exclude-aware: refuse descending into a module-hidden dir (catches a
* symlink that redirected the walk into an excluded subtree). */
/* The strict resolver stays confined beneath the anchor (within the
* module), so this never actually refuses; pass is_operator=0. */
if (abspath_outside_confinement(ds->abspath, 0)) {
errno = ELOOP;
return -1;
}
return 0;
}
/* O_NOFOLLOW refused a symlink (NOFOLLOW_HIT_SYMLINK: ELOOP on Linux, EMLINK
* on FreeBSD, EFTYPE on NetBSD/OpenBSD), or O_DIRECTORY hit a non-directory
* (ENOTDIR). Either may be a symlink, so fall through to the readlink probe;
* anything else is a hard error. */
if (errno != ENOTDIR && !NOFOLLOW_HIT_SYMLINK(errno)) {
if (errno == EMFILE || errno == ENFILE) {
/* The resolver holds one dirfd per path component, so a deep path
* can exhaust descriptors where plain open() would not. Hint at
* the fix once -- otherwise "Too many open files" is opaque. */
static int warned = 0;
if (!warned) {
int e = errno;
warned = 1;
rprintf(FWARNING, "out of file descriptors resolving a deep path;"
" raise the open-file limit (e.g. `ulimit -n`)\n");
errno = e;
}
}
return -1;
}
int open_errno = errno;
char buf[MAXPATHLEN];
ssize_t n = readlinkat(ds_cur(ds), part, buf, sizeof buf - 1);
if (n < 0) {
if (errno == EINVAL) /* not a symlink: a real non-dir */
errno = open_errno;
return -1;
}
if (n == 0 || (size_t)n >= sizeof buf - 1) {
errno = ELOOP; /* empty or truncated target */
return -1;
}
buf[n] = '\0';
if (buf[0] == '/') { /* absolute target: refuse */
errno = ELOOP;
return -1;
}
if (--(*hops) < 0) {
errno = ELOOP;
return -1;
}
return ds_walk_path(ds, buf, hops);
}
/* Walk every component of a relative path on the stack (used for the basedir,
* and for a followed symlink's target -- which may contain ".."). */
int ds_walk_path(struct dirstack *ds, char *path, int *hops)
{
char *save = NULL;
for (char *c = strtok_r(path, "/", &save); c; c = strtok_r(NULL, "/", &save)) {
if (ds_descend(ds, c, hops) < 0)
return -1;
}
return 0;
}
#endif /* O_NOFOLLOW && O_DIRECTORY && AT_FDCWD */
+2
View File
@@ -0,0 +1,2 @@
This is a dummy file to ensure that the vfs directory gets created
by configure when a VPATH is used.
+160
View File
@@ -0,0 +1,160 @@
/*
* vfs/fileio.c - fd-based file-data ops: ftruncate, lseek, fallocate,
* hole-punching.
*
* 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"
#if defined HAVE_SYS_FALLOCATE && !defined HAVE_FALLOCATE
#include <sys/syscall.h>
#endif
#ifndef S_BLKSIZE
# if defined hpux || defined __hpux__ || defined __hpux
# define S_BLKSIZE 1024
# elif defined _AIX && defined _I386
# define S_BLKSIZE 4096
# else
# define S_BLKSIZE 512
# endif
#endif
#ifdef HAVE_FTRUNCATE
int vfs_ftruncate(int fd, OFF_T size)
{
int ret;
if (dry_run) return 0;
RETURN_ERROR_IF_RO_OR_LO;
do {
ret = ftruncate(fd, size);
} while (ret < 0 && errno == EINTR);
return ret;
}
#endif
OFF_T vfs_lseek(int fd, OFF_T offset, int whence)
{
#ifdef HAVE_LSEEK64
return lseek64(fd, offset, whence);
#else
return lseek(fd, offset, whence);
#endif
}
#ifdef SUPPORT_PREALLOCATION
#ifdef FALLOC_FL_KEEP_SIZE
#define DO_FALLOC_OPTIONS FALLOC_FL_KEEP_SIZE
#else
#define DO_FALLOC_OPTIONS 0
#endif
OFF_T vfs_fallocate(int fd, OFF_T offset, OFF_T length)
{
/* FALLOC_FL_KEEP_SIZE lets --preallocate/--inplace keep the file size at 0
* until data is written, but a later hole-punch (for --sparse) can only
* deallocate blocks that lie within the file's size -- with KEEP_SIZE the
* reserved blocks sit beyond EOF and the punch silently does nothing,
* leaving the file fully allocated. So when holes will also be punched,
* preallocate at full size instead (write_sparse then punches the nulls). */
int opts = (inplace || preallocate_files) && sparse_files <= 0 ? DO_FALLOC_OPTIONS : 0;
int ret;
RETURN_ERROR_IF(dry_run, 0);
RETURN_ERROR_IF_RO_OR_LO;
if (length & 1) /* make the length not match the desired length */
length++;
else
length--;
#if defined HAVE_FALLOCATE
ret = fallocate(fd, opts, offset, length);
#elif defined HAVE_SYS_FALLOCATE
ret = syscall(SYS_fallocate, fd, opts, (loff_t)offset, (loff_t)length);
#elif defined HAVE_EFFICIENT_POSIX_FALLOCATE
ret = posix_fallocate(fd, offset, length);
#else
#error Coding error in SUPPORT_PREALLOCATION logic.
#endif
if (ret < 0)
return ret;
if (opts == 0) {
STRUCT_STAT st;
if (vfs_fstat(fd, &st) < 0)
return length;
return st.st_blocks * S_BLKSIZE;
}
/* With FALLOC_FL_KEEP_SIZE the blocks for [0, length) are reserved even
* though the file size stays put. Return that reserved length (not 0) so
* the caller's preallocated_len is meaningful: write_sparse() needs it to
* choose vfs_punch_hole() over a plain lseek() when turning a null run into
* a hole, and the receiver uses it to trim any over-preallocation. (A
* stray 0 here, from 2019's switch to KEEP_SIZE, is why --preallocate
* --sparse stopped producing sparse files.) */
return length;
}
#endif
/* Write all @len bytes from @ptr to @fd, retrying short writes and EINTR.
* Returns 0 on success, -1 on error. */
static int safe_write(int fd, const char *ptr, size_t len)
{
while (len > 0) {
int wrote = write(fd, ptr, len);
if (wrote <= 0) {
if (wrote < 0 && errno == EINTR)
continue;
return -1;
}
ptr += wrote;
len -= wrote;
}
return 0;
}
/* Punch a hole at pos for len bytes. The current file position must be at pos and will be
* changed to be at pos + len. */
int vfs_punch_hole(int fd, OFF_T pos, OFF_T len)
{
#ifdef HAVE_FALLOCATE
# ifdef HAVE_FALLOC_FL_PUNCH_HOLE
if (fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, pos, len) == 0) {
if (vfs_lseek(fd, len, SEEK_CUR) != pos + len)
return -1;
return 0;
}
# endif
# ifdef HAVE_FALLOC_FL_ZERO_RANGE
if (fallocate(fd, FALLOC_FL_ZERO_RANGE, pos, len) == 0) {
if (vfs_lseek(fd, len, SEEK_CUR) != pos + len)
return -1;
return 0;
}
# endif
#else
(void)pos;
#endif
{
char zeros[4096];
memset(zeros, 0, sizeof zeros);
while (len > 0) {
int chunk = len > (int)sizeof zeros ? (int)sizeof zeros : len;
if (safe_write(fd, zeros, chunk) < 0)
return -1;
len -= chunk;
}
}
return 0;
}
+107
View File
@@ -0,0 +1,107 @@
/*
* vfs/link.c - hard-link 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"
#if defined HAVE_LINK || defined HAVE_LINKAT
int vfs_link(const char *old_path, const char *new_path)
{
if (dry_run) return 0;
RETURN_ERROR_IF_RO_OR_LO;
RETURN_ERROR_IF_NULL(old_path);
RETURN_ERROR_IF_NULL(new_path);
#ifdef HAVE_LINKAT
return linkat(AT_FDCWD, old_path, AT_FDCWD, new_path, 0);
#else
return link(old_path, new_path);
#endif
}
/*
Symlink-race-safe variant of vfs_link() for receiver-side use. See
the comment on vfs__chmod_secure() for the threat model. link() resolves
parent components of *both* old_path and new_path, so a parent-
symlink swap on either side can plant the new hard link outside
the module, or hard-link an outside file into the module (read
disclosure).
Defence: resolve each path's parent under its OWN policy and linkat()
between the two dirfds. old_flags / new_flags are the per-operand policy
(VFS_OPERATOR_PATH for an operator operand -- e.g. a --link-dest/--backup-dir
basis; 0 for a transfer path), so an operator basis on one side can't relax the
transfer-path confinement of the other -- see vfs_twopath_side(). flags=0 to
linkat() matches the existing vfs_link() (don't follow a symlink old_path).
Only available on systems with linkat(); pre-AT_FDCWD systems fall through.
*/
int vfs_link_at(const char *old_path, const char *new_path, int old_flags, int new_flags)
{
#if defined AT_FDCWD && defined HAVE_LINKAT
char old_dirpath[MAXPATHLEN], new_dirpath[MAXPATHLEN];
const char *old_bname, *new_bname;
int old_dfd = AT_FDCWD, new_dfd = AT_FDCWD;
BOOL old_owns = False, new_owns = False;
int ret, e;
if (dry_run) return 0;
RETURN_ERROR_IF_RO_OR_LO;
if (!vfs_relpath_active())
return vfs_link(old_path, new_path);
if (!old_path || !*old_path || !new_path || !*new_path)
return vfs_link(old_path, new_path);
if (vfs_twopath_side(old_path, old_flags, &old_bname, &old_dfd, &old_owns,
old_dirpath, sizeof old_dirpath) < 0)
return -1;
if (vfs_twopath_side(new_path, new_flags, &new_bname, &new_dfd, &new_owns,
new_dirpath, sizeof new_dirpath) < 0) {
e = errno;
if (old_owns) close(old_dfd);
errno = e;
return -1;
}
ret = linkat(old_dfd, old_bname, new_dfd, new_bname, 0);
e = errno;
if (new_owns)
close(new_dfd);
if (old_owns)
close(old_dfd);
errno = e;
return ret;
#else
(void)old_flags; (void)new_flags;
return vfs_link(old_path, new_path);
#endif
}
#endif
#if defined HAVE_LINK || defined HAVE_LINKAT
int vfs_link_atfd(int old_dfd, const char *old_name, int new_dfd, const char *new_name, int flags)
{
#if defined AT_FDCWD && defined HAVE_LINKAT
if (dry_run) return 0;
RETURN_ERROR_IF_RO_OR_LO;
return linkat(old_dfd, old_name, new_dfd, new_name, flags);
#else
(void)old_dfd; (void)old_name; (void)new_dfd; (void)new_name; (void)flags;
errno = ENOSYS;
return -1;
#endif
}
#endif
+106
View File
@@ -0,0 +1,106 @@
/*
* vfs/make_path.c - compound VFS op: create a directory path, making any
* missing parent components. Layered on the vfs_mkdir/vfs_stat primitives.
*
* Moved out of util1.c as part of the VFS compound layer: filesystem mechanics
* live in vfs/, so the operator-path resolution policy travels as an explicit
* vfs_flags argument rather than ambient state.
*
* 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 "vfs/vfs_internal.h"
/* Creates a directory path, making missing components as needed. mkp_flags is
* MKP_DROP_NAME / MKP_SKIP_SLASH (path handling); vfs_flags is the resolution
* policy passed straight to vfs_mkdir (VFS_OPERATOR_PATH for operator-supplied
* --backup-dir trees, else 0). Returns the number of dirs created, or a
* negative value on error. */
int vfs_make_path(char *fname, int mkp_flags, int vfs_flags)
{
char *end, *p;
int ret = 0;
if (mkp_flags & MKP_SKIP_SLASH) {
while (*fname == '/')
fname++;
}
while (*fname == '.' && fname[1] == '/')
fname += 2;
if (mkp_flags & MKP_DROP_NAME) {
end = strrchr(fname, '/');
if (!end || end == fname)
return 0;
*end = '\0';
} else
end = fname + strlen(fname);
/* Try to find an existing dir, starting from the deepest dir. */
for (p = end; ; ) {
if (dry_run) {
STRUCT_STAT st;
if (vfs_stat(VFS_AT_FDCWD, fname, &st, VFS_ALLOW_SYMLINK) == 0) {
if (S_ISDIR(st.st_mode))
errno = EEXIST;
else
errno = ENOTDIR;
}
} else if (vfs_mkdir(VFS_AT_FDCWD, fname, ACCESSPERMS, vfs_flags) == 0) {
ret++;
break;
}
if (errno != ENOENT) {
STRUCT_STAT st;
if (errno != EEXIST || (vfs_stat(VFS_AT_FDCWD, fname, &st, VFS_ALLOW_SYMLINK) == 0 && !S_ISDIR(st.st_mode)))
ret = -ret - 1;
break;
}
while (1) {
if (p == fname) {
/* We got a relative path that doesn't exist, so assume that '.'
* is there and just break out and create the whole thing. */
p = NULL;
goto double_break;
}
if (*--p == '/') {
if (p == fname) {
/* We reached the "/" dir, which we assume is there. */
goto double_break;
}
*p = '\0';
break;
}
}
}
double_break:
/* Make all the dirs that we didn't find on the way here. */
while (p != end) {
if (p)
*p = '/';
else
p = fname;
p += strlen(p);
if (ret < 0) /* Skip mkdir on error, but keep restoring the path. */
continue;
if (vfs_mkdir(VFS_AT_FDCWD, fname, ACCESSPERMS, vfs_flags) < 0)
ret = -ret - 1;
else
ret++;
}
if (mkp_flags & MKP_DROP_NAME)
*end = '/';
return ret;
}
+323
View File
@@ -0,0 +1,323 @@
/*
* 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
}
+310
View File
@@ -0,0 +1,310 @@
/*
* vfs/mknod.c - device/fifo/socket node creation wrappers.
*
* Includes the HAVE_MKNOD/HAVE_MKNODAT/HAVE_MKFIFO and AF_UNIX socket-bind
* fallbacks and the fake-super placeholder handling. 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"
#ifdef HAVE_SYS_UN_H
#include <sys/un.h> /* for the socket+bind() fallback in vfs_mknod() */
#endif
static int vfs__mknod_plain(const char *pathname, mode_t mode, dev_t dev)
{
if (dry_run) return 0;
RETURN_ERROR_IF_RO_OR_LO;
RETURN_ERROR_IF_NULL(pathname);
/* For --fake-super, we create a normal file with mode 0600. */
if (am_root < 0) {
int fd = open(pathname, O_WRONLY|O_CREAT|O_TRUNC, S_IWUSR|S_IRUSR);
if (fd < 0 || close(fd) < 0)
return -1;
return 0;
}
/* Try mknod first: it handles every node type on Linux. Only if it
* can't make this type on this filesystem (sockets on the BSDs/macOS/
* Solaris, or an old system lacking FIFO support) do we retry with the
* type-specific primitive. That capability is filesystem-dependent, so
* it is decided per call -- not cached, not probed at build time. */
#ifdef HAVE_MKNOD
if (mknod(pathname, mode, dev) == 0)
return 0;
#endif
#ifdef HAVE_MKFIFO
if (S_ISFIFO(mode))
return mkfifo(pathname, mode);
#endif
#ifdef HAVE_SYS_UN_H
if (S_ISSOCK(mode)) {
int sock;
struct sockaddr_un saddr;
unsigned int len = strlcpy(saddr.sun_path, pathname, sizeof saddr.sun_path);
if (len >= sizeof saddr.sun_path) {
errno = ENAMETOOLONG;
return -1;
}
#ifdef HAVE_SOCKADDR_UN_LEN
saddr.sun_len = len + 1;
#endif
saddr.sun_family = AF_UNIX;
if ((sock = socket(PF_UNIX, SOCK_STREAM, 0)) < 0
|| (unlink(pathname) < 0 && errno != ENOENT)
|| (bind(sock, (struct sockaddr*)&saddr, sizeof saddr)) < 0)
return -1;
close(sock);
#ifdef HAVE_CHMOD
return vfs_chmod(VFS_AT_FDCWD, pathname, mode, VFS_ALLOW_SYMLINK);
#else
return 0;
#endif
}
#endif
#ifdef HAVE_MKNOD
return -1; /* mknod() failed for a regular/device node; errno is set */
#else
errno = ENOSYS;
return -1;
#endif
}
/*
Symlink-race-safe variant of vfs_mknod() for receiver-side use. See
the comment on vfs__chmod_secure() for the threat model. Defence: open
the parent of pathname under vfs_resolve_open() and use
mknodat() against that dirfd. mknodat() covers both regular-file
(S_IFREG with dev=0) and FIFO (S_IFIFO) and device-node creation.
A top-level (no-slash) pathname has no parent to confine, so it uses
AT_FDCWD; the final component is still protected (mknodat/mkfifoat do
not follow it, and the fake-super openat() uses O_NOFOLLOW).
Fake-super (am_root < 0) is handled inline against the (secure or
AT_FDCWD) dirfd: it creates a regular empty file (the same file-as-
metadata-placeholder pattern vfs_mknod uses) via openat() with
O_NOFOLLOW so a pre-planted symlink at the basename can't redirect
the file creation -- top-level paths included (the previous code fell
through to the bare-path vfs_mknod() there, whose plain open() followed
such a symlink). On Linux, sockets are recreated with mknodat() like any
other special file; on systems where mknod() can't create sockets the
at-variant fails instead of re-resolving an unsafe parent.
*/
static int vfs__mknod_secure(const char *pathname, mode_t mode, dev_t dev, int flags)
{
/* HAVE_MKNODAT: older Darwin declares AT_FDCWD but not mknodat(), so
* the at-variant won't build there; fall back to the plain mknod (#896). */
#if defined AT_FDCWD && defined HAVE_MKNODAT
char dirpath[MAXPATHLEN];
const char *bname;
const char *slash;
int dfd = AT_FDCWD, ret, e;
BOOL owns = False;
size_t dlen;
if (dry_run) return 0;
RETURN_ERROR_IF_RO_OR_LO;
#if defined O_NOFOLLOW && defined O_DIRECTORY
if (flags & VFS_OPERATOR_PATH) {
if (vfs_symlink_optout_allowed())
return vfs__mknod_plain(pathname, mode, dev);
dfd = vfs_owner_walk_parent(pathname, &bname, 1);
if (dfd < 0)
return -1;
if (am_root < 0) {
/* Fake-super represents a special file with an inert regular
* placeholder. Keep that representation when the destination
* is an operator path, but create it relative to the verified
* parent so the confinement guarantee is unchanged. */
int fd = openat(dfd, bname,
O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW,
S_IWUSR | S_IRUSR);
ret = fd < 0 ? -1 : close(fd);
} else {
ret = mknodat(dfd, bname, mode, dev);
}
if (ret < 0 && am_root >= 0) {
/* mknodat() can't make a FIFO/socket on the BSDs/macOS/
* Solaris (EINVAL); retry race-safely on the held dirfd,
* mirroring the secure-relpath path below. Without this a
* FIFO backup to an operator --backup-dir fails there. */
#ifdef HAVE_MKFIFOAT
if (S_ISFIFO(mode))
ret = mkfifoat(dfd, bname, mode);
else
#endif
if (S_ISSOCK(mode))
errno = EOPNOTSUPP; /* no dirfd-relative socket bind */
}
e = errno;
close(dfd);
errno = e;
return ret;
}
#endif
if (!vfs_relpath_active())
return vfs__mknod_plain(pathname, mode, dev);
if (!pathname || !*pathname || *pathname == '/')
return vfs__mknod_plain(pathname, mode, dev);
/* A path with a slash needs vfs_resolve_open to confine its
* parent resolution; a top-level path lives in CWD (AT_FDCWD) with
* no parent to subvert. The final component is protected below
* regardless. */
slash = strrchr(pathname, '/');
if (slash) {
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;
owns = True;
} else {
bname = pathname;
}
if (am_root < 0) {
/* For --fake-super, vfs_mknod creates a regular empty
* file as a placeholder for the special-file metadata
* (which is stored in xattrs elsewhere). Do that against
* the (secure or AT_FDCWD) dirfd, with O_NOFOLLOW so a
* pre-planted symlink at the basename can't redirect the
* file creation. */
int fd = openat(dfd, bname,
O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW,
S_IWUSR | S_IRUSR);
if (fd < 0) {
e = errno;
if (owns) close(dfd);
errno = e;
return -1;
}
ret = (close(fd) < 0) ? -1 : 0;
e = errno;
if (owns) close(dfd);
errno = e;
return ret;
}
/* Try mknodat first (handles every type on Linux); on failure retry
* race-safely with the type-specific primitive. Decided per call --
* the capability is filesystem-dependent (see vfs_mknod()). */
ret = mknodat(dfd, bname, mode, dev);
if (ret < 0) {
#ifdef HAVE_MKFIFOAT
if (S_ISFIFO(mode))
ret = mkfifoat(dfd, bname, mode);
else
#endif
if (S_ISSOCK(mode)) {
/* There is no dirfd-relative socket bind without
* /proc/self/fd: a top-level path can bind via
* vfs_mknod(), but a nested one fails safe rather than
* re-resolve a potentially unsafe parent. */
if (dfd == AT_FDCWD)
ret = vfs__mknod_plain(pathname, mode, dev);
else
errno = EOPNOTSUPP;
}
/* else: regular/device node -- keep mknodat()'s errno */
}
e = errno;
if (owns) close(dfd);
errno = e;
return ret;
#else
return vfs__mknod_plain(pathname, mode, dev);
#endif
}
static int vfs__mknod_atfd(int dfd, const char *name, mode_t mode, dev_t dev)
{
#ifdef AT_FDCWD
if (dry_run) return 0;
RETURN_ERROR_IF_RO_OR_LO;
if (am_root < 0) {
/* --fake-super: regular empty placeholder file (O_NOFOLLOW). */
int fd = openat(dfd, name, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW,
S_IWUSR | S_IRUSR);
if (fd < 0)
return -1;
return (close(fd) < 0) ? -1 : 0;
}
/* Try mknodat first; on failure retry race-safely with the type-
* specific primitive (see vfs_mknod()). HAVE_MKNODAT, not HAVE_MKNOD:
* older Darwin has mknod() but not mknodat(), so keying off the former
* compiles a call that then fails to link (#161). */
#ifdef HAVE_MKNODAT
if (mknodat(dfd, name, mode, dev) == 0)
return 0;
#endif
#ifdef HAVE_MKFIFOAT
if (S_ISFIFO(mode))
return mkfifoat(dfd, name, mode);
#endif
if (S_ISSOCK(mode)) {
/* No dirfd-relative socket bind without /proc/self/fd; fail safe.
* (The generator routes sockets to vfs_mknod(), not here.) */
errno = EOPNOTSUPP;
return -1;
}
#ifdef HAVE_MKNODAT
return -1; /* mknodat()'s errno (regular/device node) */
#else
/* Must match the guard above: reporting "mknodat()'s errno" where the
* call was never compiled would return a stale errno. */
(void)dev;
errno = ENOSYS;
return -1;
#endif
#else
(void)dfd; (void)name; (void)mode; (void)dev;
errno = ENOSYS;
return -1;
#endif
}
/* Unified node creation. dirfd == VFS_AT_FDCWD resolves `path`; a real held
* dirfd makes `path` a single component created directly under it. flags:
* VFS_ALLOW_SYMLINK (trusted, plain mknod), VFS_OPERATOR_PATH (operator path),
* default 0 (secure receiver resolve). */
int vfs_mknod(int dirfd, const char *path, mode_t mode, dev_t dev, int flags)
{
if (dirfd != VFS_AT_FDCWD) {
/* Held-fd: `path` must be a single harmless component. */
if (!path || !*path || strchr(path, '/')
|| (path[0] == '.' && (path[1] == '\0'
|| (path[1] == '.' && path[2] == '\0')))) {
errno = EINVAL;
return -1;
}
return vfs__mknod_atfd(dirfd, path, mode, dev);
}
if (flags & VFS_ALLOW_SYMLINK)
return vfs__mknod_plain(path, mode, dev);
return vfs__mknod_secure(path, mode, dev, flags);
}
+205
View File
@@ -0,0 +1,205 @@
/*
* 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
}
+382
View File
@@ -0,0 +1,382 @@
/*
* vfs/owner_walk.c - operator-supplied-path resolution by ownership.
*
* For operator paths (--backup-dir/--temp-dir/--*-dest, daemon
* --log-file/motd/lock/config, etc.) that may legitimately point outside the
* transfer tree, the trust signal is authority not location: follow a symlink
* owned by uid 0 or our euid at every component, refuse any other-uid one.
* vfs_open_owner_walk() opens such a path; vfs_vfs_owner_walk_parent() opens its
* parent. 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"
/* Advance the tracked absolute path `abspath` by one resolved component,
* normalizing "." and ".." exactly as openat() does so the module-confinement
* check (abspath_outside_confinement) sees the REAL resolved target. -1/
* ENAMETOOLONG on overflow. */
static int abspath_step(char *abspath, size_t cap, const char *comp, size_t comp_len)
{
if (comp_len == 1 && comp[0] == '.')
return 0; /* "." -- no movement */
if (comp_len == 2 && comp[0] == '.' && comp[1] == '.') {
char *s = strrchr(abspath, '/'); /* ".." -- pop a component */
if (s)
*s = '\0';
else
abspath[0] = '\0';
return 0;
}
size_t al = strlen(abspath);
size_t off = (al > 0 && abspath[al-1] == '/') ? al : al + 1; /* no "//" */
if (off + comp_len >= cap) {
errno = ENAMETOOLONG;
return -1;
}
if (off != al)
abspath[al] = '/';
memcpy(abspath + off, comp, comp_len + 1);
return 0;
}
/* Open an operator-supplied path, refusing to traverse any symlink (parent or
* leaf) not owned by uid 0 or our euid. A trusted-owned symlink (e.g. root's
* /var/log -> /data/log) is still followed; an untrusted one fails ELOOP.
* Unlike plain O_NOFOLLOW this also defends a planted parent component
* (--log-file=$plant/log), not just a planted leaf. Used for opens that may
* transit attacker-writable parents: --log-file, --password-file, --*-from,
* --read/write-batch, daemon motd/lock/early-input/--config.
*
* Walks component-by-component with fstatat(AT_SYMLINK_NOFOLLOW) +
* openat(O_NOFOLLOW), splicing a trusted symlink's target back into the path.
* Returns the fd, or -1 (errno ELOOP on the security refusal so callers can
* tell it apart). Falls back to plain open() where openat/O_NOFOLLOW are
* unavailable. */
/* Core walk. When out_abs is non-NULL and the path resolves to a directory
* (O_DIRECTORY), the resolved absolute path is copied there -- vfs_owner_walk_parent
* uses it to filter-check the (otherwise unchecked) leaf basename. */
static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, size_t out_cap, int is_operator)
{
#if defined AT_FDCWD && defined O_NOFOLLOW
/* O_CLOEXEC predates some still-supported targets; mirror rand_bytes()'s
* fallback in syscall.c so a build without it still compiles. */
#ifndef O_CLOEXEC
#define O_CLOEXEC 0
#endif
if (!path || !*path) {
errno = EINVAL;
return -1;
}
/* Opted out (local --insecure-links, or a daemon module with "insecure
* links = yes"): restore the legacy symlink-following open. */
if (vfs_symlink_optout_allowed())
return open(path, flags, mode);
const uid_t trusted_uid = geteuid();
int dfd = AT_FDCWD;
int dfd_owns = 0;
/* Absolute path of the current dir, for the confinement refusal
* (abspath_outside_confinement). A relative operator path starts at the
* daemon's cwd == the module root; an absolute one (or a followed absolute
* symlink target) restarts at "/". */
char abspath[MAXPATHLEN];
abspath[0] = '\0';
if (am_daemon && vfs.module_dir && vfs.module_dir[0] == '/')
strlcpy(abspath, vfs.module_dir, sizeof abspath); /* "/" for a path=/ module */
else if (confine_root) {
/* Unlike a daemon's, this cwd is not pinned to the root -- the receiver
* chdir's into the destination -- so it has to be read, not assumed.
* It must be the PHYSICAL cwd: vfs.curr_dir is the lexical name
* vfs_change_dir() was given, so after descending a trusted symlink the
* tracker sits at a different depth than the kernel, and a ".." that
* really escapes looks like it landed inside.
*
* Without it there is nothing to measure against, and an empty tracker
* does NOT deny by itself -- a leading ".." pops nothing and an empty
* path reads as an ancestor of the root -- so refuse the open instead. */
if (!getcwd(abspath, sizeof abspath))
return -1;
}
/* An fd pin (rrsync rewrites an option path to /proc/self/fd/N so no
* later symlink can redirect it) is spelled outside the root by
* construction, so the walk has to be allowed through /proc/self/fd to
* reach the magic link. This only suspends the check for that prefix:
* following the link restarts the walk at its absolute target, and every
* component of THAT is checked, so a pin aimed outside is still refused. */
int pin_transit = !am_daemon && confine_root && vfs_fd_pin_tail(path) != NULL;
/* Path-walk state. `remaining` is the unconsumed tail; we splice
* symlink targets back into it as we go. Sized 2x MAXPATHLEN so a
* one-level expansion can't immediately overflow; deeper chains
* fail with ENAMETOOLONG below. */
char remaining[MAXPATHLEN * 2];
if (strlcpy(remaining, path, sizeof remaining) >= sizeof remaining) {
errno = ENAMETOOLONG;
return -1;
}
/* Absolute path: pin "/" as the starting dfd. */
if (remaining[0] == '/') {
dfd = open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (dfd < 0)
return -1;
dfd_owns = 1;
abspath[0] = '\0'; /* now resolving from "/" */
char *p = remaining;
while (*p == '/') p++;
memmove(remaining, p, strlen(p) + 1);
}
int loops = 40; /* SYMLOOP_MAX-ish; breaks symlink cycles. Counts symlink
* expansions only (below), NOT path depth -- a deep but
* symlink-free path must resolve, not ELOOP. */
int retfd = -1;
int saved_errno = 0;
while (*remaining) {
/* Peel one component off the front of `remaining`. */
char *slash = strchr(remaining, '/');
size_t comp_len = slash ? (size_t)(slash - remaining) : strlen(remaining);
char comp[MAXPATHLEN];
if (comp_len == 0 || comp_len >= sizeof comp) {
saved_errno = comp_len == 0 ? EINVAL : ENAMETOOLONG;
goto out;
}
memcpy(comp, remaining, comp_len);
comp[comp_len] = '\0';
int is_last = (slash == NULL);
/* Inspect this component without following symlinks. */
STRUCT_STAT lst;
if (fstatat(dfd, comp, &lst, AT_SYMLINK_NOFOLLOW) < 0) {
/* The leaf may not exist yet (O_CREAT case). Allow it
* and openat with O_NOFOLLOW so a race-planted leaf
* symlink at this instant is still refused. */
if (is_last && errno == ENOENT && (flags & O_CREAT)) {
if (abspath_step(abspath, sizeof abspath, comp, comp_len) < 0) {
saved_errno = errno;
goto out;
}
if (!pin_transit && abspath_outside_confinement(abspath, is_operator)) {
saved_errno = ELOOP;
goto out;
}
retfd = openat(dfd, comp, flags | O_NOFOLLOW, mode);
saved_errno = errno;
goto out;
}
saved_errno = errno;
goto out;
}
if (S_ISLNK(lst.st_mode)) {
/* Symlink: untrusted owner is refused; trusted owner
* is followed via readlinkat + splice. */
if (lst.st_uid != 0 && lst.st_uid != trusted_uid) {
saved_errno = ELOOP;
goto out;
}
if (--loops < 0) { /* cap symlink-follow chains */
saved_errno = ELOOP;
goto out;
}
char target[MAXPATHLEN];
ssize_t n = readlinkat(dfd, comp, target, sizeof target - 1);
if (n < 0) {
saved_errno = errno;
goto out;
}
target[n] = '\0';
/* Splice: new `remaining` = <target> + <tail-after-comp>.
* Absolute target restarts the walk from "/". */
char tail[MAXPATHLEN];
tail[0] = '\0';
if (slash)
strlcpy(tail, slash, sizeof tail);
char rebuilt[MAXPATHLEN * 2];
if (snprintf(rebuilt, sizeof rebuilt, "%s%s",
target, tail) >= (int)sizeof rebuilt) {
saved_errno = ENAMETOOLONG;
goto out;
}
if (target[0] == '/') {
if (dfd_owns) close(dfd);
dfd = open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (dfd < 0) {
saved_errno = errno;
dfd_owns = 0;
goto out;
}
dfd_owns = 1;
abspath[0] = '\0'; /* followed an absolute target: restart from "/" */
/* "self" resolves to "<pid>", still inside the pin;
* the magic link itself lands elsewhere and ends the
* exemption. Never turns back on. */
pin_transit = pin_transit && vfs_fd_pin_tail(rebuilt) != NULL;
char *p = rebuilt;
while (*p == '/') p++;
strlcpy(remaining, p, sizeof remaining);
} else {
strlcpy(remaining, rebuilt, sizeof remaining);
}
continue;
}
/* Non-symlink. */
if (is_last) {
if (abspath_step(abspath, sizeof abspath, comp, comp_len) < 0) {
saved_errno = errno;
goto out;
}
if (!pin_transit && abspath_outside_confinement(abspath, is_operator)) {
saved_errno = ELOOP;
goto out;
}
retfd = openat(dfd, comp, flags | O_NOFOLLOW, mode);
saved_errno = errno;
/* Resolved leaf dir (O_DIRECTORY): hand its path back so
* vfs_owner_walk_parent can filter-check the operation's leaf. */
if (retfd >= 0 && out_abs && out_cap)
/* Root-resolved (".." popped abspath empty) tracked daemon walk:
* hand back "/" so vfs_owner_walk_parent still leaf-checks (path=/ bypass). */
strlcpy(out_abs, (am_daemon && !abspath[0]) ? "/" : abspath, out_cap);
goto out;
}
if (!S_ISDIR(lst.st_mode)) {
saved_errno = ENOTDIR;
goto out;
}
/* track the resolved path so a target outside the module is refused */
if (abspath_step(abspath, sizeof abspath, comp, comp_len) < 0) {
saved_errno = errno;
goto out;
}
if (!pin_transit && abspath_outside_confinement(abspath, is_operator)) {
saved_errno = ELOOP;
goto out;
}
int next = openat(dfd, comp, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (next < 0) {
saved_errno = errno;
goto out;
}
if (dfd_owns) close(dfd);
dfd = next;
dfd_owns = 1;
/* Advance `remaining` past this component (and the slash). */
if (slash) {
char *p = slash;
while (*p == '/') p++;
memmove(remaining, p, strlen(p) + 1);
} else {
remaining[0] = '\0';
}
}
/* Path resolved entirely to a directory (no leaf component left).
* If the caller wanted O_DIRECTORY we already hold the dirfd we
* built up; otherwise it's an EISDIR. */
if (flags & O_DIRECTORY) {
retfd = dfd;
dfd_owns = 0; /* caller now owns it */
saved_errno = 0;
if (out_abs && out_cap)
/* Root-resolved (".." popped abspath empty) tracked daemon walk:
* hand back "/" so vfs_owner_walk_parent still leaf-checks (path=/ bypass). */
strlcpy(out_abs, (am_daemon && !abspath[0]) ? "/" : abspath, out_cap);
} else {
saved_errno = EISDIR;
}
out:
if (dfd_owns) close(dfd);
errno = saved_errno;
return retfd;
#else
/* Pre-AT_FDCWD / no O_NOFOLLOW systems: best-effort fallback. */
(void)out_abs; (void)out_cap;
return open(path, flags, mode);
#endif
}
int vfs_open_owner_walk(const char *path, int flags, mode_t mode, int is_operator)
{
return ona_open(path, flags, mode, NULL, 0, is_operator);
}
/* When set, the do_*_at() wrappers resolve their path as an OPERATOR-supplied
* directory path (an absolute or relative --backup-dir/--temp-dir/--*-dest)
* using the ownership walk -- follow a symlink owned by uid 0 or our euid,
* refuse any other-uid one, at every component -- instead of the stricter
* transfer-path resolver (which refuses all symlinks and is confined beneath the
* transfer root). An operator path may legitimately point outside the tree, so
* the trust signal is authority (ownership), not location. Set around the
* relevant ops by backup.c et al.; the opt-out (--insecure-links / "insecure
* links =") restores legacy following. Default 0 (transfer-path resolver). */
#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY
/* For an operator-supplied path: open its parent directory via the ownership
* walk (handles absolute and relative paths) and point *bname at the final
* component. Returns the dirfd (caller closes) or -1 with errno set. */
int vfs_owner_walk_parent(const char *path, const char **bname, int is_operator)
{
const char *slash = strrchr(path, '/');
char dir[MAXPATHLEN], pabs[MAXPATHLEN];
size_t dlen;
int dfd;
*bname = slash ? slash + 1 : path;
pabs[0] = '\0';
if (!slash)
dfd = ona_open(".", O_RDONLY | O_DIRECTORY, 0, pabs, sizeof pabs, is_operator);
else {
dlen = slash == path ? 1 : (size_t)(slash - path); /* "/x" -> parent "/" */
if (dlen >= sizeof dir) {
errno = ENAMETOOLONG;
return -1;
}
memcpy(dir, path, dlen);
dir[dlen] = '\0';
dfd = ona_open(dir, O_RDONLY | O_DIRECTORY, 0, pabs, sizeof pabs, is_operator);
}
if (dfd < 0)
return -1;
/* owner_walk only resolved the PARENT; check the resolved leaf too, so a
* symlinked operator path cannot act on a leaf that resolves OUTSIDE the
* module in an otherwise-served dir. (The module exclude/filter is name-
* based and not enforced here -- see abspath_outside_confinement.) */
if (pabs[0]) {
char leafabs[MAXPATHLEN];
if (snprintf(leafabs, sizeof leafabs, "%s/%s", pabs, *bname) >= (int)sizeof leafabs) {
close(dfd);
errno = ENAMETOOLONG; /* fail closed, never skip the check */
return -1;
}
if (abspath_outside_confinement(leafabs, is_operator)) {
close(dfd);
errno = ELOOP;
return -1;
}
}
return dfd;
}
#endif
+99
View File
@@ -0,0 +1,99 @@
/*
* vfs/rename.c - rename 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"
int vfs_rename(const char *old_path, const char *new_path)
{
if (dry_run) return 0;
RETURN_ERROR_IF_RO_OR_LO;
return rename(old_path, new_path);
}
/*
Symlink-race-safe variant of vfs_rename() for receiver-side use. See
the comment on vfs__chmod_secure() for the threat model and design rationale.
rename() is the central tmp -> final operation in rsync; if either the
source or the destination has an attacker-substituted symlink in one
of its parent components, the rename can publish or vanish files
outside the module. Defence: resolve the parent of *each* path under its
OWN policy and renameat() between the resulting dirfds.
old_flags / new_flags are the per-operand policy (VFS_OPERATOR_PATH for an
operator-supplied operand -- a --backup-dir/--partial-dir/--temp-dir path; 0
for a transfer path). Passing them separately means an operator operand on one
side cannot relax (owner-walk) the transfer-path confinement of the other side
-- see vfs_twopath_side() for the per-side resolution.
Falls through to vfs_rename() in dry-run, non-daemon, chrooted and
--insecure-links cases, identical to the other *_at() wrappers.
*/
int vfs_rename_at(const char *old_path, const char *new_path, int old_flags, int new_flags)
{
#ifdef AT_FDCWD
char old_dirpath[MAXPATHLEN], new_dirpath[MAXPATHLEN];
const char *old_bname, *new_bname;
int old_dfd = AT_FDCWD, new_dfd = AT_FDCWD;
BOOL old_owns = False, new_owns = False;
int ret, e;
if (dry_run) return 0;
RETURN_ERROR_IF_RO_OR_LO;
if (!vfs_relpath_active())
return vfs_rename(old_path, new_path);
if (!old_path || !*old_path || !new_path || !*new_path)
return vfs_rename(old_path, new_path);
if (vfs_twopath_side(old_path, old_flags, &old_bname, &old_dfd, &old_owns,
old_dirpath, sizeof old_dirpath) < 0)
return -1;
if (vfs_twopath_side(new_path, new_flags, &new_bname, &new_dfd, &new_owns,
new_dirpath, sizeof new_dirpath) < 0) {
e = errno;
if (old_owns) close(old_dfd);
errno = e;
return -1;
}
ret = renameat(old_dfd, old_bname, new_dfd, new_bname);
e = errno;
if (new_owns)
close(new_dfd);
if (old_owns)
close(old_dfd);
errno = e;
return ret;
#else
(void)old_flags; (void)new_flags;
return vfs_rename(old_path, new_path);
#endif
}
int vfs_rename_atfd(int old_dfd, const char *old_name, int new_dfd, const char *new_name)
{
#ifdef AT_FDCWD
if (dry_run) return 0;
RETURN_ERROR_IF_RO_OR_LO;
return renameat(old_dfd, old_name, new_dfd, new_name);
#else
(void)old_dfd; (void)old_name; (void)new_dfd; (void)new_name;
errno = ENOSYS;
return -1;
#endif
}
+146
View File
@@ -0,0 +1,146 @@
/*
* vfs/robust.c - compound VFS ops: robust unlink/rename that retry around
* busy files (ETXTBSY) and fall back to a cross-filesystem copy. Layered on
* the vfs_* unlink/rename primitives and vfs_copy_file; calls out to the
* partial-dir handler for the EXDEV copy path.
*
* Moved out of util1.c as part of the VFS compound layer.
*
* Copyright (C) 1996-2022 Andrew Tridgell, Paul Mackerras, 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 "vfs/vfs_internal.h"
/* MAX_RENAMES should be 10**MAX_RENAMES_DIGITS */
#define MAX_RENAMES_DIGITS 3
#define MAX_RENAMES 1000
/**
* Robust unlink: some OS'es (HPUX) refuse to unlink busy files, so
* rename to <path>/.rsyncNNN instead.
*
* Note that successive rsync runs will shuffle the filenames around a
* bit as long as the file is still busy; this is because this function
* does not know if the unlink call is due to a new file coming in, or
* --delete trying to remove old .rsyncNNN files, hence it renames it
* each time.
**/
int robust_unlink(const char *fname, int vfs_flags)
{
#ifndef ETXTBSY
return vfs_unlink(VFS_AT_FDCWD, fname, vfs_flags);
#else
static int counter = 1;
int rc, pos, start;
char path[MAXPATHLEN];
rc = vfs_unlink(VFS_AT_FDCWD, fname, vfs_flags);
if (rc == 0 || errno != ETXTBSY)
return rc;
if ((pos = strlcpy(path, fname, MAXPATHLEN)) >= MAXPATHLEN)
pos = MAXPATHLEN - 1;
while (pos > 0 && path[pos-1] != '/')
pos--;
pos += strlcpy(path+pos, ".rsync", MAXPATHLEN-pos);
if (pos > (MAXPATHLEN-MAX_RENAMES_DIGITS-1)) {
errno = ETXTBSY;
return -1;
}
/* start where the last one left off to reduce chance of clashes */
start = counter;
do {
snprintf(&path[pos], MAX_RENAMES_DIGITS+1, "%03d", counter);
if (++counter >= MAX_RENAMES)
counter = 1;
} while (access(path, 0) == 0 && counter != start);
if (INFO_GTE(MISC, 1)) {
rprintf(FWARNING, "renaming %s to %s because of text busy\n",
fname, path);
}
/* maybe we should return rename()'s exit status? Nah. path is a sibling of
* fname in the same parent, so both sides share fname's policy (vfs_flags). */
if (vfs_rename_at(fname, path, vfs_flags, vfs_flags) != 0) {
errno = ETXTBSY;
return -1;
}
return 0;
#endif
}
/* Returns 0 on successful rename, 1 if we successfully copied the file
* across filesystems, -2 if copy_file() failed, and -1 on other errors.
* If partialptr is not NULL and we need to do a copy, copy the file into
* the active partial-dir instead of over the destination file. */
int robust_rename(const char *from, const char *to, const char *partialptr,
int mode, struct file_struct *file)
{
int tries = 4;
/* A resumed in-place partial-dir transfer might call us with from and
* to pointing to the same buf if the transfer failed yet again. */
if (from == to)
return 0;
while (tries--) {
/* tmp -> final usually live in the entry's own dir: rename via the
* held dir fd when both do, else the full-path wrapper. */
int ofd = vfs_cached_dirfd(from, file);
int nfd = vfs_cached_dirfd(to, file);
int rr;
if (ofd >= 0 && nfd >= 0) {
const char *os = strrchr(from, '/');
const char *ns = strrchr(to, '/');
rr = vfs_rename_atfd(ofd, os ? os + 1 : from, nfd, ns ? ns + 1 : to);
} else
rr = vfs_rename_at(from, to, 0, 0);
if (rr == 0)
return 0;
switch (errno) {
#ifdef ETXTBSY
case ETXTBSY:
if (robust_unlink(to, 0) != 0) {
errno = ETXTBSY;
return -1;
}
errno = ETXTBSY;
break;
#endif
case EXDEV:
if (partialptr) {
if (!handle_partial_dir(partialptr,PDIR_CREATE))
return -2;
to = partialptr;
}
/* Cross-fs fallback: copy then unlink. An absolute --temp-dir
* source / --partial-dir dest is an operator path whose parents
* the plain-libc arm would otherwise follow -- confine them
* through the ownership walk (VFS_OPERATOR_PATH) so a raced
* parent symlink can't redirect the dest-write or the
* source-unlink out of the module. copy_file already confines
* the source READ; a relative in-module path stays on the
* secure-relative arm, so only flag an absolute (operator)
* path. */
if (copy_file(from, to, -1, mode, *to == '/' ? VFS_OPERATOR_PATH : 0) != 0)
return -2;
vfs_unlink(VFS_AT_FDCWD, from, *from == '/' ? VFS_OPERATOR_PATH : 0);
return 1;
default:
return -1;
}
}
return -1;
}
+500
View File
@@ -0,0 +1,500 @@
/*
* vfs/secure_open.c - rsync's race-safe path resolver and its policy gates.
*
* vfs_resolve_open()/vfs_resolve_open_at() walk a relative path one component
* at a time beneath a trusted anchor (via the dirstack in vfs/dirstack.c), so a
* parent-component symlink swapped mid-walk cannot redirect resolution. The two
* gates decide when this hardening applies and whether the symlink confinement
* is opted out. 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"
extern int am_chrooted;
extern int am_sender;
extern int module_id;
extern int insecure_links;
extern int open_noatime;
/* Single gate for whether path resolution must be hardened against
* parent-component symlink races (TOCTOU). Used by the do_*_at()/do_*_atfd()
* wrappers and the receiver's secure-open/secure-mkstemp choices. Hardens
* every non-chrooted receiver (a chroot is its own confinement); the sender is
* excluded so it still follows -L/--copy-links symlinks. A daemon chroot with
* an inner-module /./ boundary still needs these checks because the kernel
* chroot confines the outer path, not the inner module. */
int vfs_relpath_active(void)
{
/* The "insecure links" / --insecure-links opt-out restores the legacy
* follow-any-symlink behaviour uniformly, so it disables the secure
* resolver on the RECEIVER side too (not just the sender enumeration that
* already checks vfs_symlink_optout_allowed()). Without this an opted-out
* module still confined receiver writes/stats through a pre-existing
* in-module symlink -- failing to match the pre-3.4.3 behaviour the opt-out
* promises (documented in rsyncd.conf(5) "munge symlinks"/"insecure links"). */
if (vfs_symlink_optout_allowed())
return 0;
if (am_daemon && am_chrooted && vfs.module_dirlen)
return 1;
return !am_chrooted && (am_daemon || !am_sender);
}
/* Whether the operator-supplied-path symlink confinement is opted out. For a
* non-daemon transfer this is the local --insecure-links flag. For a daemon it
* is governed ONLY by the module's "insecure links" config (lp_insecure_links)
* -- never by a peer-supplied --insecure-links (a client cannot disable a
* daemon's confinement; the daemon also drops a connection that sends it). So a
* forwarded flag is structurally inert here. */
int vfs_symlink_optout_allowed(void)
{
if (am_daemon)
return module_id >= 0 && lp_insecure_links(module_id);
return insecure_links;
}
/* STRICT_CONFINEMENT enforcement predicate (no effect on production behaviour).
* True when `path` is a relative, multi-component path that the receiver-side
* confinement is meant to protect -- i.e. a metadata op on it was expected to go
* through a confined fd, never a raw path-based syscall. False for: a build
* lacking the *at/O_NOFOLLOW primitives (the portability-fallback regime), an
* operator-supplied path (its own ownership walk governs it), an absolute path,
* and a top-level (no-slash) path with no parent component to flip. */
int vfs_must_be_confined(const char *path, int is_operator)
{
#if defined(O_NOFOLLOW) && defined(O_DIRECTORY) && defined(AT_FDCWD)
if (is_operator)
return 0;
if (!path || !*path || *path == '/')
return 0;
if (!strchr(path, '/'))
return 0;
return vfs_relpath_active();
#else
(void)path; (void)is_operator;
return 0;
#endif
}
#ifdef STRICT_CONFINEMENT
/* The strict build's hard stop: a confined-regime path-based metadata op was
* about to run where a confined fd was required. Loudly fail (the test suite
* then catches the reintroduced fallback) rather than silently doing an
* unconfined op a flipped parent could redirect outside the tree. */
void vfs_strict_confine_fail(const char *path, const char *what)
{
rprintf(FERROR,
"STRICT_CONFINEMENT violation: unconfined path-based %s on confined path \"%s\"\n",
what ? what : "metadata op", path ? path : "(null)");
abort();
}
#endif
/*
open a file relative to a base directory. The basedir can be NULL,
in which case the current working directory is used. The relpath
must be a relative path. Resolution cannot escape basedir (or the
cwd, when basedir is NULL): no ".." jumps above the start, no
symlinks pointing outside, no absolute paths.
Symlinks *within* basedir are followed normally earlier rsync
versions rejected every symlink with O_NOFOLLOW on each component,
which broke legitimate directory symlinks on the receiver side
(https://github.com/RsyncProject/rsync/issues/715).
Escape prevention is handled by a single portable mechanism on every
platform: a per-component O_NOFOLLOW walk on a stack of held parent
dirfds (the dirstack helpers above). Each component is opened
relative to a pinned parent fd, so no rename or symlink-swap can
redirect resolution; ".." pops to the already-held parent (never
above the anchor); an in-tree directory symlink is followed by
reading its target and walking it on the same stack (absolute
targets refused, symlink hops bounded). Single-component + O_NOFOLLOW
+ a pinned parent fd is race-free by construction, with no kernel
"beneath" primitive required (see dev-notes/resolver-race-freeness).
The relpath must also not contain any ../ elements in the path
(except for a deliberately re-anchored module path; see below).
*/
#if defined(O_NOFOLLOW) && defined(O_DIRECTORY) && defined(AT_FDCWD)
/* Walk `relpath` confined beneath the borrowed anchor dirfd (which may be
* AT_FDCWD) and return the opened leaf fd, or -1. Does NOT close `anchor_fd` --
* the caller owns it. Shared by vfs_resolve_open() (which first resolves a
* basedir to the anchor) and vfs_resolve_open_at() (handed an already-open
* anchor, e.g. a held module-root fd). `hops` is the shared symlink-hop budget. */
static int secure_walk_at(int anchor_fd, const char *anchor_abspath,
const char *relpath, int flags, mode_t mode, int *hops)
{
struct dirstack ds;
int retfd = -1;
char *path_copy;
if (ds_init(&ds, anchor_fd) < 0)
return -1;
/* Seed the abspath tracker so the exclude-aware refusal can map a resolved
* path back to module-relative. Only an absolute anchor enables it. */
if (anchor_abspath && anchor_abspath[0] == '/')
strlcpy(ds.abspath, anchor_abspath, sizeof ds.abspath);
path_copy = my_strdup(relpath, __FILE__, __LINE__);
if (!path_copy) {
ds_free(&ds);
return -1;
}
/* Trim trailing slashes so the last-component test below is exact, then
* note the offset of the final component. */
size_t pclen = strlen(path_copy);
while (pclen > 1 && path_copy[pclen-1] == '/')
path_copy[--pclen] = '\0';
char *last_slash = strrchr(path_copy, '/');
size_t last_off = last_slash ? (size_t)(last_slash + 1 - path_copy) : 0;
int saw_component = 0;
char *psave = NULL;
for (char *part = strtok_r(path_copy, "/", &psave);
part != NULL;
part = strtok_r(NULL, "/", &psave))
{
int is_last = (size_t)(part - path_copy) == last_off;
saw_component = 1;
/* A literal "." or ".." is a movement, not a name to open. It must go
* through ds_descend(), which refuses to pop above the anchor, BEFORE
* the leaf fast paths below -- those openat() the component directly,
* so a final ".." would otherwise hand back the anchor's own parent
* (with O_NOFOLLOW) or open it transiently (without O_DIRECTORY). */
if (part[0] == '.'
&& (part[1] == '\0' || (part[1] == '.' && part[2] == '\0'))) {
if (ds_descend(&ds, part, hops) < 0)
goto cleanup;
if (is_last) {
if (flags & O_DIRECTORY)
retfd = ds_take(&ds);
else
errno = EISDIR;
goto cleanup;
}
continue;
}
/* File leaf (final component, caller did not ask for O_DIRECTORY):
* never follow a symlink leaf. */
if (is_last && !(flags & O_DIRECTORY)) {
if (ds.abspath[0]) {
char leafabs[MAXPATHLEN];
if (snprintf(leafabs, sizeof leafabs, "%s/%s", ds.abspath, part)
< (int)sizeof leafabs
&& abspath_outside_confinement(leafabs, 0)) {
errno = ELOOP;
goto cleanup;
}
}
int next_fd = openat(ds_cur(&ds), part, O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
if (next_fd == -1 && (errno == ENOTDIR || errno == ENOENT)) {
retfd = openat(ds_cur(&ds), part, flags | O_NOFOLLOW, mode);
goto cleanup;
}
if (next_fd == -1)
goto cleanup;
close(next_fd);
errno = EISDIR;
goto cleanup;
}
/* O_DIRECTORY|O_NOFOLLOW leaf: the caller's O_NOFOLLOW governs the leaf. */
if (is_last && (flags & O_NOFOLLOW)) {
retfd = openat(ds_cur(&ds), part, O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
goto cleanup;
}
/* Directory component (intermediate, or an O_DIRECTORY leaf to follow):
* descend on the stack, following in-tree symlinks. */
if (ds_descend(&ds, part, hops) < 0) {
if (!is_last && errno == ENOTDIR)
errno = ELOOP;
goto cleanup;
}
if (is_last) {
retfd = ds_take(&ds);
goto cleanup;
}
}
/* Empty relpath: hand back a real anchor for an O_DIRECTORY caller (ds_take
* dups the borrowed anchor), else EISDIR. An AT_FDCWD anchor is not a
* resolvable target, so it fails rather than silently returning the cwd. */
if (!saw_component) {
if ((flags & O_DIRECTORY) && anchor_fd != AT_FDCWD)
retfd = ds_take(&ds);
else
errno = EISDIR;
}
cleanup:
free(path_copy);
ds_free(&ds);
return retfd;
}
#endif /* O_NOFOLLOW && O_DIRECTORY && AT_FDCWD */
int vfs_resolve_open(const char *basedir, const char *relpath, int flags, mode_t mode)
{
extern int am_daemon, am_chrooted;
char modrel_buf[MAXPATHLEN];
int reanchored = 0;
if (!relpath || relpath[0] == '/') {
// must be a relative path
errno = EINVAL;
return -1;
}
/* Sanitizing daemon (am_daemon && !am_chrooted) and the /./ inner-module
* chroot (am_daemon && am_chrooted && vfs.module_dirlen) -- both keep the module
* root, not the cwd, as the trust boundary. Here we have chdir'd into a
* sub-dir of the module (the transfer destination), so a relative alt-dest
* like "../01" may legitimately climb to a sibling that is still inside the
* module (#915). Confining beneath the cwd would reject that climb.
* Re-anchor at the module root by prefixing the cwd's module-relative path
* (from rsync's logical vfs.curr_dir[], a guaranteed lexical prefix of
* vfs.module_dir, unlike getcwd()) and resolving beneath vfs.module_dir; RESOLVE_
* BENEATH then allows in-module climbs and still rejects escapes. Only for
* paths that contain "..". vfs.module_dirlen is 0 for a `path = /` module
* (clientserver.c), so the non-chroot arm gates on vfs.module_dir, not its
* length, to cover that case too -- the prefix check below treats
* vfs.module_dirlen 0 as "module root is /". */
if (am_daemon && (!am_chrooted || vfs.module_dirlen)
&& vfs.module_dir && vfs.module_dir[0] == '/'
&& (basedir == NULL || basedir[0] != '/')
&& (path_has_dotdot_component(relpath)
|| (basedir && path_has_dotdot_component(basedir)))) {
const char *p;
int n;
if (vfs.curr_dir_len >= vfs.module_dirlen
&& strncmp(vfs.curr_dir, vfs.module_dir, vfs.module_dirlen) == 0
&& (vfs.curr_dir[vfs.module_dirlen] == '\0' || vfs.curr_dir[vfs.module_dirlen] == '/')) {
for (p = vfs.curr_dir + vfs.module_dirlen; *p == '/'; p++) {}
if (basedir)
n = snprintf(modrel_buf, sizeof modrel_buf, "%s%s%s/%s",
p, *p ? "/" : "", basedir, relpath);
else
n = snprintf(modrel_buf, sizeof modrel_buf, "%s%s%s",
p, *p ? "/" : "", relpath);
if (n < 0 || n >= (int)sizeof modrel_buf) {
errno = ENAMETOOLONG;
return -1;
}
basedir = vfs.module_dir; /* absolute, operator-trusted anchor */
relpath = modrel_buf;
reanchored = 1;
}
/* else: cwd not under module root as expected -- fall through to the
* front-door rejection below (fail safe). */
}
/* Reject any path with a literal ".." component (bare "..",
* "../foo", "foo/..", "foo/../bar", "subdir/..") at the front door,
* with EINVAL, so callers can rely on the validation regardless of
* platform. Skipped for a re-anchored path: its ".." is deliberate,
* stays within the module, and is adjudicated safely by the walk
* below (ds_descend pops a "../" to the held parent, never above the
* anchor). */
if (!reanchored) {
if (path_has_dotdot_component(relpath)) {
errno = EINVAL;
return -1;
}
if (basedir && basedir[0] != '/' && path_has_dotdot_component(basedir)) {
errno = EINVAL;
return -1;
}
}
#ifdef O_NOATIME
if (open_noatime)
flags |= O_NOATIME;
#endif
#if !defined(O_NOFOLLOW) || !defined(O_DIRECTORY) || !defined(AT_FDCWD)
// really old system, all we can do is live with the risks
if (!basedir) {
return open(relpath, flags, mode);
}
char fullpath[MAXPATHLEN];
pathjoin(fullpath, sizeof fullpath, basedir, relpath);
return open(fullpath, flags, mode);
#else
int dirfd = AT_FDCWD; /* anchor for the relpath walk (owned unless AT_FDCWD) */
int hops = SECURE_OPEN_MAXSYMLINKS; /* shared symlink-hop budget */
if (basedir != NULL) {
if (basedir[0] == '/') {
/* Absolute basedir: operator-trusted. Prefer the identity-pinned
* module-root fd when this is the served module, so a dropped-
* privilege daemon need not re-traverse the absolute path. */
dirfd = open_anchor_dirfd(basedir);
if (dirfd == -1)
return -1;
} else {
/* Relative basedir: resolve it on a dirfd stack anchored at
* the CWD, following in-tree directory symlinks -- the
* portable RESOLVE_BENEATH equivalent. A symlink target's
* ".." may climb but not above the CWD anchor. */
struct dirstack bds;
char *bcopy;
if (ds_init(&bds, AT_FDCWD) < 0)
return -1;
bcopy = my_strdup(basedir, __FILE__, __LINE__);
if (!bcopy) {
ds_free(&bds);
return -1;
}
if (ds_walk_path(&bds, bcopy, &hops) < 0) {
int e = errno;
free(bcopy);
ds_free(&bds);
errno = e;
return -1;
}
free(bcopy);
dirfd = ds_take(&bds); /* owned dirfd for the basedir */
ds_free(&bds);
if (dirfd == -1)
return -1;
}
}
/* Absolute path of the anchor, for the exclude-aware refusal: the cwd (==
* module root for a daemon) when AT_FDCWD, or an operator-trusted absolute
* basedir. A relative basedir's resolved abspath isn't tracked, so leave it
* unseeded (the refusal is then a no-op for that uncommon case). */
const char *anchor_abspath = !basedir ? vfs.curr_dir
: (basedir[0] == '/' ? basedir : NULL);
int retfd = secure_walk_at(dirfd, anchor_abspath, relpath, flags, mode, &hops);
if (dirfd != AT_FDCWD)
close(dirfd);
return retfd;
#endif // O_NOFOLLOW, O_DIRECTORY
}
/* Common fd-anchored resolver. A caller may explicitly allow literal ".."
* components when the fd itself is the confinement boundary: secure_walk_at()
* resolves each one by popping its held-dirfd stack and refuses a pop above the
* anchor. Other callers retain the front-door validation used by
* vfs_resolve_open(). */
static int vfs__resolve_open_at_internal(int anchor_fd, const char *relpath,
int flags, mode_t mode, int allow_dotdot)
{
#if !defined(O_NOFOLLOW) || !defined(O_DIRECTORY) || !defined(AT_FDCWD)
(void)anchor_fd; (void)relpath; (void)flags; (void)mode; (void)allow_dotdot;
errno = ENOSYS;
return -1;
#else
int hops = SECURE_OPEN_MAXSYMLINKS;
if (!relpath || relpath[0] == '/') {
errno = EINVAL;
return -1;
}
if (!allow_dotdot && path_has_dotdot_component(relpath)) {
errno = EINVAL;
return -1;
}
#ifdef O_NOATIME
if (open_noatime)
flags |= O_NOATIME;
#endif
/* The anchor fd's absolute path isn't known here (it may be a held module
* root or a climbed-to dir), so leave the abspath tracker unseeded; the
* exclude-aware refusal is a no-op for this entry point. */
return secure_walk_at(anchor_fd, NULL, relpath, flags, mode, &hops);
#endif
}
/* Like vfs_resolve_open() but anchored at an already-open directory fd
* (borrowed -- the caller keeps ownership) rather than a basedir path. Lets a
* caller pin the trust root once -- e.g. a daemon's module root opened while
* still privileged -- and resolve a relative path beneath it without re-walking
* the absolute path as a dropped-privilege uid. The ordinary entry point keeps
* rejecting literal ".." components as suspicious caller input. */
int vfs_resolve_open_at(int anchor_fd, const char *relpath, int flags, mode_t mode)
{
return vfs__resolve_open_at_internal(anchor_fd, relpath, flags, mode, 0);
}
/* Resolve a path that may contain literal ".." beneath a trusted anchor fd.
* Used for a followed symlink target, where parent-relative components are
* normal pathname semantics. The held-fd stack still refuses every escape
* above anchor_fd. */
int vfs_resolve_open_at_beneath(int anchor_fd, const char *relpath,
int flags, mode_t mode)
{
return vfs__resolve_open_at_internal(anchor_fd, relpath, flags, mode, 1);
}
/* Resolve ONE operand of a two-path op (rename/link) to a parent dirfd + leaf,
* per that operand's OWN policy -- so a two-path op can confine each side
* independently (an operator basis/backup path on one side must not relax the
* transfer-path confinement of the other). Policy:
* - side_flags & VFS_OPERATOR_PATH, or an absolute path: ownership walk
* (follow uid0/euid symlinks, refuse foreign; module-exclude enforced).
* - a relative path with a slash: secure receiver resolve of the parent.
* - a bare name: AT_FDCWD + the name.
* Sets *bname and *dfd_out (a dirfd or AT_FDCWD), and *owns True when *dfd_out
* must be closed by the caller. dirbuf (>= MAXPATHLEN) is scratch for a parent
* path. Returns 0 on success, -1 (errno set) on error. The caller must already
* have confirmed vfs_relpath_active() (otherwise it does the plain libc op). */
int vfs_twopath_side(const char *path, int side_flags, const char **bname,
int *dfd_out, BOOL *owns, char *dirbuf, size_t dirbufsz)
{
*owns = False;
#ifdef AT_FDCWD
const char *slash = strrchr(path, '/');
size_t dlen;
#if defined O_NOFOLLOW && defined O_DIRECTORY
if (((side_flags & VFS_OPERATOR_PATH) || *path == '/')
&& !vfs_symlink_optout_allowed()) {
int dfd = vfs_owner_walk_parent(path, bname, 1);
if (dfd < 0)
return -1;
*dfd_out = dfd;
*owns = True;
return 0;
}
#endif
if (*path == '/' || !slash) {
/* absolute under --insecure-links, or a bare name: AT_FDCWD + path. */
*bname = path;
*dfd_out = AT_FDCWD;
return 0;
}
dlen = (size_t)(slash - path);
if (dlen >= dirbufsz) {
errno = ENAMETOOLONG;
return -1;
}
memcpy(dirbuf, path, dlen);
dirbuf[dlen] = '\0';
*bname = slash + 1;
*dfd_out = vfs_resolve_open(NULL, dirbuf, O_RDONLY | O_DIRECTORY, 0);
if (*dfd_out < 0)
return -1;
*owns = True;
return 0;
#else
(void)side_flags; (void)dirbuf; (void)dirbufsz;
*bname = path;
*dfd_out = -1;
return 0;
#endif
}
+189
View File
@@ -0,0 +1,189 @@
/*
* 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
}
+259
View File
@@ -0,0 +1,259 @@
/*
* vfs/symlink.c - symlink and readlink wrappers.
*
* Includes the fake-super (NO_SYMLINK_*XATTRS) placeholder-file handling that
* stands in for real symlinks when the receiver can't create them. 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"
#ifdef SUPPORT_LINKS
static int vfs__symlink_plain(const char *lnk, const char *path)
{
if (dry_run) return 0;
RETURN_ERROR_IF_RO_OR_LO;
RETURN_ERROR_IF_NULL(lnk);
RETURN_ERROR_IF_NULL(path);
#if defined NO_SYMLINK_XATTRS || defined NO_SYMLINK_USER_XATTRS
/* For --fake-super, we create a normal file with mode 0600
* and write the lnk into it. */
if (am_root < 0) {
int ok, len = strlen(lnk);
int fd = open(path, O_WRONLY|O_CREAT|O_TRUNC, S_IWUSR|S_IRUSR);
if (fd < 0)
return -1;
ok = write(fd, lnk, len) == len;
if (close(fd) < 0)
ok = 0;
return ok ? 0 : -1;
}
#endif
return symlink(lnk, path);
}
/*
Symlink-race-safe variant of vfs_symlink() for receiver-side use. See
the comment on vfs__chmod_secure() for the threat model. For a real symlink
only the parent directory of `path` needs protection -- symlinkat()
does not resolve the final component (it creates it). Defence: open
the parent of `path` under vfs_resolve_open() and call symlinkat()
against that dirfd; a top-level (no-slash) path has no parent to
confine, so it uses AT_FDCWD directly. The link target string `lnk` is
stored verbatim and not resolved at creation time, so it doesn't need
scrutiny here.
For --fake-super (am_root < 0) the "symlink" is written as a regular
file, so the final component IS resolved at creation: we create it
with openat(... O_NOFOLLOW) so a pre-planted symlink at the basename
cannot redirect the write outside the module. This protection applies
to top-level paths too -- the previous code fell through to the
bare-path vfs_symlink() there, whose plain open() followed such a
symlink.
*/
static int vfs__symlink_secure(const char *lnk, const char *path, int flags)
{
#ifdef AT_FDCWD
char dirpath[MAXPATHLEN];
const char *bname;
const char *slash;
int dfd = AT_FDCWD, ret, e;
BOOL owns = False;
size_t dlen;
if (dry_run) return 0;
RETURN_ERROR_IF_RO_OR_LO;
#if defined O_NOFOLLOW && defined O_DIRECTORY
if (flags & VFS_OPERATOR_PATH) {
/* Operator path (e.g. an absolute --backup-dir): confine the
* parent with the ownership walk, then fall through to the shared
* leaf-creation below so fake-super emulation is preserved. */
if (vfs_symlink_optout_allowed())
return vfs__symlink_plain(lnk, path);
dfd = vfs_owner_walk_parent(path, &bname, 1);
if (dfd < 0)
return -1;
owns = True;
} else
#endif
{
(void)flags;
if (!vfs_relpath_active())
return vfs__symlink_plain(lnk, path);
if (!path || !*path || *path == '/')
return vfs__symlink_plain(lnk, path);
/* A path with a slash needs vfs_resolve_open to confine its
* parent; a top-level path is in CWD (AT_FDCWD), no parent to
* subvert. The leaf is protected below either way (symlinkat()
* won't follow it; the fake-super openat() uses O_NOFOLLOW). */
slash = strrchr(path, '/');
if (slash) {
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;
owns = True;
} else {
bname = path;
}
}
#if defined NO_SYMLINK_XATTRS || defined NO_SYMLINK_USER_XATTRS
/* For --fake-super, vfs_symlink writes the link target into a
* regular file rather than creating a real symlink. Do that here
* against the (secure or AT_FDCWD) dirfd, with O_NOFOLLOW so a pre-
* planted symlink at the basename can't redirect the file creation. */
if (am_root < 0) {
int len = strlen(lnk);
int fd = openat(dfd, bname,
O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW,
S_IWUSR | S_IRUSR);
if (fd < 0) {
e = errno;
if (owns) close(dfd);
errno = e;
return -1;
}
ret = (write(fd, lnk, len) == len) ? 0 : -1;
if (close(fd) < 0)
ret = -1;
e = errno;
if (owns) close(dfd);
errno = e;
return ret;
}
#endif
ret = symlinkat(lnk, dfd, bname);
e = errno;
if (owns) close(dfd);
errno = e;
return ret;
#else
return vfs__symlink_plain(lnk, path);
#endif
}
/* NOFOLLOW_HIT_SYMLINK() lives in rsync.h (shared with util1.c's change_dir). */
#if defined NO_SYMLINK_XATTRS || defined NO_SYMLINK_USER_XATTRS
ssize_t vfs_readlink(const char *path, char *buf, size_t bufsiz)
{
/* For --fake-super, we read the link from the file. */
if (am_root < 0) {
int fd = vfs_open_nofollow(path, O_RDONLY);
if (fd >= 0) {
int len = read(fd, buf, bufsiz);
close(fd);
return len;
}
if (!NOFOLLOW_HIT_SYMLINK(errno))
return -1;
/* A real symlink needs to be turned into a fake one on the receiving
* side, so tell the generator that the link has no length. */
if (!am_sender)
return 0;
/* Otherwise fall through and let the sender report the real length. */
}
return readlink(path, buf, bufsiz);
}
#endif
ssize_t vfs_readlink_atfd(int dfd, const char *name, char *buf, size_t bufsiz)
{
#ifdef AT_FDCWD
# if defined NO_SYMLINK_XATTRS || defined NO_SYMLINK_USER_XATTRS
if (am_root < 0) {
int fd = openat(dfd, name, O_RDONLY | O_NOFOLLOW);
if (fd >= 0) {
int len = read(fd, buf, bufsiz);
close(fd);
return len;
}
if (!NOFOLLOW_HIT_SYMLINK(errno))
return -1;
if (!am_sender)
return 0;
}
# endif
return readlinkat(dfd, name, buf, bufsiz);
#else
(void)dfd;
return vfs_readlink(name, buf, bufsiz);
#endif
}
#endif
static int vfs__symlink_atfd(const char *lnk, int dfd, const char *name)
{
#ifdef AT_FDCWD
if (dry_run) return 0;
RETURN_ERROR_IF_RO_OR_LO;
#if defined NO_SYMLINK_XATTRS || defined NO_SYMLINK_USER_XATTRS
/* --fake-super: store the link target in a regular placeholder file,
* created with O_NOFOLLOW so a planted basename symlink can't redirect
* the write (mirrors vfs__symlink_secure()). */
if (am_root < 0) {
int len = strlen(lnk);
int ok;
int fd = openat(dfd, name, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW,
S_IWUSR | S_IRUSR);
if (fd < 0)
return -1;
ok = write(fd, lnk, len) == len;
if (close(fd) < 0)
ok = 0;
return ok ? 0 : -1;
}
#endif
return symlinkat(lnk, dfd, name);
#else
(void)lnk; (void)dfd; (void)name;
errno = ENOSYS;
return -1;
#endif
}
/* Unified symlink creation. dirfd == VFS_AT_FDCWD resolves `path`; a real held
* dirfd makes `path` a single validated component under it. flags:
* VFS_ALLOW_SYMLINK (trusted, plain symlink), VFS_OPERATOR_PATH (operator
* path: ownership walk), default 0 (secure receiver resolve). */
int vfs_symlink(const char *lnk, int dirfd, const char *path, int flags)
{
if (dirfd != VFS_AT_FDCWD) {
if (!path || !*path || strchr(path, '/')
|| (path[0] == '.' && (path[1] == '\0'
|| (path[1] == '.' && path[2] == '\0')))) {
errno = EINVAL;
return -1;
}
return vfs__symlink_atfd(lnk, dirfd, path);
}
if (flags & VFS_ALLOW_SYMLINK)
return vfs__symlink_plain(lnk, path);
return vfs__symlink_secure(lnk, path, flags);
}
+380
View File
@@ -0,0 +1,380 @@
/*
* vfs/times.c - timestamp-setting wrappers (utimensat/lutimes/utimes/utime)
* plus the macOS setattrlist crtime path and the Cygwin SetFileTime path.
*
* 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"
#ifdef HAVE_SYS_ATTR_H
#include <sys/attr.h>
#endif
#ifdef SUPPORT_CRTIMES
#ifdef HAVE_GETATTRLIST
#pragma pack(push, 4)
struct create_time {
uint32 length;
struct timespec crtime;
};
#pragma pack(pop)
#elif defined __CYGWIN__
#include <windows.h>
#endif
#endif
#ifdef HAVE_SETATTRLIST
int vfs_setattrlist_times(const char *path, STRUCT_STAT *stp)
{
struct attrlist attrList;
struct timespec ts[2];
if (dry_run) return 0;
RETURN_ERROR_IF_RO_OR_LO;
/* setattrlist() takes a raw path and follows parent symlinks
* (FSOPT_NOFOLLOW only blocks the final component). When hardened
* resolution is active -- vfs_relpath_active(): any non-chroot
* daemon/receiver module, plus a /./ inner-module chroot -- return
* ENOSYS so set_times()' tier walk falls through to vfs_utimensat_at(),
* which routes the update through a secure parent dirfd. The attribute
* set this would have used (ATTR_CMN_MODTIME / ATTR_CMN_ACCTIME) is the
* same set utimensat() handles, so no functionality is lost. */
if (vfs_relpath_active()) {
errno = ENOSYS;
return -1;
}
/* Yes, this is in the opposite order of utime and similar. */
ts[0].tv_sec = stp->st_mtime;
ts[0].tv_nsec = stp->ST_MTIME_NSEC;
ts[1].tv_sec = stp->st_atime;
ts[1].tv_nsec = stp->ST_ATIME_NSEC;
memset(&attrList, 0, sizeof attrList);
attrList.bitmapcount = ATTR_BIT_MAP_COUNT;
attrList.commonattr = ATTR_CMN_MODTIME | ATTR_CMN_ACCTIME;
return setattrlist(path, &attrList, ts, sizeof ts, FSOPT_NOFOLLOW);
}
#ifdef SUPPORT_CRTIMES
int vfs_setattrlist_crtime(const char *path, time_t crtime)
{
struct attrlist attrList;
struct timespec ts;
if (dry_run) return 0;
RETURN_ERROR_IF_RO_OR_LO;
/* setattrlist() is path-based and follows parent symlinks
* (FSOPT_NOFOLLOW only blocks the final component), and macOS has no
* at-aware variant targeting ATTR_CMN_CRTIME. As with POSIX ACLs where
* the OS offers no race-safe primitive, we keep --crtimes functional
* (daemon and non-daemon) and accept the parent-component symlink race
* as a documented residual rather than dropping crtime. A daemon
* operator who does not want that residual can disable the feature with
* "refuse options = crtimes" in rsyncd.conf. */
ts.tv_sec = crtime;
ts.tv_nsec = 0;
memset(&attrList, 0, sizeof attrList);
attrList.bitmapcount = ATTR_BIT_MAP_COUNT;
attrList.commonattr = ATTR_CMN_CRTIME;
return setattrlist(path, &attrList, &ts, sizeof ts, FSOPT_NOFOLLOW);
}
#endif
#endif /* HAVE_SETATTRLIST */
#ifdef SUPPORT_CRTIMES
time_t vfs_get_create_time(const char *path, STRUCT_STAT *stp)
{
#ifdef HAVE_GETATTRLIST
static struct create_time attrBuf;
struct attrlist attrList;
(void)stp;
/* getattrlist() is path-based and follows parent symlinks; like
* vfs_setattrlist_crtime() there is no race-safe variant, so reading the
* source crtime stays functional and the parent-component symlink race
* is an accepted residual (refusable via "refuse options = crtimes"). */
memset(&attrList, 0, sizeof attrList);
attrList.bitmapcount = ATTR_BIT_MAP_COUNT;
attrList.commonattr = ATTR_CMN_CRTIME;
if (getattrlist(path, &attrList, &attrBuf, sizeof attrBuf, FSOPT_NOFOLLOW) < 0)
return 0;
return attrBuf.crtime.tv_sec;
#elif defined __CYGWIN__
(void)path;
return stp->st_birthtime;
#else
#error Unknown crtimes implementation
#endif
}
#if defined __CYGWIN__
int vfs_SetFileTime(const char *path, time_t crtime)
{
if (dry_run) return 0;
RETURN_ERROR_IF_RO_OR_LO;
int cnt = MultiByteToWideChar(CP_UTF8, 0, path, -1, NULL, 0);
if (cnt == 0)
return -1;
WCHAR *pathw = new_array(WCHAR, cnt);
if (!pathw)
return -1;
MultiByteToWideChar(CP_UTF8, 0, path, -1, pathw, cnt);
HANDLE handle = CreateFileW(pathw, FILE_WRITE_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
free(pathw);
if (handle == INVALID_HANDLE_VALUE)
return -1;
int64 temp_time = (crtime * 10000000LL) + 116444736000000000LL;
FILETIME birth_time;
birth_time.dwLowDateTime = (DWORD)temp_time;
birth_time.dwHighDateTime = (DWORD)(temp_time >> 32);
int ok = SetFileTime(handle, &birth_time, NULL, NULL);
CloseHandle(handle);
return ok ? 0 : -1;
}
#endif
#endif /* SUPPORT_CRTIMES */
#ifdef HAVE_UTIMENSAT
int vfs_utimensat(const char *path, STRUCT_STAT *stp)
{
struct timespec t[2];
if (dry_run) return 0;
RETURN_ERROR_IF_RO_OR_LO;
RETURN_ERROR_IF_NULL(path);
t[0].tv_sec = stp->st_atime;
#ifdef ST_ATIME_NSEC
t[0].tv_nsec = stp->ST_ATIME_NSEC;
#else
t[0].tv_nsec = 0;
#endif
t[1].tv_sec = stp->st_mtime;
#ifdef ST_MTIME_NSEC
t[1].tv_nsec = stp->ST_MTIME_NSEC;
#else
t[1].tv_nsec = 0;
#endif
return utimensat(AT_FDCWD, path, t, AT_SYMLINK_NOFOLLOW);
}
/*
Symlink-race-safe variant of vfs_utimensat() for receiver-side use.
See the comment on vfs__chmod_secure() for the threat model. utimes()
resolves parent components and follows a final-component symlink;
lutimes() doesn't follow the final component but still resolves
parents. Either way, a parent-symlink swap can redirect the
timestamp update outside the module. Defence: open the parent of
path under vfs_resolve_open() and call utimensat() with
AT_SYMLINK_NOFOLLOW against that dirfd.
Falls through to vfs_utimensat() in the same dry-run / non-daemon /
chrooted / no-parent / absolute-path cases as the other wrappers.
Returns -1 with errno=ENOSYS on systems without utimensat()
(caller is expected to fall back to the legacy tier walk).
*/
int vfs_utimensat_at(const char *path, STRUCT_STAT *stp)
{
#ifdef AT_FDCWD
struct timespec t[2];
char dirpath[MAXPATHLEN];
const char *bname;
const char *slash;
int dfd, ret, e;
size_t dlen;
if (dry_run) return 0;
RETURN_ERROR_IF_RO_OR_LO;
if (!vfs_relpath_active())
return vfs_utimensat(path, stp);
if (!path || !*path || *path == '/')
return vfs_utimensat(path, stp);
slash = strrchr(path, '/');
if (!slash)
return vfs_utimensat(path, stp);
dlen = slash - path;
if (dlen >= sizeof dirpath) {
errno = ENAMETOOLONG;
return -1;
}
memcpy(dirpath, path, dlen);
dirpath[dlen] = '\0';
bname = slash + 1;
t[0].tv_sec = stp->st_atime;
#ifdef ST_ATIME_NSEC
t[0].tv_nsec = stp->ST_ATIME_NSEC;
#else
t[0].tv_nsec = 0;
#endif
t[1].tv_sec = stp->st_mtime;
#ifdef ST_MTIME_NSEC
t[1].tv_nsec = stp->ST_MTIME_NSEC;
#else
t[1].tv_nsec = 0;
#endif
dfd = vfs_resolve_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0);
if (dfd < 0)
return -1;
ret = utimensat(dfd, bname, t, AT_SYMLINK_NOFOLLOW);
e = errno;
close(dfd);
errno = e;
return ret;
#else
return vfs_utimensat(path, stp);
#endif
}
#endif
#ifdef HAVE_LUTIMES
int vfs_lutimes(const char *path, STRUCT_STAT *stp)
{
struct timeval t[2];
if (dry_run) return 0;
RETURN_ERROR_IF_RO_OR_LO;
t[0].tv_sec = stp->st_atime;
#ifdef ST_ATIME_NSEC
t[0].tv_usec = stp->ST_ATIME_NSEC / 1000;
#else
t[0].tv_usec = 0;
#endif
t[1].tv_sec = stp->st_mtime;
#ifdef ST_MTIME_NSEC
t[1].tv_usec = stp->ST_MTIME_NSEC / 1000;
#else
t[1].tv_usec = 0;
#endif
return lutimes(path, t);
}
#endif
#ifdef HAVE_UTIMES
int vfs_utimes(const char *path, STRUCT_STAT *stp)
{
struct timeval t[2];
if (dry_run) return 0;
RETURN_ERROR_IF_RO_OR_LO;
t[0].tv_sec = stp->st_atime;
#ifdef ST_ATIME_NSEC
t[0].tv_usec = stp->ST_ATIME_NSEC / 1000;
#else
t[0].tv_usec = 0;
#endif
t[1].tv_sec = stp->st_mtime;
#ifdef ST_MTIME_NSEC
t[1].tv_usec = stp->ST_MTIME_NSEC / 1000;
#else
t[1].tv_usec = 0;
#endif
return utimes(path, t);
}
#elif defined HAVE_UTIME
int vfs_utime(const char *path, STRUCT_STAT *stp)
{
#ifdef HAVE_STRUCT_UTIMBUF
struct utimbuf tbuf;
#else
time_t t[2];
#endif
if (dry_run) return 0;
RETURN_ERROR_IF_RO_OR_LO;
# ifdef HAVE_STRUCT_UTIMBUF
tbuf.actime = stp->st_atime;
tbuf.modtime = stp->st_mtime;
return utime(path, &tbuf);
# else
t[0] = stp->st_atime;
t[1] = stp->st_mtime;
return utime(path, t);
# endif
}
#else
#error Need utimes or utime function.
#endif
#ifdef HAVE_UTIMENSAT
int vfs_utimensat_atfd(int dfd, const char *name, STRUCT_STAT *stp)
{
#ifdef AT_FDCWD
struct timespec t[2];
if (dry_run) return 0;
RETURN_ERROR_IF_RO_OR_LO;
t[0].tv_sec = stp->st_atime;
#ifdef ST_ATIME_NSEC
t[0].tv_nsec = stp->ST_ATIME_NSEC;
#else
t[0].tv_nsec = 0;
#endif
t[1].tv_sec = stp->st_mtime;
#ifdef ST_MTIME_NSEC
t[1].tv_nsec = stp->ST_MTIME_NSEC;
#else
t[1].tv_nsec = 0;
#endif
return utimensat(dfd, name, t, AT_SYMLINK_NOFOLLOW);
#else
(void)dfd; (void)name; (void)stp;
errno = ENOSYS;
return -1;
#endif
}
#endif
#ifdef HAVE_FUTIMENS
/* Set times on an already-open fd (the race-free counterpart for a pinned
* cross-tree operator leaf -- see set_file_attrs()). */
int vfs_futimens(int fd, STRUCT_STAT *stp)
{
struct timespec t[2];
if (dry_run) return 0;
RETURN_ERROR_IF_RO_OR_LO;
t[0].tv_sec = stp->st_atime;
#ifdef ST_ATIME_NSEC
t[0].tv_nsec = stp->ST_ATIME_NSEC;
#else
t[0].tv_nsec = 0;
#endif
t[1].tv_sec = stp->st_mtime;
#ifdef ST_MTIME_NSEC
t[1].tv_nsec = stp->ST_MTIME_NSEC;
#else
t[1].tv_nsec = 0;
#endif
return futimens(fd, t);
}
#endif
+107
View File
@@ -0,0 +1,107 @@
/*
* vfs/unlink.c - unlink and rmdir 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"
/* Secure receiver-side resolve for an unlink/rmdir. unlink() resolves parent
* components, so a parent-symlink swap can delete an outside file under the
* daemon's authority -- defence is to resolve the parent securely and unlinkat()
* the leaf. Both unlink and rmdir honour the operator ownership walk
* (VFS_OPERATOR_PATH): a foreign-owned parent component is refused while the
* operator's own is followed, absolute and relative alike.
* Falls through to a plain unlink()/rmdir() in non-daemon/sender, chrooted,
* no-parent and absolute-path cases. */
static int vfs__unlink_secure(const char *path, int flags)
{
int rmdir_op = flags & VFS_REMOVEDIR;
#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY
char dirpath[MAXPATHLEN];
const char *bname, *slash;
int dfd, ret, e, atflag = rmdir_op ? AT_REMOVEDIR : 0;
size_t dlen;
if (flags & VFS_OPERATOR_PATH) {
if (vfs_symlink_optout_allowed())
return rmdir_op ? rmdir(path) : unlink(path);
dfd = vfs_owner_walk_parent(path, &bname, 1);
if (dfd < 0)
return -1;
ret = unlinkat(dfd, bname, atflag);
e = errno;
close(dfd);
errno = e;
return ret;
}
if (!vfs_relpath_active() || !*path || *path == '/')
return rmdir_op ? rmdir(path) : unlink(path);
slash = strrchr(path, '/');
if (!slash)
return rmdir_op ? rmdir(path) : unlink(path);
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 = unlinkat(dfd, bname, atflag);
e = errno;
close(dfd);
errno = e;
return ret;
#else
return rmdir_op ? rmdir(path) : unlink(path);
#endif
}
/* Unified unlink/rmdir. dirfd == VFS_AT_FDCWD resolves `path`; a real held
* dirfd makes `path` a single component removed directly under it. flags:
* VFS_REMOVEDIR (rmdir/AT_REMOVEDIR instead of unlink), VFS_ALLOW_SYMLINK
* (trusted, plain), VFS_OPERATOR_PATH (operator path: ownership walk), default 0
* (secure receiver resolve). */
int vfs_unlink(int dirfd, const char *path, 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
if (!*path || strchr(path, '/')
|| (path[0] == '.' && (path[1] == '\0'
|| (path[1] == '.' && path[2] == '\0')))) {
errno = EINVAL;
return -1;
}
return unlinkat(dirfd, path, (flags & VFS_REMOVEDIR) ? AT_REMOVEDIR : 0);
#else
(void)dirfd;
errno = ENOSYS;
return -1;
#endif
}
if (flags & VFS_ALLOW_SYMLINK)
return (flags & VFS_REMOVEDIR) ? rmdir(path) : unlink(path);
return vfs__unlink_secure(path, flags);
}
+46
View File
@@ -0,0 +1,46 @@
/*
* vfs/vfs.c - core state for rsync's virtual filesystem layer.
*
* 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"
/* The single global VFS state. The designated initializer makes the cache and
* module snapshot safe by construction (a plain definition would zero base/
* module_dirfd, making fd 0 look like a valid cached dir) -- this matters for
* the t_*_secure test harnesses, which never run main() and so never call
* vfs_init(). */
struct vfs vfs = {
.dpc = { .base = -1, .anchor = VFS_DPC_ANCHOR_NONE },
.module_dirfd = -1,
};
/* Reset the VFS to a safe state. Idempotent; re-establishes the same safe
* sentinels as the static initializer, and clears the module-root snapshot so
* confinement state can never be stale (no module is attached yet). */
void vfs_init(void)
{
vfs.dpc.base = -1;
vfs.dpc.anchor = VFS_DPC_ANCHOR_NONE;
vfs.dpc.depth = 0;
vfs.module_dir = NULL;
vfs.module_dirlen = 0;
vfs.module_dirfd = -1;
}
/* Snapshot the served daemon module root for the confinement checks. Called
* by clientserver.c once the module path is final (with dirfd == -1), and again
* once the root dirfd is pinned. The dirfd is BORROWED -- open_anchor_dirfd()
* dup()s it; the VFS never closes it. */
void vfs_set_module_root(const char *dir, unsigned int len, int dirfd)
{
vfs.module_dir = dir;
vfs.module_dirlen = len;
vfs.module_dirfd = dirfd;
}
+241
View File
@@ -0,0 +1,241 @@
/*
* vfs/vfs.h - public interface to rsync's virtual filesystem layer.
*
* The VFS owns the messy, security-critical filesystem details (the vfs_*
* syscall wrappers, the race-safe path resolver, the held-dirfd cache, the
* operator-path ownership walk and the daemon module confinement) so the
* mainline protocol/transfer code can stay clean. Implementations live in the
* per-concern sources under vfs/ in two layers: PRIMITIVES (one file per
* operation family, plus the resolver/dirstack/dircache/owner-walk cores) and
* COMPOUNDS built on them (vfs_make_path, copy_file, robust_unlink/rename). The
* remaining VFS-internal state lives in the single "struct vfs vfs" below.
*
* Each single-path operation is one call taking a dirfd and a flags word:
*
* vfs_<op>(int dirfd, path, ..., int flags)
*
* dirfd == VFS_AT_FDCWD -- resolve `path`, per flags:
* 0 secure receiver resolve (race-safe O_NOFOLLOW parent
* walk + *at() leaf) when vfs_relpath_active(), else plain
* VFS_ALLOW_SYMLINK plain libc op -- the call site asserts the path is
* trusted to follow symlinks (was the bare vfs_<op>())
* VFS_OPERATOR_PATH operator-supplied path (--backup-dir/--temp-dir/
* --partial-dir/--link-dest): ownership walk (follow a
* uid0/euid-owned symlink, refuse a foreign one) +
* daemon module-root confinement
* a real held dirfd -- `path` is a single component acted on directly under
* it (from vfs_opendir/vfs_get_dirfd); no resolution.
*
* The operator-path policy is this explicit per-call flag, never ambient state.
* VFS_REMOVEDIR turns vfs_unlink into rmdir. chmod/lchown have no
* ownership-walk branch (VFS_OPERATOR_PATH resolves as the default secure walk).
* Two-path ops (vfs_rename_at, vfs_link_at) and open (distinct nofollow/
* checklinks variants) keep explicit forms + a vfs_flags arg; vfs_fstat and the
* fileio ops are fd-based.
*
* This header is included by rsync.h (just after proto.h) so every translation
* unit sees the vfs_* API. It must not include rsync.h itself.
*
* 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.
*/
#ifndef RSYNC_VFS_H
#define RSYNC_VFS_H
/* Exercise the pre-*at() portability tier on modern build hosts. rsync.h pulls
* this header in after the system headers that define AT_FDCWD, so stripping the
* *at primitives here strips them for the whole translation unit -- including
* the VFS_AT_FDCWD sentinel below, which then takes its no-AT_FDCWD value. The
* CHECK_COMPILE_OBJS target compiles every vfs/ source with this flag to keep
* the fallback arms building. (Was syscall.c's top before the split into vfs/;
* it must precede the VFS_AT_FDCWD definition.) */
#ifdef RSYNC_TEST_NO_AT_FDCWD
#undef AT_FDCWD
#undef AT_SYMLINK_NOFOLLOW
#undef HAVE_LINKAT
#undef HAVE_OPENAT2
#undef HAVE_UTIMENSAT
#undef O_RESOLVE_BENEATH
#endif
/* Max held ancestor-dirfd cache depth (was DPC_MAXDEPTH in syscall.c). */
#define VFS_DPC_MAXDEPTH 64
/* dirfd sentinel for the unified vfs_* ops: "no held directory -- resolve the
* path argument". Always defined (even on platforms without AT_FDCWD, where the
* held-fd forms are unavailable and report ENOSYS), so mainline code never has
* to mention AT_FDCWD directly. */
#ifdef AT_FDCWD
#define VFS_AT_FDCWD AT_FDCWD
#else
#define VFS_AT_FDCWD (-100)
#endif
/* Per-call flags for the unified vfs_* operations. The default (0) is the
* secure, no-follow parent resolve used by the receiver; the flags are explicit
* opt-ins that each carry a security meaning the call site is asserting.
* VFS_ALLOW_SYMLINK and VFS_OPERATOR_PATH are mutually exclusive policies; if
* both are passed VFS_ALLOW_SYMLINK wins (it is checked first). No call site
* needs both. */
#define VFS_ALLOW_SYMLINK (1<<0) /* call site is known-safe to follow symlinks */
#define VFS_OPERATOR_PATH (1<<1) /* operator-supplied path: ownership walk + module confinement */
#define VFS_REMOVEDIR (1<<2) /* unlink op targets a directory (AT_REMOVEDIR) */
/* Sentinel for dpc.anchor meaning "no anchor cached" -- a non-NULL, non-pointer
* value so it can never alias a real anchor path (NULL is a valid anchor: cwd). */
#define VFS_DPC_ANCHOR_NONE ((const char *)-2)
/* The single global VFS state instance (defined in vfs/vfs.c).
*
* Only curr_dir/curr_dir_len are read by mainline code; the dpc cache and the
* module_* snapshot are VFS-internal (touched only by the vfs/ sources) and are
* documented as such. The operator-supplied path resolution policy is no
* longer ambient state -- it travels as an explicit VFS_OPERATOR_PATH flag. */
struct vfs {
char curr_dir[MAXPATHLEN]; /* logical cwd (tracked by change_dir) */
unsigned int curr_dir_len;
/* VFS-INTERNAL: held ancestor-dirfd cache. */
struct {
const char *anchor; /* anchor path, or VFS_DPC_ANCHOR_NONE */
int base; /* owned anchor dir fd, or -1 */
int fd[VFS_DPC_MAXDEPTH]; /* fd after components 0..i */
char name[VFS_DPC_MAXDEPTH][256]; /* component names */
int depth;
} dpc;
/* VFS-INTERNAL: daemon served-module-root snapshot. */
const char *module_dir;
unsigned int module_dirlen;
int module_dirfd; /* identity-pinned module root fd, or -1 */
};
extern struct vfs vfs;
/* Reset the VFS to a safe between-transfers state. Safety at startup comes
* from the static initializer in vfs/vfs.c, not from this call. */
void vfs_init(void);
/* Snapshot the served daemon module root (clientserver.c calls this); the
* dirfd is borrowed, never closed by the VFS. */
void vfs_set_module_root(const char *dir, unsigned int len, int dirfd);
/* Race-safe path resolution (vfs/secure_open.c). */
int vfs_relpath_active(void);
int vfs_symlink_optout_allowed(void);
int vfs_resolve_open(const char *basedir, const char *relpath, int flags, mode_t mode);
int vfs_resolve_open_at(int anchor_fd, const char *relpath, int flags, mode_t mode);
int vfs_resolve_open_at_beneath(int anchor_fd, const char *relpath, int flags, mode_t mode);
/* STRICT_CONFINEMENT enforcement (vfs/secure_open.c): a build-time-gated CI
* harness that turns a confined-regime raw path metadata op into a hard failure.
* vfs_must_be_confined() is the predicate; vfs_strict_confine_fail() the hard
* stop, only compiled/called under STRICT_CONFINEMENT. */
int vfs_must_be_confined(const char *path, int is_operator);
#ifdef STRICT_CONFINEMENT
void vfs_strict_confine_fail(const char *path, const char *what);
#endif
/* Operator-supplied-path resolution by ownership (vfs/owner_walk.c). */
int vfs_open_owner_walk(const char *path, int flags, mode_t mode, int is_operator);
int vfs_owner_walk_parent(const char *path, const char **bname, int is_operator);
/* Held ancestor-dirfd cache for directory traversal (vfs/dircache.c). */
int vfs_opendir(const char *dirname);
int vfs_get_dirfd(const char *dirname);
int vfs_path_dirfd(const char *anchor, const char *dirpath);
int vfs_cached_dirfd(const char *path, const struct file_struct *file);
void vfs_dircache_reset(void);
/* stat/lstat/fstat (vfs/stat.c). */
int vfs_stat(int dirfd, const char *path, STRUCT_STAT *st, int flags);
int vfs_lstat(int dirfd, const char *path, STRUCT_STAT *st, int flags);
int vfs_fstat(int fd, STRUCT_STAT *st);
/* rename (vfs/rename.c). */
int vfs_rename(const char *old_path, const char *new_path);
int vfs_rename_at(const char *old_path, const char *new_path, int old_flags, int new_flags);
int vfs_rename_atfd(int old_dfd, const char *old_name, int new_dfd, const char *new_name);
/* unlink and rmdir (vfs/unlink.c). */
int vfs_unlink(int dirfd, const char *path, int flags);
/* open (vfs/open.c). */
int vfs_open(const char *pathname, int flags, mode_t mode);
int vfs_open_at(const char *pathname, int flags, mode_t mode, int vfs_flags);
int vfs_open_atfd(int dfd, const char *name, int flags, mode_t mode);
int vfs_open_nofollow(const char *pathname, int flags);
int vfs_open_checklinks(const char *pathname);
/* chmod (vfs/chmod.c). */
int vfs_chmod(int dirfd, const char *path, mode_t mode, int flags);
#ifdef HAVE_CHMOD
int vfs_fchmod(int fd, mode_t mode);
#endif
/* symlink/readlink (vfs/symlink.c). vfs_readlink is a function only in
* fake-super builds; otherwise it is a macro -> readlink() (see rsync.h). */
int vfs_symlink(const char *lnk, int dirfd, const char *path, int flags);
ssize_t vfs_readlink_atfd(int dfd, const char *name, char *buf, size_t bufsiz);
#if defined NO_SYMLINK_XATTRS || defined NO_SYMLINK_USER_XATTRS
ssize_t vfs_readlink(const char *path, char *buf, size_t bufsiz);
#endif
/* hard links (vfs/link.c). */
int vfs_link(const char *old_path, const char *new_path);
int vfs_link_at(const char *old_path, const char *new_path, int old_flags, int new_flags);
int vfs_link_atfd(int old_dfd, const char *old_name, int new_dfd, const char *new_name, int flags);
/* mkdir / mkstemp and the trim_trailing_slashes path helper (vfs/mkdir.c). */
void trim_trailing_slashes(char *name);
/* dirfd == VFS_AT_FDCWD: resolve `path` (secure parent walk unless
* VFS_ALLOW_SYMLINK / VFS_OPERATOR_PATH); a real held dirfd: `path` is a single
* component created directly under it. */
int vfs_mkdir(int dirfd, char *path, mode_t mode, int flags);
int vfs_mkstemp(char *template, mode_t perms);
int vfs_mkstemp_atfd(int dfd, char *filename, mode_t perms);
int vfs_secure_mkstemp(char *template, mode_t perms, int operator_path);
/* lchown (vfs/chown.c). */
int vfs_lchown(int dirfd, const char *path, uid_t owner, gid_t group, int flags);
int vfs_fchown(int fd, uid_t owner, gid_t group);
/* device/fifo/socket node creation (vfs/mknod.c). */
int vfs_mknod(int dirfd, const char *path, mode_t mode, dev_t dev, int flags);
/* timestamp setting + crtimes (vfs/times.c). */
int vfs_setattrlist_times(const char *path, STRUCT_STAT *stp);
int vfs_setattrlist_crtime(const char *path, time_t crtime);
time_t vfs_get_create_time(const char *path, STRUCT_STAT *stp);
int vfs_SetFileTime(const char *path, time_t crtime);
/* Compound operations (vfs/<op>.c): filesystem mechanics layered on the
* primitives above. The resolution policy travels as an explicit vfs_flags
* argument (VFS_OPERATOR_PATH for operator-supplied paths, else 0). */
int vfs_make_path(char *fname, int mkp_flags, int vfs_flags);
int copy_file(const char *source, const char *dest, int tmpfilefd, mode_t mode, int vfs_flags);
int robust_unlink(const char *fname, int vfs_flags);
int robust_rename(const char *from, const char *to, const char *partialptr,
int mode, struct file_struct *file);
int vfs_utimensat(const char *path, STRUCT_STAT *stp);
int vfs_utimensat_at(const char *path, STRUCT_STAT *stp);
int vfs_utimensat_atfd(int dfd, const char *name, STRUCT_STAT *stp);
#ifdef HAVE_FUTIMENS
int vfs_futimens(int fd, STRUCT_STAT *stp);
#endif
int vfs_lutimes(const char *path, STRUCT_STAT *stp);
int vfs_utimes(const char *path, STRUCT_STAT *stp);
int vfs_utime(const char *path, STRUCT_STAT *stp);
/* fd-based file-data ops (vfs/fileio.c). */
int vfs_ftruncate(int fd, OFF_T size);
OFF_T vfs_lseek(int fd, OFF_T offset, int whence);
OFF_T vfs_fallocate(int fd, OFF_T offset, OFF_T length);
int vfs_punch_hole(int fd, OFF_T pos, OFF_T len);
#endif /* RSYNC_VFS_H */
+98
View File
@@ -0,0 +1,98 @@
/*
* vfs/vfs_internal.h - private interface shared among the vfs/ sources (and,
* during the syscall.c -> vfs/ migration, by syscall.c itself). NOT for use by
* mainline rsync code; the public surface is vfs/vfs.h.
*
* 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.
*/
#ifndef RSYNC_VFS_INTERNAL_H
#define RSYNC_VFS_INTERNAL_H
/* Option/daemon globals the VFS internals read (defined in options.c /
* clientserver.c / syscall.c). Centralized here so each vfs/ source picks them
* up from one place rather than re-declaring them. */
extern int dry_run;
extern int am_root;
extern int am_sender;
extern int am_daemon;
extern int read_only;
extern int list_only;
extern int inplace;
extern int preallocate_files;
extern int sparse_files;
extern int preserve_perms;
extern int preserve_executability;
extern int open_noatime;
extern int copy_links;
extern int copy_unsafe_links;
extern int insecure_links;
extern int module_id;
extern char *confine_root; /* --confine-root, or NULL; see confinement_root() */
extern unsigned int confine_rootlen;
/* Dry-run / read-only guard macros shared by the syscall wrappers. */
#define RETURN_ERROR_IF(x,e) \
do { \
if (x) { \
errno = (e); \
return -1; \
} \
} while (0)
#define RETURN_ERROR_IF_RO_OR_LO RETURN_ERROR_IF(read_only || list_only, EROFS)
/* A NULL path reaching one of the path-forwarding wrappers is always a caller
* bug; reject it rather than forwarding NULL to libc. Also quiets the static
* analyzer's interprocedural nonnull false positives. */
#define RETURN_ERROR_IF_NULL(p) RETURN_ERROR_IF(!(p), EFAULT)
/* Module-confinement helpers (pure logic, always compiled). */
int path_has_dotdot_component(const char *path);
int abspath_outside_confinement(const char *abspath, int is_operator);
const char *vfs_fd_pin_tail(const char *p);
/* Per-operand parent resolver for the two-path ops (vfs_rename_at/vfs_link_at). */
int vfs_twopath_side(const char *path, int side_flags, const char **bname,
int *dfd_out, BOOL *owns, char *dirbuf, size_t dirbufsz);
#if defined(O_NOFOLLOW) && defined(O_DIRECTORY) && defined(AT_FDCWD)
#ifndef SECURE_OPEN_MAXSYMLINKS
#define SECURE_OPEN_MAXSYMLINKS 40
#endif
/* Max directory levels held open at once during a single resolve. The walk
* holds one fd per component, so depth is bounded by RLIMIT_NOFILE anyway; a
* fixed array (no malloc/realloc) keeps the stack simple and the static
* analyzer happy. Mirrors DPC_MAXDEPTH's fixed-cap approach. */
#define DS_MAXDEPTH 1024
/* The component-walk dirfd stack used by the secure resolver: a stack of the
* open dirfds from the anchor (index 0, borrowed) down to the current dir. */
struct dirstack {
int fds[DS_MAXDEPTH]; /* fds[0] = anchor (borrowed); fds[top] = current dir */
int top;
/* Absolute path of fds[top], maintained as we descend/pop, for the
* exclude-aware refusal (abspath_outside_confinement). Empty unless the
* caller seeds it with the anchor's absolute path; then a followed symlink
* that redirects the walk into a module-excluded dir is refused. */
char abspath[MAXPATHLEN];
};
int open_anchor_dirfd(const char *path);
int ds_init(struct dirstack *ds, int anchor);
void ds_free(struct dirstack *ds);
int ds_cur(struct dirstack *ds);
int ds_take(struct dirstack *ds);
int ds_descend(struct dirstack *ds, const char *part, int *hops);
int ds_walk_path(struct dirstack *ds, char *path, int *hops);
#endif /* O_NOFOLLOW && O_DIRECTORY && AT_FDCWD */
#endif /* RSYNC_VFS_INTERNAL_H */
+14 -13
View File
@@ -1154,7 +1154,7 @@ int set_xattr(const char *fname, const struct file_struct *file, const char *fna
#endif
&& access(fname, W_OK) < 0
&& (fd >= 0 ? fchmod(fd, (sxp->st.st_mode & CHMOD_BITS) | S_IWUSR)
: do_chmod_at(fname, (sxp->st.st_mode & CHMOD_BITS) | S_IWUSR)) == 0)
: vfs_chmod(VFS_AT_FDCWD, fname, (sxp->st.st_mode & CHMOD_BITS) | S_IWUSR, 0)) == 0)
added_write_perm = 1;
ndx = F_XATTR(file);
@@ -1166,7 +1166,7 @@ int set_xattr(const char *fname, const struct file_struct *file, const char *fna
if (fd >= 0)
fchmod(fd, sxp->st.st_mode);
else
do_chmod_at(fname, sxp->st.st_mode);
vfs_chmod(VFS_AT_FDCWD, fname, sxp->st.st_mode, 0);
}
return 0;
}
@@ -1177,7 +1177,7 @@ int set_xattr(const char *fname, const struct file_struct *file, const char *fna
if (fd >= 0)
fchmod(fd, sxp->st.st_mode);
else
do_chmod_at(fname, sxp->st.st_mode);
vfs_chmod(VFS_AT_FDCWD, fname, sxp->st.st_mode, 0);
}
return return_value;
}
@@ -1286,7 +1286,7 @@ int set_stat_xattr(const char *fname, struct file_struct *file, mode_t new_mode,
}
if (fd >= 0) {
if (do_fstat(fd, &fst) < 0) {
if (vfs_fstat(fd, &fst) < 0) {
rsyserr(FERROR_XFER, errno, "failed to re-stat %s",
full_fname(fname));
return -1;
@@ -1295,7 +1295,7 @@ int set_stat_xattr(const char *fname, struct file_struct *file, mode_t new_mode,
xst = fst; /* keep xst fully defined; st_mode=0 means "no stat xattr" */
xst.st_mode = 0;
}
} else if (x_lstat(fname, &fst, &xst) < 0) {
} else if (x_lstat(fname, &fst, &xst, 0) < 0) {
rsyserr(FERROR_XFER, errno, "failed to re-stat %s",
full_fname(fname));
return -1;
@@ -1317,7 +1317,7 @@ int set_stat_xattr(const char *fname, struct file_struct *file, mode_t new_mode,
if (fd >= 0)
fchmod(fd, mode);
else
do_chmod_at(fname, mode);
vfs_chmod(VFS_AT_FDCWD, fname, mode, 0);
}
if (!IS_DEVICE(fst.st_mode))
fst.st_rdev = 0; /* just in case */
@@ -1356,22 +1356,23 @@ int set_stat_xattr(const char *fname, struct file_struct *file, mode_t new_mode,
return 0;
}
int x_stat(const char *fname, STRUCT_STAT *fst, STRUCT_STAT *xst)
int x_stat(const char *fname, STRUCT_STAT *fst, STRUCT_STAT *xst, int vfs_flags)
{
/* Use the *_at variants so that on a daemon-no-chroot deployment
* the metadata read goes through a secure parent dirfd instead
* of bare path resolution. The *_at wrappers fall through to
* plain do_stat outside the daemon-no-chroot context, so this
* change is transparent for non-daemon use. */
int ret = do_stat_at(fname, fst);
* plain vfs_stat outside the daemon-no-chroot context, so this
* change is transparent for non-daemon use. vfs_flags carries the
* operator-path policy (VFS_OPERATOR_PATH for a backup-dir stat). */
int ret = vfs_stat(VFS_AT_FDCWD, fname, fst, vfs_flags);
if ((ret < 0 || get_stat_xattr(fname, -1, fst, xst) < 0) && xst)
xst->st_mode = 0;
return ret;
}
int x_lstat(const char *fname, STRUCT_STAT *fst, STRUCT_STAT *xst)
int x_lstat(const char *fname, STRUCT_STAT *fst, STRUCT_STAT *xst, int vfs_flags)
{
int ret = do_lstat_at(fname, fst);
int ret = vfs_lstat(VFS_AT_FDCWD, fname, fst, vfs_flags);
if ((ret < 0 || get_stat_xattr(fname, -1, fst, xst) < 0) && xst)
xst->st_mode = 0;
return ret;
@@ -1379,7 +1380,7 @@ int x_lstat(const char *fname, STRUCT_STAT *fst, STRUCT_STAT *xst)
int x_fstat(int fd, STRUCT_STAT *fst, STRUCT_STAT *xst)
{
int ret = do_fstat(fd, fst);
int ret = vfs_fstat(fd, fst);
if ((ret < 0 || get_stat_xattr(NULL, fd, fst, xst) < 0) && xst)
xst->st_mode = 0;
return ret;