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
117 changed files with 6240 additions and 7219 deletions

No files matched your search

+4 -5
View File
@@ -1,9 +1,5 @@
name: Lint GitHub Actions workflows
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
# Static-check the workflow YAML with rhysd/actionlint. Catches missing
# secrets, bad expressions, expression-type errors, unsupported runner
# images, and (via embedded shellcheck) common pitfalls in `run:` scripts.
@@ -18,7 +14,7 @@ on:
- '.github/actionlint.yaml'
- '.github/actionlint.yml'
pull_request:
types: [opened, synchronize, reopened]
types: [opened, synchronize, reopened, labeled]
paths:
- '.github/workflows/*.yml'
- '.github/actionlint.yaml'
@@ -29,6 +25,9 @@ permissions:
jobs:
actionlint:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: ubuntu-latest
name: actionlint
steps:
+6 -8
View File
@@ -1,9 +1,5 @@
name: Test rsync on AlmaLinux 8
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
# Older-LTS coverage on the Fedora/RHEL family to help with backporting
# security fixes. AlmaLinux 8 is the RHEL 8 rebuild and is the oldest
# active LTS in this family (RHEL 8 full support runs to 2029).
@@ -17,7 +13,7 @@ on:
- '.github/workflows/*.yml'
- '!.github/workflows/almalinux-8-build.yml'
pull_request:
types: [opened, synchronize, reopened]
types: [opened, synchronize, reopened, labeled]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/almalinux-8-build.yml'
@@ -26,6 +22,9 @@ on:
jobs:
test:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: ubuntu-latest
container:
image: almalinux:8
@@ -52,8 +51,7 @@ jobs:
attr libattr-devel acl libacl-devel \
zstd libzstd-devel \
lz4 lz4-devel \
xxhash xxhash-devel \
libidn2 libidn2-devel
xxhash xxhash-devel
alternatives --set python3 /usr/bin/python3.9
pip3 install commonmark
- name: configure
@@ -67,7 +65,7 @@ jobs:
# crtimes-not-supported skip matches the other Linux jobs;
# daemon-chroot-acl and proxy-response-line-too-long skip because
# the default (secure) transport opens no listening socket.
run: RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/linux.txt,@testsuite/skiplist/almalinux-8.txt make check
run: RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/linux.txt make check
- name: check (TCP daemon transport)
# Second run exercising the real loopback-TCP daemon path.
run: ./runtests.py --rsync-bin="$PWD/rsync" --use-tcp -j 8
+5 -6
View File
@@ -1,9 +1,5 @@
name: Build static rsync for Android
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
# Cross-compiles statically-linked rsync binaries with the Android NDK,
# suitable for dropping onto a phone (adb push / Termux) with no shared
# libraries. arm64-v8a covers all modern phones; armeabi-v7a covers older
@@ -20,7 +16,7 @@ on:
- '.github/workflows/*.yml'
- '!.github/workflows/android-static-build.yml'
pull_request:
types: [opened, synchronize, reopened]
types: [opened, synchronize, reopened, labeled]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/android-static-build.yml'
@@ -35,6 +31,9 @@ env:
jobs:
build:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: ubuntu-latest
name: ${{ matrix.abi }}
strategy:
@@ -80,7 +79,7 @@ jobs:
# checksums and its bundled zlib.
./configure --host=${{ matrix.triple }} --build=x86_64-pc-linux-gnu \
--enable-ipv6 \
--disable-zstd --disable-lz4 --disable-xxhash --disable-openssl --disable-idn \
--disable-zstd --disable-lz4 --disable-xxhash --disable-openssl \
--disable-iconv --disable-iconv-open \
--disable-acl-support --disable-xattr-support \
--disable-md2man --disable-roll-simd \
+6 -7
View File
@@ -1,9 +1,5 @@
name: rsync ASan+UBSan (clang)
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
on:
push:
branches: [ master ]
@@ -11,7 +7,7 @@ on:
- '.github/workflows/*.yml'
- '!.github/workflows/asan-build.yml'
pull_request:
types: [opened, synchronize, reopened]
types: [opened, synchronize, reopened, labeled]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/asan-build.yml'
@@ -25,6 +21,9 @@ on:
jobs:
asan:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: ubuntu-latest
name: rsync ASan+UBSan (clang)
env:
@@ -45,7 +44,7 @@ jobs:
- name: prep
run: |
sudo apt-get update
sudo apt-get install -y clang acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev libidn2-dev openssl
sudo apt-get install -y clang acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev openssl
echo "/usr/local/bin" >>"$GITHUB_PATH"
- name: configure
# -DNDEBUG builds as a shipped release does (assert() compiled out), so
@@ -58,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
+5 -6
View File
@@ -1,9 +1,5 @@
name: Coverage (Ubuntu)
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
on:
push:
branches: [ master ]
@@ -11,7 +7,7 @@ on:
- '.github/workflows/*.yml'
- '!.github/workflows/coverage.yml'
pull_request:
types: [opened, synchronize, reopened]
types: [opened, synchronize, reopened, labeled]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/coverage.yml'
@@ -21,6 +17,9 @@ on:
jobs:
coverage:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: ubuntu-latest
name: gcov coverage
steps:
@@ -30,7 +29,7 @@ jobs:
- name: prep
run: |
sudo apt-get update
sudo apt-get install -y acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev libidn2-dev python3-cmarkgfm openssl gcovr
sudo apt-get install -y acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev python3-cmarkgfm openssl gcovr
echo "/usr/local/bin" >>"$GITHUB_PATH"
- name: configure
run: ./configure --enable-coverage --with-rrsync
+10 -50
View File
@@ -1,9 +1,5 @@
name: Test rsync on Cygwin
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
on:
push:
branches: [ master ]
@@ -11,7 +7,7 @@ on:
- '.github/workflows/*.yml'
- '!.github/workflows/cygwin-build.yml'
pull_request:
types: [opened, synchronize, reopened]
types: [opened, synchronize, reopened, labeled]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/cygwin-build.yml'
@@ -20,6 +16,9 @@ on:
jobs:
test:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: windows-2022
name: Test rsync on Cygwin
steps:
@@ -27,49 +26,11 @@ jobs:
with:
fetch-depth: 0
- name: cygwin
run: choco install -y --no-progress cygwin cyg-get
- name: prep
run: |
$setup = Join-Path $Env:RUNNER_TEMP 'setup-x86_64.exe'
$sums = Join-Path $Env:RUNNER_TEMP 'cygwin-sha512.sum'
$packages = 'make,autoconf,automake,gcc-core,attr,libattr-devel,python39,python39-pip,libzstd-devel,liblz4-devel,libssl-devel,libxxhash0,libxxhash-devel,libidn2-devel'
Invoke-WebRequest https://cygwin.com/setup-x86_64.exe -OutFile $setup
Invoke-WebRequest https://cygwin.com/sha512.sum -OutFile $sums
$sum = Select-String -LiteralPath $sums -Pattern '^[0-9a-fA-F]{128}\s+\*?setup-x86_64\.exe$' | Select-Object -First 1
if (-not $sum) {
throw 'setup-x86_64.exe is missing from Cygwin sha512.sum'
}
$expected = ($sum.Line -split '\s+')[0]
$actual = (Get-FileHash -LiteralPath $setup -Algorithm SHA512).Hash
if ($actual -ine $expected) {
throw 'Cygwin setup SHA-512 mismatch'
}
$arguments = @(
'--quiet-mode',
'--no-desktop',
'--no-startmenu',
'--no-shortcuts',
'--root', 'C:\tools\cygwin',
'--local-package-dir', (Join-Path $Env:RUNNER_TEMP 'cygwin-packages'),
'--site', 'https://mirrors.kernel.org/sourceware/cygwin/',
'--packages', $packages
)
$install = Start-Process -FilePath $setup -ArgumentList $arguments -Wait -PassThru -NoNewWindow
if ($install.ExitCode -ne 0) {
exit $install.ExitCode
}
$bash = 'C:\tools\cygwin\bin\bash.exe'
if (-not (Test-Path -LiteralPath $bash)) {
throw 'Cygwin setup did not install bash'
}
& $bash -lc 'command -v make aclocal gcc python3 >/dev/null'
if ($LASTEXITCODE -ne 0) {
throw 'Cygwin setup did not install all required build tools'
}
echo 'C:/tools/cygwin/bin' >>$Env:GITHUB_PATH
cyg-get make autoconf automake gcc-core attr libattr-devel python39 python39-pip libzstd-devel liblz4-devel libssl-devel libxxhash0 libxxhash-devel
echo "C:/tools/cygwin/bin" >>$Env:GITHUB_PATH
- name: commonmark
run: bash -c 'python3 -mpip install --user commonmark'
- name: configure
@@ -85,9 +46,8 @@ jobs:
# (rsyncfns.py drives xattrs via getfattr/setfattr from the `attr`
# package installed above), verified on a real Cygwin host. The real
# chown/devices tests still skip (need root/mknod), as do the
# RESOLVE_BENEATH symlink-race tests. Cygwin runs non-root, so the
# namecvt empty-response regression can run despite its common root skip.
run: bash -c 'RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/cygwin.txt,-daemon-namecvt-empty-response make check'
# RESOLVE_BENEATH symlink-race tests.
run: bash -c 'RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/cygwin.txt make check'
- name: check (TCP daemon transport)
# Second run with daemon tests over a real loopback rsyncd; the default
# 'make check' above uses the secure stdio-pipe transport.
+5 -6
View File
@@ -1,9 +1,5 @@
name: Test fleettest harness
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
# Bitrot check for testsuite/fleettest.py (the developer fleet CI harness).
# fleettest is meant to be run by developers on a modern Ubuntu box, so this
# job runs only on ubuntu-latest: it stands up a one-host "fleet" of two
@@ -20,7 +16,7 @@ on:
- 'testsuite/skiplist/**'
- 'testsuite/skiplist-spec_test.py'
pull_request:
types: [opened, synchronize, reopened]
types: [opened, synchronize, reopened, labeled]
paths:
- 'testsuite/fleettest.py'
- '.github/workflows/fleettest.yml'
@@ -33,6 +29,9 @@ on:
jobs:
fleettest:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: ubuntu-latest
name: fleettest against localhost
steps:
@@ -43,7 +42,7 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y gcc g++ gawk autoconf automake \
acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev libidn2-dev \
acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev \
python3-cmarkgfm openssl rsync openssh-server
- name: set up ssh to localhost
run: |
+5 -6
View File
@@ -1,9 +1,5 @@
name: Test rsync on FreeBSD
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
on:
push:
branches: [ master ]
@@ -11,7 +7,7 @@ on:
- '.github/workflows/*.yml'
- '!.github/workflows/freebsd-build.yml'
pull_request:
types: [opened, synchronize, reopened]
types: [opened, synchronize, reopened, labeled]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/freebsd-build.yml'
@@ -20,6 +16,9 @@ on:
jobs:
test:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: ubuntu-latest
name: Test rsync on FreeBSD
steps:
@@ -35,7 +34,7 @@ jobs:
pkg install -y bash autotools m4 devel/xxhash zstd liblz4 python3 archivers/liblz4 git
run: |
freebsd-version
./configure --with-rrsync -disable-zstd --disable-md2man --disable-xxhash --disable-lz4 --disable-idn
./configure --with-rrsync -disable-zstd --disable-md2man --disable-xxhash --disable-lz4
make
./rsync --version
make check
+5 -6
View File
@@ -1,9 +1,5 @@
name: Test rsync on macOS
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
on:
push:
branches: [ master ]
@@ -11,7 +7,7 @@ on:
- '.github/workflows/*.yml'
- '!.github/workflows/macos-build.yml'
pull_request:
types: [opened, synchronize, reopened]
types: [opened, synchronize, reopened, labeled]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/macos-build.yml'
@@ -20,6 +16,9 @@ on:
jobs:
test:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: macos-latest
name: Test rsync on macOS
steps:
@@ -28,7 +27,7 @@ jobs:
fetch-depth: 0
- name: prep
run: |
brew install automake openssl xxhash zstd lz4 libidn2
brew install automake openssl xxhash zstd lz4
pip3 install --user --break-system-packages commonmark
echo "$(brew --prefix)/bin" >>"$GITHUB_PATH"
- name: configure
+5 -6
View File
@@ -1,9 +1,5 @@
name: Test rsync on NetBSD
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
on:
push:
branches: [ master ]
@@ -11,7 +7,7 @@ on:
- '.github/workflows/*.yml'
- '!.github/workflows/netbsd-build.yml'
pull_request:
types: [opened, synchronize, reopened]
types: [opened, synchronize, reopened, labeled]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/netbsd-build.yml'
@@ -20,6 +16,9 @@ on:
jobs:
test:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: ubuntu-latest
name: Test rsync on NetBSD
steps:
@@ -36,7 +35,7 @@ jobs:
ln -sf /usr/pkg/bin/python3.12 /usr/pkg/bin/python3
run: |
uname -a
./configure --with-rrsync --disable-zstd --disable-md2man --disable-xxhash --disable-lz4 --disable-idn
./configure --with-rrsync --disable-zstd --disable-md2man --disable-xxhash --disable-lz4
make
./rsync --version
make check
+5 -6
View File
@@ -1,9 +1,5 @@
name: Test rsync on OpenBSD
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
on:
push:
branches: [ master ]
@@ -11,7 +7,7 @@ on:
- '.github/workflows/*.yml'
- '!.github/workflows/openbsd-build.yml'
pull_request:
types: [opened, synchronize, reopened]
types: [opened, synchronize, reopened, labeled]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/openbsd-build.yml'
@@ -20,6 +16,9 @@ on:
jobs:
test:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: ubuntu-latest
name: Test rsync on OpenBSD
steps:
@@ -37,7 +36,7 @@ jobs:
uname -a
export AUTOCONF_VERSION=2.71
export AUTOMAKE_VERSION=1.16
./configure --with-rrsync --disable-zstd --disable-md2man --disable-xxhash --disable-lz4 --disable-idn
./configure --with-rrsync --disable-zstd --disable-md2man --disable-xxhash --disable-lz4
make
./rsync --version
# The flipper (symlink-race) tests are excluded on OpenBSD, as on the
+9 -7
View File
@@ -1,9 +1,5 @@
name: rsync scan-build (clang analyzer)
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
on:
push:
branches: [ master ]
@@ -11,7 +7,7 @@ on:
- '.github/workflows/*.yml'
- '!.github/workflows/scan-build.yml'
pull_request:
types: [opened, synchronize, reopened]
types: [opened, synchronize, reopened, labeled]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/scan-build.yml'
@@ -24,6 +20,9 @@ jobs:
# when a new finding appears. Pin both the analyzer (clang-18/clang-tools-18)
# and the runner (ubuntu-24.04, whose apt repos carry those packages).
gate-clang18:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: ubuntu-24.04
name: scan-build gate (clang-18, pinned)
steps:
@@ -33,7 +32,7 @@ jobs:
- name: prep
run: |
sudo apt-get update
sudo apt-get install -y clang-18 clang-tools-18 acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev libidn2-dev libpopt-dev openssl
sudo apt-get install -y clang-18 clang-tools-18 acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev libpopt-dev openssl
- name: configure (under scan-build)
# Run configure under scan-build so its analyzer compiler-wrapper is baked
# into the Makefile's $(CC); --disable-md2man avoids the doc toolchain.
@@ -65,6 +64,9 @@ jobs:
# gate bump -- without blocking merges. continue-on-error keeps a noisy or
# broken run from affecting the workflow's required status.
informational-latest:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: ubuntu-latest
name: scan-build (latest clang, informational)
continue-on-error: true
@@ -75,7 +77,7 @@ jobs:
- name: prep
run: |
sudo apt-get update
sudo apt-get install -y clang clang-tools acl libacl1-dev attr libattr1-dev liblz4-dev libidn2-dev libzstd-dev libxxhash-dev libpopt-dev openssl
sudo apt-get install -y clang clang-tools acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev libpopt-dev openssl
- name: configure (under scan-build)
run: scan-build ./configure --with-rrsync --disable-md2man
- name: scan-build (informational)
+5 -6
View File
@@ -1,9 +1,5 @@
name: Test rsync on Solaris
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
on:
push:
branches: [ master ]
@@ -11,7 +7,7 @@ on:
- '.github/workflows/*.yml'
- '!.github/workflows/solaris-build.yml'
pull_request:
types: [opened, synchronize, reopened]
types: [opened, synchronize, reopened, labeled]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/solaris-build.yml'
@@ -20,6 +16,9 @@ on:
jobs:
test:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: ubuntu-latest
name: Test rsync on Solaris
steps:
@@ -35,7 +34,7 @@ jobs:
pkg install bash automake gnu-m4 pkg://solaris/runtime/python-35 autoconf gcc git
run: |
uname -a
./configure --with-rrsync -disable-zstd --disable-md2man --disable-xxhash --disable-lz4 --disable-idn
./configure --with-rrsync -disable-zstd --disable-md2man --disable-xxhash --disable-lz4
make
./rsync --version
make check
+5 -7
View File
@@ -1,9 +1,5 @@
name: Test rsync on Ubuntu 22.04
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
# Older-LTS coverage to help with backporting security fixes. ubuntu-22.04
# is currently the oldest GitHub Actions runner image (20.04 was retired
# in April 2025).
@@ -15,7 +11,7 @@ on:
- '.github/workflows/*.yml'
- '!.github/workflows/ubuntu-22.04-build.yml'
pull_request:
types: [opened, synchronize, reopened]
types: [opened, synchronize, reopened, labeled]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/ubuntu-22.04-build.yml'
@@ -24,6 +20,9 @@ on:
jobs:
test:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: ubuntu-22.04
name: Test rsync on Ubuntu 22.04
steps:
@@ -32,8 +31,7 @@ jobs:
fetch-depth: 0
- name: prep
run: |
sudo apt-get update
sudo apt-get install acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev libidn2-dev python3-cmarkgfm openssl
sudo apt-get install acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev python3-cmarkgfm openssl
echo "/usr/local/bin" >>"$GITHUB_PATH"
- name: configure
run: ./configure --with-rrsync
+5 -7
View File
@@ -1,9 +1,5 @@
name: Test rsync on Ubuntu
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
on:
push:
branches: [ master ]
@@ -11,7 +7,7 @@ on:
- '.github/workflows/*.yml'
- '!.github/workflows/ubuntu-build.yml'
pull_request:
types: [opened, synchronize, reopened]
types: [opened, synchronize, reopened, labeled]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/ubuntu-build.yml'
@@ -20,6 +16,9 @@ on:
jobs:
test:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: ubuntu-latest
name: Test rsync on Ubuntu
steps:
@@ -28,8 +27,7 @@ jobs:
fetch-depth: 0
- name: prep
run: |
sudo apt-get update
sudo apt-get install acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev libidn2-dev python3-cmarkgfm openssl
sudo apt-get install acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev python3-cmarkgfm openssl
echo "/usr/local/bin" >>"$GITHUB_PATH"
- name: configure
run: ./configure --with-rrsync
+5 -7
View File
@@ -1,9 +1,5 @@
name: Test rsync version mixing on Ubuntu
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
# Runs the CURRENT test suite with two different rsync binaries: the freshly
# built ./rsync as the client/driver, and a committed OLD static binary
# (old_versions/rsync_<ver>) as the daemon / remote-shell peer. This exercises
@@ -32,7 +28,7 @@ on:
- '.github/workflows/*.yml'
- '!.github/workflows/ubuntu-version-mix.yml'
pull_request:
types: [opened, synchronize, reopened]
types: [opened, synchronize, reopened, labeled]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/ubuntu-version-mix.yml'
@@ -41,6 +37,9 @@ on:
jobs:
version-mix:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: ubuntu-latest
name: rsync version-mix
steps:
@@ -49,8 +48,7 @@ jobs:
fetch-depth: 0
- name: prep
run: |
sudo apt-get update
sudo apt-get install acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev libidn2-dev python3-cmarkgfm openssl
sudo apt-get install acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev python3-cmarkgfm openssl
echo "/usr/local/bin" >>"$GITHUB_PATH"
- name: configure
run: ./configure --with-rrsync
+5 -6
View File
@@ -1,9 +1,5 @@
name: Valgrind memcheck
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
on:
push:
branches: [ master ]
@@ -11,7 +7,7 @@ on:
- '.github/workflows/*.yml'
- '!.github/workflows/valgrind.yml'
pull_request:
types: [opened, synchronize, reopened]
types: [opened, synchronize, reopened, labeled]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/valgrind.yml'
@@ -21,6 +17,9 @@ on:
jobs:
memcheck:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: ubuntu-latest
timeout-minutes: 120
strategy:
@@ -37,7 +36,7 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y valgrind acl libacl1-dev attr libattr1-dev \
liblz4-dev libzstd-dev libxxhash-dev libidn2-dev python3-cmarkgfm openssl
liblz4-dev libzstd-dev libxxhash-dev python3-cmarkgfm openssl
echo "/usr/local/bin" >>"$GITHUB_PATH"
- name: configure
run: ./configure --with-rrsync --enable-debug
+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
-16
View File
@@ -114,16 +114,6 @@ checksums.
[4]: https://www.openssl.org/docs/man1.0.2/man3/crypto.html
## libidn2
The [libidn2 library][5] converts an internationalized domain name into the
IDNA A-label ("Punycode") form that a resolver understands. Installing this
development library lets rsync connect to a daemon whose name has non-ASCII
characters in it, and lets a daemon's "hosts allow" & "hosts deny" settings be
written the same way.
[5]: https://www.gnu.org/software/libidn/#libidn2
## Package summary
To help you get the libraries installed, here are some package install commands
@@ -141,7 +131,6 @@ like.
> sudo apt install -y libzstd-dev
> sudo apt install -y liblz4-dev
> sudo apt install -y libssl-dev
> sudo apt install -y libidn2-dev
Or run support/install_deps_ubuntu.sh
@@ -155,7 +144,6 @@ Or run support/install_deps_ubuntu.sh
> sudo yum -y install libzstd-devel
> sudo yum -y install lz4-devel
> sudo yum -y install openssl-devel
> sudo yum -y install libidn2-devel
> python3 -mpip install --user commonmark
- For Fedora 33:
@@ -166,7 +154,6 @@ Or run support/install_deps_ubuntu.sh
> sudo dnf -y install libzstd-devel
> sudo dnf -y install lz4-devel
> sudo dnf -y install openssl-devel
> sudo dnf -y install libidn2-devel
- For FreeBSD (this assumes that the python3 version is 3.7):
@@ -174,7 +161,6 @@ Or run support/install_deps_ubuntu.sh
> sudo pkg install -y xxhash
> sudo pkg install -y zstd
> sudo pkg install -y liblz4
> sudo pkg install -y libidn2
- For macOS:
@@ -183,7 +169,6 @@ Or run support/install_deps_ubuntu.sh
> brew install zstd
> brew install lz4
> brew install openssl
> brew install libidn2
- For Cygwin (with all cygwin programs stopped, run the appropriate setup program from a cmd shell):
@@ -192,7 +177,6 @@ Or run support/install_deps_ubuntu.sh
> setup-x86_64 --quiet-mode -P libzstd-devel
> setup-x86_64 --quiet-mode -P liblz4-devel
> setup-x86_64 --quiet-mode -P libssl-devel
> setup-x86_64 --quiet-mode -P libidn2-devel
Sometimes cygwin has commonmark packaged and sometimes it doesn't. Now that
its python38 has stabilized, you could install python38-commonmark. Or just
+45 -22
View File
@@ -18,10 +18,12 @@ CXXFLAGS=@CXXFLAGS@
EXEEXT=@EXEEXT@
LDFLAGS=@LDFLAGS@
LIBOBJDIR=lib/
AR=@AR@
ARFLAGS=cr
RANLIB=@RANLIB@
INSTALLCMD=@INSTALL@
INSTALLMAN=@INSTALL@
STRIP=@STRIP@
srcdir=@srcdir@
MKDIR_P=@MKDIR_P@
@@ -39,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
@@ -53,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) \
@@ -68,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:
@@ -81,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
@@ -117,8 +131,7 @@ install-ssl-daemon: stunnel-rsyncd.conf
install-all: install install-ssl-daemon
install-strip:
$(MAKE) install
$(STRIP) $(DESTDIR)$(bindir)/rsync$(EXEEXT)
$(MAKE) INSTALL_STRIP='-s' install
.PHONY: uninstall
uninstall:
@@ -143,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
@@ -204,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)
@@ -220,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)
@@ -233,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)
@@ -350,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
+28 -34
View File
@@ -146,29 +146,28 @@ following symlinks by design.
### The mechanism
Resolution of attacker-influenceable paths goes through `secure_relative_open()`,
`secure_relative_dirfd()`, and the `do_*_at()` wrappers in `syscall.c`, never a
raw `open()`/`rename()`/`chmod()` on a full path string. The principle is:
**trust the operator-named transfer root, and confine all resolution beneath
it**, rejecting escapes via `..` above the anchor, absolute symlinks, or
out-of-tree symlinks. `secure_relative_open()` opens the resolved endpoint with
the caller's requested access. `secure_relative_dirfd()` instead returns
traversal authority for `fchdir()` or an at-style operation on a known child;
it does not imply permission to enumerate the directory.
Resolution of attacker-influenceable paths goes through `secure_relative_open()`
and the `do_*_at()` wrappers in `syscall.c`, never a raw `open()`/`rename()`/
`chmod()` on a full path string. The principle is: **trust the operator-named
transfer root, and confine all resolution beneath it**, rejecting escapes via
`..` above the anchor, absolute symlinks, or out-of-tree symlinks.
`secure_relative_open()` resolves the parent directory by walking it one
component at a time on a stack of held directory fds, then operates on the final
component with an at-style call on the resulting directory fd.
For per-entry work the receiver and generator go one step further and hold the
parent directory open: `open_dir_secure()` resolves an entry's directory once
as traversal authority, `held_dfd_for()` caches that descriptor for the
duration of the entry, and every operation on the entry — `lstat`, the
temp-file `mkstemp`, the temp->final `rename`, `chmod`/`chown`/`utimes`,
`mkdir`, special-file and symlink creation, the delta-basis open, and the
recursive delete — runs through that one held fd via an `*at()` call
(`do_*_atfd()`). Because the descriptor is pinned to the directory inode, a
parent component flipped to a symlink *after* the open cannot redirect any of
those operations. The alternate-destination lookups are confined the same way
(`basis_link_stat()` in `generator.c` and `secure_basis_open()` in
`receiver.c`), so a peer-chosen `--link-dest`/`--compare-dest`/`--copy-dest`
basis index cannot reach an out-of-module file through a symlinked parent.
(via `secure_relative_open()`), `held_dfd_for()` caches that descriptor for the
duration of the entry, and every operation on the entry — `lstat`, the temp-file
`mkstemp`, the temp->final `rename`, `chmod`/`chown`/`utimes`, `mkdir`, special-
file and symlink creation, the delta-basis open, and the recursive delete — runs
through that one held fd via an `*at()` call (`do_*_atfd()`). Because the
descriptor is pinned to the directory inode, a parent component flipped to a
symlink *after* the open cannot redirect any of those operations. The alternate-
destination lookups are confined the same way (`basis_link_stat()` in
`generator.c` and `secure_basis_open()` in `receiver.c`), so a peer-chosen
`--link-dest`/`--compare-dest`/`--copy-dest` basis index cannot reach an
out-of-module file through a symlinked parent.
The sender's source-directory *enumeration* is confined the same way as its
content open. `send_directory()` opens each scanned directory through
@@ -191,17 +190,13 @@ symlink would otherwise introduce.
### Path resolution
`secure_relative_open()` resolves a path with a single portable mechanism on
every platform: a per-component walk on a stack of held directory fds. On
Linux, anchors and traversal components use
`O_PATH|O_DIRECTORY|O_NOFOLLOW`; other platforms retain the
`O_RDONLY|O_DIRECTORY` fallback. Descending into a real subdirectory pushes
its fd, a `..` pops back to the already-held parent (a pop at the anchor is
refused), and an in-tree directory symlink is followed by reading its target
and walking that off the same stack (absolute targets refused, symlink hops
bounded). A final directory endpoint is reopened with the caller's requested
flags. Thus `secure_opendir()` still receives a readable fd, while known-name
operations beneath a searchable but unreadable directory do not require
permission to list it.
every platform: a per-component walk on a stack of held directory fds. Each
component is opened relative to the held parent with `openat(parent_fd,
"component", O_NOFOLLOW)`; descending into a real subdirectory pushes its fd, a
`..` pops back to the already-held parent (a pop at the anchor is refused), and an
in-tree directory symlink is followed by reading its target and walking that off
the same stack (absolute targets refused, symlink hops bounded). The final
component is opened `O_NOFOLLOW`.
Because every component is opened relative to a *pinned* fd under `O_NOFOLLOW`,
and `..` is resolved by the held-fd stack rather than by the kernel, the walk is
@@ -240,9 +235,8 @@ chmod-ing through a raced leaf symlink.
influenced by the remote peer or by another local user, use a `do_*_at()`
wrapper (or `secure_relative_open()`), not a raw full-path syscall.
* When introducing a new operation, add a matching `do_<op>_at()` wrapper that
resolves a parent used only as at-style authority with
`secure_relative_dirfd()`. Use `secure_relative_open()` when the returned fd
itself must be readable or otherwise support the caller's requested access.
resolves the parent with `secure_relative_open()` and acts via an at-style call
on the returned dirfd.
* Do not assume a non-daemon transfer is safe; the question is whether rsync has
more authority than whoever controls the path components.
* On platforms whose API lacks an at-style equivalent (e.g. `setattrlist()`),
-11
View File
@@ -33,9 +33,6 @@ static int match_hostname(const char **host_ptr, const char *addr, const char *t
struct hostent *hp;
unsigned int i;
const char *host = *host_ptr;
#ifdef SUPPORT_IDN
char idn_tok[1024];
#endif
if (!host || !*host)
return 0;
@@ -45,14 +42,6 @@ static int match_hostname(const char **host_ptr, const char *addr, const char *t
return innetgr(tok + 1, host, NULL, NULL);
#endif
#ifdef SUPPORT_IDN
/* A hostname reaches us from DNS as ASCII, so fold an IDN token to its
* A-label form before comparing. An all-ASCII token, and a token we
* can't fold, are both left as they are. */
if (idn_to_ascii(tok, 0, idn_tok, sizeof idn_tok))
tok = idn_tok;
#endif
/* First check if the reverse-DNS-determined hostname matches. */
if (iwildmatch(tok, host))
return 1;
+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;
}
+8 -8
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,24 +259,24 @@ 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));
exit_cleanup(RERR_FILEIO);
}
/* --read-batch: the file's bytes drive the protocol parser,
* allow FIFOs used by shell process substitution while continuing to reject other non-regular inputs. */
/* --read-batch: the file's bytes drive the protocol parser, so refuse
* 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) && !S_ISFIFO(st.st_mode)) {
rprintf(FERROR, "Batch file %s is neither a regular file nor a FIFO\n",
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 {
+11 -25
View File
@@ -13,7 +13,7 @@ AC_CHECK_HEADERS(poll.h sys/fcntl.h sys/select.h fcntl.h sys/time.h sys/unistd.h
sys/acl.h acl/libacl.h attr/xattr.h sys/xattr.h sys/extattr.h dl.h \
popt.h popt/popt.h linux/falloc.h netinet/in_systm.h netgroup.h \
zlib.h xxhash.h openssl/md4.h openssl/md5.h zstd.h lz4.h sys/file.h \
sys/resource.h bsd/string.h idn2.h)
sys/resource.h bsd/string.h)
AC_CHECK_HEADERS([netinet/ip.h], [], [], [[#include <netinet/in.h>]])
AC_HEADER_MAJOR_FIXED
@@ -59,8 +59,9 @@ AC_PROG_CXX
AC_PROG_AWK
AC_PROG_EGREP
AC_PROG_INSTALL
AC_CHECK_TOOL([STRIP], [strip], [strip])
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])
@@ -105,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)
@@ -372,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
@@ -627,27 +634,6 @@ else
AC_MSG_RESULT(no)
fi
AC_MSG_CHECKING([whether to enable IDN support])
AC_ARG_ENABLE([idn],
AS_HELP_STRING([--disable-idn], [disable to omit IDN (Internationalized Domain Name) support]))
AH_TEMPLATE([SUPPORT_IDN],
[Undefine if you do not want IDN support. By default this is defined.])
if test x"$enable_idn" != x"no"; then
if test x"$ac_cv_header_idn2_h" = x"yes"; then
AC_MSG_RESULT(yes)
AC_SEARCH_LIBS(idn2_lookup_ul, idn2,
[AC_DEFINE(SUPPORT_IDN)],
[err_msg="$err_msg$nl- Failed to find idn2_lookup_ul function in idn2 lib.";
no_lib="$no_lib idn"])
else
AC_MSG_RESULT(no)
err_msg="$err_msg$nl- Failed to find idn2.h for IDN support."
no_lib="$no_lib idn"
fi
else
AC_MSG_RESULT(no)
fi
if test x"$no_lib" != x; then
echo ""
echo "Configure found the following issues:"
@@ -1564,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_dirfd(NULL, dir)) < 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))
+7 -33
View File
@@ -117,13 +117,6 @@ static time_t last_io_out;
* transfer timeout and may be supplied by the module or client. */
static time_t daemon_handshake_deadline;
/* Wall-clock bound the client puts on establishing a daemon connection made
* through a remote shell (daemon_connection == 1): spawning the helper, its
* connect()/TLS handshake, and the exchange of the daemon greeting all happen
* before any buffered I/O begins, so --contimeout can time the whole phase the
* same way the socket path times its connect(). */
static time_t client_connect_deadline;
static int write_batch_monitor_in = -1;
static int write_batch_monitor_out = -1;
@@ -153,28 +146,17 @@ static int handshake_poll_timeout_ms(void)
time_t now, left;
int timeout = poll_timeout_ms();
if (!daemon_handshake_deadline && !client_connect_deadline)
if (!daemon_handshake_deadline)
return timeout;
now = time(NULL);
if (daemon_handshake_deadline) {
left = daemon_handshake_deadline - now;
if (left <= 0) {
rprintf(FERROR, "[%s] daemon handshake timeout -- exiting\n", who_am_i());
exit_cleanup(RERR_TIMEOUT);
}
if (left <= INT_MAX / 1000 && left * 1000 < timeout)
timeout = (int)left * 1000;
}
if (client_connect_deadline) {
left = client_connect_deadline - now;
if (left <= 0) {
rprintf(FERROR, "[%s] connection timed out -- exiting\n", who_am_i());
exit_cleanup(RERR_CONTIMEOUT);
}
if (left <= INT_MAX / 1000 && left * 1000 < timeout)
timeout = (int)left * 1000;
left = daemon_handshake_deadline - now;
if (left <= 0) {
rprintf(FERROR, "[%s] daemon handshake timeout -- exiting\n", who_am_i());
exit_cleanup(RERR_TIMEOUT);
}
if (left <= INT_MAX / 1000 && left * 1000 < timeout)
timeout = (int)left * 1000;
return timeout;
}
@@ -1321,14 +1303,6 @@ void set_daemon_handshake_timeout(int secs)
daemon_handshake_deadline = 0;
}
void set_client_connect_timeout(int secs)
{
if (secs > 0)
client_connect_deadline = time(NULL) + secs;
else
client_connect_deadline = 0;
}
static void check_for_d_option_error(const char *msg)
{
static const char rsync263_opts[] = "BCDHIKLPRSTWabceghlnopqrtuvxz";
+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);
+18 -37
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;
@@ -521,16 +519,6 @@ static pid_t do_cmd(char *cmd, char *machine, char *user, char **remote_argv, in
char *args[MAX_ARGS], *need_to_free = NULL;
pid_t pid;
int dash_l_set = 0;
#ifdef SUPPORT_IDN
char idn_machine[1024];
/* A daemon-over-remote-shell host is ours to resolve, so give the helper
* the A-label form. A "host:path" transfer is left alone because that
* name belongs to the user's ssh, which may be matching it against an
* ssh_config Host pattern. */
if (machine && daemon_connection > 0 && idn_to_ascii(machine, 1, idn_machine, sizeof idn_machine))
machine = idn_machine;
#endif
if (!read_batch && !local_server) {
char *t, *f, in_quote = '\0';
@@ -747,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)) {
@@ -764,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;
}
@@ -811,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));
@@ -850,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
@@ -872,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++) {
@@ -887,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
@@ -901,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);
@@ -1035,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);
@@ -1055,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);
@@ -1612,7 +1600,7 @@ static int start_client(int argc, char *argv[])
exit_cleanup(RERR_SYNTAX);
}
if (connect_timeout && !daemon_connection) {
if (connect_timeout) {
rprintf(FERROR, "The --contimeout option may only be "
"used when connecting to an rsync daemon.\n");
exit_cleanup(RERR_SYNTAX);
@@ -1644,14 +1632,6 @@ static int start_client(int argc, char *argv[])
(void)env_port;
#endif
/* For a daemon reached through a remote shell, the "connection" rsync
* waits on is: the helper is spawned, it establishes its own link (e.g.
* rsync-ssl's openssl connect + TLS handshake), and the daemon greeting is
* exchanged. Bound that whole phase with --contimeout so the option
* behaves for daemon-via-rsh the way it does for a socket connection. */
if (daemon_connection && connect_timeout > 0)
set_client_connect_timeout(connect_timeout);
pid = do_cmd(shell_cmd, shell_machine, shell_user, remote_argv, remote_argc, &f_in, &f_out);
/* if we're running an rsync server on the remote host over a
@@ -1659,7 +1639,6 @@ static int start_client(int argc, char *argv[])
if (daemon_connection) {
int tmpret;
tmpret = start_inband_exchange(f_in, f_out, shell_user, remote_argc, remote_argv);
set_client_connect_timeout(0);
if (tmpret < 0)
return tmpret;
}
@@ -1801,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.
@@ -1833,6 +1812,8 @@ int main(int argc,char *argv[])
raw_argc = argc;
raw_argv = argv;
vfs_init();
raise_fd_limit();
#ifdef HAVE_SIGACTION
+18 -31
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},
@@ -1157,12 +1156,6 @@ static int count_args(const char **argv)
return i;
}
/* The largest value parse_size_arg() will accept when no explicit max_value is
* given. It is SIZE_MAX/2 rather than SIZE_MAX because the parser computes and
* returns the size as a signed ssize_t (with a negative return meaning error),
* so this keeps every accepted size representable as a positive ssize_t. */
#define SIZE_ARG_MAX ((ssize_t)(SIZE_MAX / 2))
/* If the size_arg is an invalid string or the value is < min_value, an error
* is put into err_buf & the return is -1. Note that this parser does NOT
* support negative numbers, so a min_value < 0 doesn't make any sense. */
@@ -1172,7 +1165,7 @@ static ssize_t parse_size_arg(const char *size_arg, char def_suf, const char *op
int reps, mult, len;
const char *arg, *err = "invalid", *min_max = NULL;
ssize_t limit = -1, size = 1;
ssize_t size_max = max_value >= 0 ? max_value : SIZE_ARG_MAX;
ssize_t size_max = max_value >= 0 ? max_value : (ssize_t)(SIZE_MAX / 2);
double dsize;
for (arg = size_arg; isDigit(arg); arg++) {}
@@ -2073,17 +2066,14 @@ int parse_arguments(int *argc_p, const char ***argv_p)
ssize_t size = parse_size_arg(max_alloc_arg, 'B', "max-alloc", 1024*1024, -1, True);
if (size < 0)
goto cleanup;
if (size == 0) {
snprintf(err_buf, sizeof err_buf, "max-alloc must be greater than zero\n");
goto cleanup;
}
max_alloc = size;
}
/* A 0 value means "as large as this build allows". We resolve it to the
* same ceiling parse_size_arg() enforces, so that --max-alloc=0 is exactly
* the largest value a user could also have typed, and never a limit that
* only the 0 spelling can reach. Note that max_alloc_arg is forwarded to
* the peer un-normalized (see server_options()), which is what lets each
* side resolve 0 against its own SIZE_MAX -- a 64-bit client and a 32-bit
* daemon each get their own ceiling from the one portable spelling. */
if (!max_alloc)
max_alloc = SIZE_ARG_MAX;
max_alloc = SIZE_MAX;
if (old_style_args < 0) {
if (!am_server && protect_args <= 0 && (arg = getenv("RSYNC_OLD_ARGS")) != NULL && *arg) {
@@ -2187,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);
}
@@ -2381,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);
@@ -2654,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",
@@ -3028,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();
+4 -15
View File
@@ -226,20 +226,9 @@ if [[ "$1" == --HELPER ]]; then
rsync_ssl_helper "${@}"
fi
args=()
dash_dash_seen=false
for arg in "$@"; do
if [[ $dash_dash_seen == true ]]; then
args+=("$arg")
elif [[ "$arg" == "--" ]]; then
args+=("$arg")
dash_dash_seen=true
elif [[ "$arg" == --type=* ]]; then
export RSYNC_SSL_TYPE="${arg#--type=}"
else
args+=("$arg")
fi
done
set -- "${args[@]}"
if [[ "$1" == --type=* ]]; then
export RSYNC_SSL_TYPE="${1/--type=/}"
shift
fi
rsync_ssl_run "${@}"
+2 -5
View File
@@ -25,15 +25,12 @@ rsync version to be at least 3.2.0.
## OPTIONS
If an arg is a `--type=SSL_TYPE` option, the script will only use
If the **first** arg is a `--type=SSL_TYPE` option, the script will only use
that particular program to open an ssl connection instead of trying to find an
openssl or stunnel executable via a simple heuristic (assuming that the
`RSYNC_SSL_TYPE` environment variable is not set as well -- see below). This
option must specify one of `openssl` or `stunnel`. The equal sign is
required for this particular option. The wrapper's option scan stops at a
`--` argument: the `--` and everything after it are passed through to rsync
unchanged, so a `--type=...` token after a `--` is not consumed by the
wrapper.
required for this particular option.
All the other options are passed through to the rsync command, so consult the
**rsync**(1) manpage for more information on how it works.
+1 -10
View File
@@ -1,12 +1,3 @@
REGARDING OPENSSL AND XXHASH
In addition, as a special exception, the copyright holders give
permission to dynamically link rsync with the OpenSSL and xxhash
libraries when those libraries are being distributed in compliance
with their license terms, and to distribute a dynamically linked
combination of rsync and these libraries. This is also considered
to be covered under the GPL's System Libraries exception.
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
@@ -680,4 +671,4 @@ into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
&lt;<a href="https://www.gnu.org/licenses/why-not-lgpl.html">https://www.gnu.org/licenses/why-not-lgpl.html</a>&gt;.
&lt;<a href="https://www.gnu.org/philosophy/why-not-lgpl.html">https://www.gnu.org/philosophy/why-not-lgpl.html</a>&gt;.
+16 -47
View File
@@ -300,8 +300,7 @@ entirely. See the [SYMBOLIC LINKS](#) section for how these interact.
Separately, the directory and file paths that *you* supply on the command line --
[`--backup-dir`](#opt), [`--temp-dir`](#opt), [`--partial-dir`](#opt), the
[`--link-dest`](#opt)/[`--compare-dest`](#opt)/[`--copy-dest`](#opt) basis directories,
[`--log-file`](#opt), [`--password-file`](#opt),
[`--files-from`](#opt)/`--include-from`/`--exclude-from`,
[`--log-file`](#opt), [`--files-from`](#opt)/`--include-from`/`--exclude-from`,
[`--filter`](#opt) merge files, [`--write-batch`](#opt)/[`--read-batch`](#opt),
and the destination itself -- are resolved so that a symlink component is followed
only when it is owned by you or by root; an attacker-planted symlink along one of
@@ -413,20 +412,6 @@ when scripting rsync.
WARNING: On some systems, environment variables are visible to all users. On
those systems using [`--password-file`](#opt) is recommended.
If rsync was built with IDN support (look for "IDN" in `rsync --version`), the
daemon host may contain non-ASCII characters: those labels are converted to
their IDNA A-label ("Punycode") form before the name is looked up. The name is
read using your locale's character encoding, so be sure your locale is set
correctly. A name typed with combining characters is normalized on the way, so
it is looked up the same as its precomposed spelling. Only the non-ASCII
labels change, so an address literal, a name you punycoded yourself, and a name
that is not a valid IDN are all looked up just as you typed them.
This applies to the host of a daemon connection only. The host of a plain
remote-shell transfer (the single-colon syntax) is passed to your remote-shell
program unchanged, since that name may well be an ssh_config "Host" alias
rather than a DNS name.
You may establish the connection via a web proxy by setting the environment
variable [`RSYNC_PROXY`](#) to a hostname:port pair pointing to your web proxy.
Note that your web proxy's configuration must support proxy connections to port
@@ -1756,20 +1741,20 @@ sign) if you want the local shell to expand it.
0. `--devices`
This option causes rsync to transfer character and block device files to
the remote system to recreate these devices. This option has no effect if
the receiving rsync is not run as the super-user and neither
[`--super`](#opt) nor [`--fake-super`](#opt) is in effect.
the remote system to recreate these devices. If the receiving rsync is not
being run as the super-user, rsync silently skips creating the device files
(see also the [`--super`](#opt) and [`--fake-super`](#opt) options).
When a device file is not created, rsync generates the usual "non-regular
file" warning. You can silence the warning by specifying
[`--info=nonreg0`](#opt).
By default, rsync generates a "non-regular file" warning for each device
file encountered when this option is not set. You can silence the warning
by specifying [`--info=nonreg0`](#opt).
0. `--specials`
This option causes rsync to transfer special files, such as named sockets
and fifos. Creating these files does not normally require super-user
privileges. Use [`--drop-D`](#opt) to make the receiving rsync refuse to
create them regardless of what the transfer requested.
and fifos. If the receiving rsync is not being run as the super-user,
rsync silently skips creating the special files (see also the
[`--super`](#opt) and [`--fake-super`](#opt) options).
By default, rsync generates a "non-regular file" warning for each special
file encountered when this option is not set. You can silence the warning
@@ -2340,22 +2325,12 @@ sign) if you want the local shell to expand it.
See the [`--max-size`](#opt) option for a description of how SIZE can be
specified. The default suffix if none is given is bytes.
A value of 0 is an easy way to say "the largest limit this build supports".
It resolves to the same ceiling an explicit SIZE is checked against, so it
is never a higher limit than one you could have typed out yourself.
Because the option is passed to the remote rsync as you wrote it, each side
resolves a 0 against its own maximum. That makes 0 the only spelling that
is correct for both ends of a transfer between hosts of different word
sizes: a literal value large enough to be useful on a 64-bit client is
rejected as too large by a 32-bit daemon.
A value of 0 was accepted beginning in 3.2.3 and rejected in 3.5.0; the
release after 3.5.0 accepts it again.
A daemon administrator who does not want clients to change the configured
allocation ceiling can set `refuse options = max-alloc` in the module's
`rsyncd.conf`. This refuses every client-supplied value, including 0.
Beginning in 3.2.7, a value of 0 was an easy way to specify SIZE_MAX (the
largest limit possible). However, beginning with 3.5.0, a value of 0 is
rejected as invalid for security reasons (a 0-byte cap could be used to
disable the allocation limit, which could lead to a denial-of-service via
memory exhaustion). Use an explicit very large value if you want a very
high limit.
You can set a default value using the environment variable
[`RSYNC_MAX_ALLOC`](#) using the same SIZE values as supported by this
@@ -2635,12 +2610,6 @@ sign) if you want the local shell to expand it.
options are parsed (e.g. [`-a`](#opt) works the same before or after
`--files-from`, as does `--no-R` and all other options).
Listing individual files with `--files-from` does not make unlisted
siblings eligible for deletion. The [`--delete`](#opt) option only removes
entries from directories whose complete contents are being synchronised,
so list the directory itself and enable recursion if that deletion scope is
intended.
The filenames that are read from the FILE are all relative to the source
directory: any leading slash is removed, and ".." components are resolved away so
an entry cannot rise above the source directory -- e.g. "../foo" is taken as "foo"
+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
-14
View File
@@ -853,20 +853,6 @@ in the values of parameters. See that section for details.
- an '@' followed by a netgroup name, which will match if the reverse DNS
of the connecting IP is in the specified netgroup.
If rsync was built with IDN support (look for "IDN" in `rsync --version`),
a hostname pattern may contain non-ASCII characters: this file is read as
UTF-8, and each non-ASCII label is converted to its IDNA A-label
("Punycode") form before matching, since the name the daemon has for a
client always reaches it from DNS as ASCII. A pattern written with
combining characters is normalized on the way, so it matches the same as
its precomposed spelling. Only those labels change, so an address, a
mask, an already-punycoded name, and the wildcard characters are all
matched exactly as written. A pattern that cannot be converted that way
is matched as it stands, and thus matches nothing. That includes a
pattern whose conversion would have introduced a character it was not
written with, such as the U+FF0A FULLWIDTH ASTERISK that the IDNA mapping
turns into a "*".
Note IPv6 link-local addresses can have a scope in the address
specification:
+35 -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,20 +127,21 @@ static int secure_sender_parent_fd(struct file_struct *file, const char *fname,
#endif
while (*rel == '/')
rel++;
return secure_relative_dirfd("/", 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_dirfd(NULL, dir);
return vfs_resolve_open(NULL, dir, O_RDONLY | O_DIRECTORY, 0);
}
errno = 0; /* top-level file: no parent component to confine */
return -1;
@@ -175,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_dirfd(module_dir, dir);
dfd = vfs_resolve_open(module_dir, dir, O_RDONLY | O_DIRECTORY, 0);
} else
dfd = secure_relative_dirfd(module_dir, "");
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. */
@@ -194,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
@@ -236,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
}
@@ -252,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. */
@@ -291,12 +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_dirfd_at_beneath(module_dirfd, dir);
pdfd = vfs_resolve_open_at_beneath(module_dirfd, dir,
O_RDONLY | O_DIRECTORY, 0);
else
pdfd = secure_relative_dirfd(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
@@ -324,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
}
@@ -421,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;
}
@@ -447,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)
@@ -644,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
@@ -668,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++;
@@ -701,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) {
@@ -722,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);
-8
View File
@@ -343,14 +343,6 @@ int open_socket_out(char *host, int port, const char *bind_addr, int af_hint)
int proxied = 0;
char buffer[1024];
char *proxy_user = NULL, *proxy_pass = NULL;
#ifdef SUPPORT_IDN
char idn_host[1024];
/* The resolver only speaks ASCII, so an IDN host goes out as A-labels.
* An all-ASCII host is passed along untouched. */
if (idn_to_ascii(host, 1, idn_host, sizeof idn_host))
host = idn_host;
#endif
/* if we have a RSYNC_PROXY env variable then redirect our
* connection via a web proxy at the given address. */
+1 -8
View File
@@ -57,14 +57,7 @@ def main():
for fn in files:
if args.prefix:
fn = args.prefix + fn
try:
mtime = os.lstat(fn).st_mtime
except FileNotFoundError:
# Tracked in git but absent from the tree we are stamping.
# export-ignore in .gitattributes keeps rsync-web/ and the
# old_versions/ binaries out of "git archive", so they have
# no file here to give a commit time to.
continue
mtime = os.lstat(fn).st_mtime
if args.list:
print_line(fn, mtime, commit_time)
elif mtime != commit_time:
-1
View File
@@ -9,4 +9,3 @@ sudo apt install -y libxxhash-dev
sudo apt install -y libzstd-dev
sudo apt install -y liblz4-dev
sudo apt install -y libssl-dev
sudo apt install -y libidn2-dev
-4062
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)
+137 -61
View File
@@ -16,16 +16,30 @@
# *symlink* components. The fix sanitizes the wire xname itself (for basis
# types only, leaving the hard-link "=> target" xname alone).
#
# Test: build an instrumented rsync (env-gated sender.c edit that, when
# Test: build an instrumented daemon-sender (env-gated sender.c edit that, when
# RSYNC_MAL_XNAME is set, injects ITEM_XNAME_FOLLOWS|ITEM_BASIS_TYPE_FOLLOWS +
# fnamecmp_type=FNAMECMP_FUZZY+1 (== basis_dir[0]) + xname onto each transfer).
# An env-gated receiver.c edit records the exact basedir and relpath passed to
# secure_basis_open(). This observes the security decision directly without
# relying on timing-sensitive FIFO rendezvous behaviour across operating systems.
# fnamecmp_type=FNAMECMP_FUZZY+1 (== basis_dir[0]) + xname onto each transfer),
# run it via RSYNC_CONNECT_PROG with the production rsync as the receiver pulling
# with --link-dest, and observe where the receiver opens the basis.
#
# Two FIFOs, each with a helper blocked in open(O_WRONLY) that drops a flag when
# some reader opens it, tell RED from GREEN without hanging the receiver (it
# reads EOF and finishes):
# * ESCAPE base/secret reached only by an unsanitized "../secret"
# * DECOY linkdest/secret where the SANITIZED "secret" lands
# Injected xname is "../secret":
# - vulnerable receiver opens ESCAPE -> escape flag -> FAIL (traversal)
# - fixed receiver sanitizes to "secret", opens DECOY -> decoy flag -> PASS
# (the decoy flag also proves the crafted xname actually crossed the wire,
# so a stale/failed injection build can't false-PASS as "confined")
# - neither flag -> the injection never took effect -> FAIL (vacuous)
import os
import shlex
import subprocess
import time
from pathlib import Path
import sys
from rsyncfns import (
SCRATCHDIR, build_patched_rsync, forced_protocol, makepath, rmtree,
@@ -39,7 +53,11 @@ from rsyncfns import (
_proto = forced_protocol()
if _proto is not None and _proto < 29:
test_skipped("basis-xname-traversal: xname/item flags need protocol >= 29")
# -- Build the instrumented peer (shared helper: Cygwin skip, CCACHE_DISABLE,
if not hasattr(os, 'mkfifo'):
test_skipped("basis-xname-traversal: os.mkfifo unavailable on this platform")
# -- Build the instrumented sender (shared helper: Cygwin skip, CCACHE_DISABLE,
# forced rebuild of the patched unit) -------------------------------------
PATCH_OLD = ("\t\twrite_ndx_and_attrs(f_out, ndx, iflags, fname, file, fnamecmp_type, xname, xlen);\n"
"\t\twrite_sum_head(f_xfer, s);")
@@ -50,32 +68,14 @@ PATCH_NEW = ("\t\tif (getenv(\"RSYNC_MAL_XNAME\")) { /* basis-xname-traversal Po
"\t\t}\n"
"\t\twrite_ndx_and_attrs(f_out, ndx, iflags, fname, file, fnamecmp_type, xname, xlen);\n"
"\t\twrite_sum_head(f_xfer, s);")
TRACE_OLD = ("static int secure_basis_open(const char *basedir, const char *relpath, int flags, mode_t mode)\n"
"{\n"
"\textern int am_daemon, am_chrooted;")
TRACE_NEW = ("static int secure_basis_open(const char *basedir, const char *relpath, int flags, mode_t mode)\n"
"{\n"
"\tconst char *trace_path = getenv(\"RSYNC_BASIS_TRACE\");\n"
"\tif (trace_path) {\n"
"\t\tFILE *trace = fopen(trace_path, \"a\");\n"
"\t\tif (trace) {\n"
"\t\t\tfprintf(trace, \"%s\\t%s\\n\", basedir ? basedir : \"\", relpath);\n"
"\t\t\tfclose(trace);\n"
"\t\t}\n"
"\t}\n"
"\textern int am_daemon, am_chrooted;")
mal_rsync = build_patched_rsync(
'mal-xname-rsync',
[('sender.c', PATCH_OLD, PATCH_NEW),
('receiver.c', TRACE_OLD, TRACE_NEW)],
)
mal_rsync = build_patched_rsync('mal-xname-rsync', [('sender.c', PATCH_OLD, PATCH_NEW)])
# -- Workspace ----------------------------------------------------------------
# base/serversrc/file the file the instrumented daemon offers
# base/linkdest/ the client's --link-dest (basis_dir[0])
# base/linkdest/secret where a sanitized "secret" resolves
# base/secret where an unsanitized "../secret" resolves
# base/linkdest/secret DECOY fifo -- where a sanitized "secret" resolves
# base/secret ESCAPE fifo -- where an unsanitized "../secret" lands
# base/dest/ the client's destination
base = SCRATCHDIR / 'xname-race'
rmtree(base)
@@ -84,57 +84,133 @@ linkdest = base / 'linkdest'
dest = base / 'dest'
escape = base / 'secret' # linkdest/../secret
decoy = linkdest / 'secret' # linkdest/secret
trace_file = base / 'basis.trace'
esc_flag = base / 'escape.flag'
dec_flag = base / 'decoy.flag'
makepath(serversrc)
makepath(linkdest)
makepath(dest)
(serversrc / 'file').write_text("from the server\n")
escape.write_text("escaped basis\n")
decoy.write_text("confined basis\n")
conf = write_daemon_conf(
[('m', {'path': str(serversrc), 'read only': 'yes', 'use chroot': 'no'})],
name='mal-xname-rsyncd.conf')
os.environ['RSYNC_CONNECT_PROG'] = f'{shlex.quote(str(mal_rsync))} --config={shlex.quote(str(conf))} --daemon'
os.environ['RSYNC_MAL_XNAME'] = '../secret' # from basis_dir[0] == linkdest
os.environ['RSYNC_BASIS_TRACE'] = str(trace_file)
try:
argv = rsync_argv('-a', f'--link-dest={linkdest}',
'rsync://localhost/m/file', str(dest) + '/')
argv[0] = str(mal_rsync)
proc = subprocess.run(
argv,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=120)
finally:
os.environ.pop('RSYNC_BASIS_TRACE', None)
os.environ.pop('RSYNC_MAL_XNAME', None)
os.environ.pop('RSYNC_CONNECT_PROG', None)
# A helper that blocks in open(fifo, O_WRONLY) until some reader opens the FIFO,
# then records the flag. Terminated below if no reader ever appears.
WRITER = ("import os,sys\n"
"open(sys.argv[3],'w').close()\n" # ready: about to block in open()
"fd=os.open(sys.argv[1],os.O_WRONLY)\n"
"open(sys.argv[2],'w').close()\n"
"os.close(fd)\n")
def spawn(fifo, flag):
ready = Path(str(flag) + '.ready')
if ready.exists():
ready.unlink()
proc = subprocess.Popen(
[sys.executable, '-c', WRITER, str(fifo), str(flag), str(ready)])
# Wait until the helper is actually at its blocking open(). Starting the
# transfer before that lets the receiver come and go while nothing is
# watching the FIFO, and the run reports a vacuous result -- which is what
# made this test flaky on the slower fleet VMs.
deadline = time.time() + 30
while not ready.exists() and proc.poll() is None and time.time() < deadline:
time.sleep(0.02)
return proc
def settle(w):
"""Give a rendezvoused helper a bounded chance to record its flag; a still-
blocked one just times out. (Closes the terminate-before-flag race.)"""
try:
w.wait(timeout=15)
except subprocess.TimeoutExpired:
pass
def reap(w):
if w.poll() is None:
w.terminate()
try:
w.wait(timeout=10)
except subprocess.TimeoutExpired:
w.kill()
w.wait()
def attempt():
"""One injection run. Returns the receiver's CompletedProcess.
Re-creates the FIFOs and flags each time so a retry starts clean.
"""
for f in (escape, decoy, esc_flag, dec_flag):
if os.path.lexists(f):
os.unlink(f)
rmtree(dest)
makepath(dest)
os.mkfifo(escape)
os.mkfifo(decoy)
esc_w = spawn(escape, esc_flag)
dec_w = spawn(decoy, dec_flag)
proc = None
try:
conf = write_daemon_conf(
[('m', {'path': str(serversrc), 'read only': 'yes', 'use chroot': 'no'})],
name='mal-xname-rsyncd.conf')
os.environ['RSYNC_CONNECT_PROG'] = f'{shlex.quote(str(mal_rsync))} --config={shlex.quote(str(conf))} --daemon'
os.environ['RSYNC_MAL_XNAME'] = '../secret' # from basis_dir[0] == linkdest
proc = subprocess.run(
rsync_argv('-a', f'--link-dest={linkdest}',
'rsync://localhost/m/file', str(dest) + '/'),
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=120)
settle(esc_w)
settle(dec_w)
finally:
os.environ.pop('RSYNC_MAL_XNAME', None)
os.environ.pop('RSYNC_CONNECT_PROG', None)
reap(esc_w)
reap(dec_w)
for f in (escape, decoy):
if os.path.lexists(f):
os.unlink(f)
return proc
# A run where NEITHER fifo was opened proves nothing: the injection did not
# take effect, so there was no traversal attempt to confine. That is a setup
# failure, not a security signal, and on the slower fleet VMs it happens often
# enough to make the test unusable -- so retry it. An ESCAPE is never retried:
# the loop stops the moment the escape flag appears.
attempts = 0
for _try in range(6):
attempts += 1
proc = attempt()
if esc_flag.is_file() or dec_flag.is_file():
break
# -- Oracle -------------------------------------------------------------------
out_tail = '\n'.join(proc.stdout.splitlines()[-20:])
trace = trace_file.read_text().splitlines() if trace_file.is_file() else []
escaped = f'{linkdest}\t../secret'
confined = f'{linkdest}\tsecret'
out_tail = '\n'.join((proc.stdout if proc else '').splitlines()[-20:])
if escaped in trace:
if esc_flag.is_file():
test_fail(
"malicious server traversed the client's filesystem via the alt-dest "
f"xname: the receiver attempted {escape} (one level above the --link-dest "
f"xname: the receiver opened {escape} (one level above the --link-dest "
"dir) as the delta basis. A server-supplied xname of '../secret' was "
"not sanitized on the client (sanitize_paths==0 off-daemon). Fix: "
"sanitize a basis-type xname in read_ndx_and_attrs(). Receiver output "
f"tail:\n{out_tail}")
# The trace proves the crafted xname reached the receiver and was confined to
# the basedir (sanitized "../secret" -> "secret" -> linkdest/secret). Its
# absence means the injection never took effect (e.g. a stale patched build).
if confined not in trace:
# The decoy flag proves the crafted xname reached the receiver AND was confined
# to the basedir (sanitized "../secret" -> "secret" -> linkdest/secret). Its
# absence means the injection never took effect (e.g. a stale patched build),
# so a clear escape flag alone would be a vacuous pass.
if not dec_flag.is_file():
test_fail(
"the crafted xname never reached the receiver's confined basis open; "
"the instrumented injection did not take effect, so this run is "
f"vacuous. Trace={trace!r}. Receiver rc={proc.returncode}. "
f"Output tail:\n{out_tail}")
"the crafted xname never reached the receiver's basis open (neither the "
"escape nor the decoy FIFO was opened) -- the instrumented-sender "
f"injection did not take effect, so this run is vacuous after "
f"{attempts} attempt(s). This is a harness failure, NOT a traversal: "
"an escape is reported separately and is never retried. Receiver rc="
f"{proc.returncode if proc else 'n/a'}. Output tail:\n{out_tail}")
if proc.returncode != 0:
test_fail(
+1 -1
View File
@@ -209,7 +209,7 @@ proc = subprocess.run(
# node fails first with ENXIO, which is an equally valid refusal at the open.
# A clean (returncode 0) run would be the real failure -- rsync accepting the
# device as a batch file.
refused = ('is neither a regular file nor a FIFO' in proc.stderr
refused = ('is not a regular file' in proc.stderr
or ('open error' in proc.stderr and proc.returncode != 0))
if not refused:
test_fail(
-69
View File
@@ -1,69 +0,0 @@
#!/usr/bin/env python3
# --contimeout is documented as a "daemon connection timeout". The guard that
# rejects it only looked at whether connect_timeout was set, so a daemon
# connection made through a remote shell (e.g. rsync-ssl, which runs rsync with
# --rsh pointing at its helper) was rejected with the same syntax error as a
# plain non-daemon remote-shell transfer. Only reject the option when there is
# no daemon connection at all: a daemon reached via --rsh (daemon_connection ==
# 1) is still a daemon connection, and rsync now also times that connection's
# establishment phase with --contimeout the same way it times a socket connect.
import subprocess
import time
from rsyncfns import SCRATCHDIR, SRCDIR, rsync_argv, rmtree, test_fail
RERR_CONTIMEOUT = 35
base = SCRATCHDIR / 'contimeout-rsh'
rmtree(base)
base.mkdir(parents=True)
def run(*args):
return subprocess.run(rsync_argv(*args), capture_output=True, text=True)
rejected_marker = "may only be used when connecting to an rsync daemon"
# A remote-shell command that fails immediately: the option guard runs before
# rsync ever tries to exec it, so it only has to exist as a plausible --rsh
# target to put rsync into its daemon-via-rsh connection mode.
rsh_prog = str(SRCDIR / 'support' / 'lsh.sh')
# --- Daemon via --rsh must accept --contimeout (rsync-ssl's shape of call).
proc = run('--contimeout=5', '--rsh=' + rsh_prog,
'-av', 'rsync://127.0.0.1:9/mod/', str(base / 'dest'))
if rejected_marker in (proc.stderr or ''):
test_fail(f"--contimeout was rejected for a daemon-via-rsh connection:\n{proc.stderr}")
# --- A plain remote-shell (non-daemon) destination must still be rejected.
proc = run('--contimeout=5', '-av', str(base / 'src'), 'localhost:' + str(base / 'dst'))
if rejected_marker not in (proc.stderr or ''):
test_fail("--contimeout was not rejected for a non-daemon remote shell:\n" +
(proc.stderr or '') + (proc.stdout or ''))
# --- A daemon-via-rsh connection that never establishes must time out with the
# daemon-connection timeout exit code. The fake helper sleeps instead of
# connecting, so the only thing that can end the run is --contimeout firing.
fake_rsh = base / 'hang-rsh'
# The helper inherits rsync's stderr; redirect it so an orphaned "sleep" does
# not keep the harness's captured-pipe open after rsync has already exited.
fake_rsh.write_text("#!/bin/sh\nexec 2>/dev/null\nsleep 60\n")
fake_rsh.chmod(0o755)
start = time.monotonic()
proc = run('--contimeout=1', '--rsh=' + str(fake_rsh),
'-av', 'rsync://127.0.0.1:9/mod/', str(base / 'dest2'))
elapsed = time.monotonic() - start
if proc.returncode != RERR_CONTIMEOUT:
test_fail(f"--contimeout did not abort the hung connection with exit "
f"{RERR_CONTIMEOUT}; got {proc.returncode}:\n{proc.stderr}")
if elapsed >= 15:
test_fail(f"--contimeout=1 took {elapsed:.1f}s; the timeout did not bound "
"the connection establishment phase")
print("contimeout-rsh: --contimeout is accepted for a daemon-via-rsh "
"connection, rejected for a non-daemon remote shell, and times out a "
"connection that never establishes")
-169
View File
@@ -1,169 +0,0 @@
#!/usr/bin/env python3
"""Daemon coverage: IDN hosts allow / hosts deny matching (access.c).
A daemon gets its peer's name from DNS as ASCII, so an rsyncd.conf entry
written with non-ASCII characters is folded to its IDNA A-label form before
being matched. The IDNA mapping also folds some non-ASCII characters onto
ASCII ones (U+FF0A FULLWIDTH ASTERISK becomes '*'), so the checks below cover
both directions: a Unicode name that has to match, and the tokens that must
stay denied rather than turn into a wildcard or an address/mask. Punycode,
mixed case and an unconvertible name are covered too.
The peer name isn't assumed: a throwaway daemon comes up first to log the name
this host's resolver gives it, and the real config is written around that.
"forward lookup" is off throughout, which pins the match on the reverse-DNS
name and keeps a denied module from waiting out a resolver timeout on a name
that deliberately doesn't exist.
Like daemon-access-ip_test.py this needs a real TCP peer (--use-tcp), and the
config sets no global hosts allow so each module's own patterns decide.
"""
import re
import subprocess
from rsyncfns import (
FROMDIR, SCRATCHDIR,
claim_ports, make_tree, require_tcp, rmtree, rsync_argv, start_rsyncd,
start_test_daemon, test_fail, test_skipped,
)
PROBE_PORT = 12896
DAEMON_PORT = 12898
require_tcp("hosts allow/deny hostname matching needs a real TCP peer")
if '"IDN": true' not in subprocess.run(rsync_argv('-VV'), capture_output=True,
text=True).stdout:
test_skipped("rsync built without IDN support")
src = FROMDIR
rmtree(src)
make_tree(src, depth=2)
def write_conf(path, modules, log, pidfile):
lines = [
'# autogenerated by daemon-access-idn_test.py',
f'pid file = {pidfile}',
'use chroot = no',
'forward lookup = no',
f'log file = {log}',
'',
]
for mod, params in modules:
lines.append(f'[{mod}]')
lines.append(f'\tpath = {src}')
lines.append('\tread only = yes')
lines += [f'\t{k} = {v}' for k, v in params.items()]
lines.append('')
# rsyncd.conf is read as UTF-8 by the daemon whatever the test's locale is.
path.write_text('\n'.join(lines) + '\n', encoding='utf-8')
return path
# --- find out what this host's resolver calls the loopback peer -------------
# A throwaway daemon with one wide-open module: connect once, read the name it
# logged for us, then shut it down before the real config goes up.
probe_log = SCRATCHDIR / 'rsyncd-idn-probe.log'
probe_conf = write_conf(SCRATCHDIR / 'access-idn-probe.conf', [('probe', {})],
probe_log, SCRATCHDIR / 'rsyncd-idn-probe.pid')
claim_ports(PROBE_PORT)
probe = start_rsyncd(probe_conf, PROBE_PORT)
try:
subprocess.run(rsync_argv('-r', f'rsync://localhost:{PROBE_PORT}/probe/'),
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
finally:
probe.terminate()
probe.wait(timeout=5)
m = re.search(r'connect from (\S+) \(', probe_log.read_text(errors='replace'))
if not m:
test_fail(f"no 'connect from' line in the probe daemon log {probe_log}")
peer = m.group(1)
print(f"daemon sees its peer as {peer!r}")
def fullwidth(name):
"""The fullwidth-forms spelling of an ASCII name.
IDNA (UTS #46) maps U+FF01..U+FF5E back onto ASCII, so this gives us a
genuinely non-ASCII name that folds to a peer name we can predict without
controlling DNS. Label separators stay ASCII dots.
"""
return ''.join(chr(ord(c) - 0x21 + 0xFF01) if '!' <= c <= '~' and c != '.'
else c for c in name)
if fullwidth(peer) == peer:
test_skipped(f"peer name {peer!r} has no ASCII to respell in fullwidth")
ZWSP = '' # maps to nothing, leaving no label at all
FW_STAR = '' # FULLWIDTH ASTERISK, which IDNA maps to '*'
FW_SLASH = '' # FULLWIDTH SOLIDUS, which IDNA maps to '/'
modules = [
('ascii-name', {'hosts allow': peer}),
('ascii-upper', {'hosts allow': peer.upper()}),
('ascii-wild', {'hosts allow': peer[:1] + '*'}),
# The same name in fullwidth forms, which only matches once the token has
# been folded to A-labels.
('idn-name', {'hosts allow': fullwidth(peer)}),
('idn-mixedcase', {'hosts allow': fullwidth(peer.upper())}),
('idn-deny', {'hosts deny': fullwidth(peer)}),
# A real IDN that is not the peer, plus its punycode spelling: both must
# stay denied, and neither may be mistaken for a wildcard.
('idn-other', {'hosts allow': 'čičku.example'}),
('idn-puny', {'hosts allow': 'xn--iku-eqab.example'}),
# idn-other's token respelled with combining carons: an equivalent name
# under Unicode, so it has to be treated the same way. (A decomposed
# token that *matches* isn't constructible here -- the peer name comes
# from DNS and is ASCII -- so idn_test checks the two spellings convert
# alike, and this checks the daemon agrees they don't match.)
('idn-nfd', {'hosts allow': 'c\u030ci' 'c\u030cku.example'}),
# Tokens whose IDNA mapping yields ASCII the author never wrote. Left
# unconverted they match nothing; converted blindly, the first two would
# allow every host and the third would read as an address/mask.
('wide-star', {'hosts allow': FW_STAR}),
('wide-star-dom', {'hosts allow': FW_STAR + '.example'}),
('wide-mask', {'hosts allow': '127.0.0.0' + FW_SLASH + '8'}),
# An IDN that can't be converted at all (its label maps to nothing).
('bad-idn', {'hosts allow': ZWSP + '.example'}),
]
conf = write_conf(SCRATCHDIR / 'access-idn.conf', modules,
SCRATCHDIR / 'rsyncd.log', SCRATCHDIR / 'rsyncd.pid')
url = start_test_daemon(conf, DAEMON_PORT)
def connect(mod):
"""Return rsync's exit code for listing the module over the daemon."""
return subprocess.run(rsync_argv('-r', f'{url}{mod}/'),
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE,
text=True).returncode
def allowed(mod, why):
if connect(mod) != 0:
test_fail(f"connection to {mod} should be ALLOWED ({why}) but was refused")
def denied(mod, why):
if connect(mod) == 0:
test_fail(f"connection to {mod} should be DENIED ({why}) but succeeded")
allowed('ascii-name', "the peer's own name in a hosts allow")
allowed('ascii-upper', "hostname matching is case-insensitive")
allowed('ascii-wild', "an ASCII wildcard still matches")
allowed('idn-name', f"fullwidth {peer!r} folds to the peer's name")
allowed('idn-mixedcase', "IDNA case-folds the token")
denied('idn-deny', "hosts deny sees the folded token too")
denied('idn-other', "a different IDN must not match the peer")
denied('idn-puny', "an A-label for a different host must not match the peer")
denied('idn-nfd', "a decomposed spelling of that name must not match either")
denied('wide-star', "U+FF0A must not become a '*' that allows every host")
denied('wide-star-dom', "U+FF0A must not become a wildcard label")
denied('wide-mask', "U+FF0F must not become an address/mask separator")
denied('bad-idn', "an unconvertible IDN must not match anything")
print("daemon-access-idn: IDN hosts allow/deny matching + no wildcard widening")
+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 = [
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""Daemon-mode: the server must reject a wire-supplied --max-alloc=0.
max-alloc-zero-rejected_test.py only proves the *local* client refuses
--max-alloc=0. That alone doesn't protect a daemon: a modified or older client
still forwards --max-alloc=0 on the wire, and an unpatched daemon honours it and
disables its my_alloc() allocation cap (the defence behind CVE-2024-12084 and
friends). This test drives an older rsync client -- which lacks the reject-zero
check and so forwards the option -- against the current rsync daemon, and
asserts the *daemon* refuses it.
It uses the in-tree old_versions/rsync_3.2.7 as the client (3.2.7 predates the
reject-zero fix, so it forwards --max-alloc=0 on the wire). If that binary is
missing or can't run here (e.g. a non-Linux host that can't run the static
archive) the test skips.
"""
import subprocess
from pathlib import Path
from rsyncfns import (
FROMDIR, RSYNC, SCRATCHDIR,
makepath, rmtree, start_test_daemon, test_fail, test_skipped,
write_daemon_conf,
)
DAEMON_PORT = 12932
REJECT_MSG = 'max-alloc must be greater than zero'
OLD_CLIENT = Path(__file__).resolve().parents[1] / 'old_versions' / 'rsync_3.2.7'
if not OLD_CLIENT.exists():
test_skipped(f"{OLD_CLIENT} not present")
# Confirm the static binary actually runs as rsync on this OS/arch before we
# depend on it: exec of a foreign-arch/OS binary raises OSError, while one that
# loads but can't run won't print the rsync banner. (3.2.7 predates the
# reject-zero fix, so once it runs it forwards --max-alloc=0 on the wire.)
try:
probe = subprocess.run([str(OLD_CLIENT), '--version'],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True)
except OSError as e:
test_skipped(f"cannot run {OLD_CLIENT.name} on this OS/arch: {e}")
if probe.returncode != 0 or 'version 3.2.7' not in probe.stdout:
test_skipped(f"{OLD_CLIENT.name} does not run as rsync on this OS/arch")
# Module served by the *current* (patched) daemon.
src = FROMDIR
rmtree(src)
makepath(src)
(src / 'file.txt').write_text('hello\n')
conf = write_daemon_conf([('mod', {'path': str(src), 'read only': 'yes'})])
url = start_test_daemon(conf, DAEMON_PORT, rsync_cmd=RSYNC)
dest = SCRATCHDIR / 'out.txt'
def run_client(*extra):
argv = [str(OLD_CLIENT), *extra, f'{url}mod/file.txt', str(dest)]
return subprocess.run(argv, stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE, text=True)
# Positive control: the old client and current daemon transfer fine without the
# option, so the failure below is specifically the daemon refusing the option.
dest.unlink(missing_ok=True)
ctrl = run_client()
if ctrl.returncode != 0:
test_fail(f"old client could not talk to the current daemon:\n{ctrl.stderr}")
# The attack: a forwarded --max-alloc=0 must be refused by the daemon.
dest.unlink(missing_ok=True)
proc = run_client('--max-alloc=0')
if proc.returncode == 0:
test_fail("daemon accepted a wire-supplied --max-alloc=0")
if REJECT_MSG not in proc.stderr:
test_fail("daemon did not reject --max-alloc=0 with the expected message; "
f"stderr:\n{proc.stderr}")
print("daemon-max-alloc-zero: daemon refuses a wire-supplied --max-alloc=0 "
f"(client {OLD_CLIENT.name})")
+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.",
-186
View File
@@ -1,186 +0,0 @@
#!/usr/bin/env python3
# Verify that rsync converts an IDN (internationalized domain name) host to
# its IDNA A-label (Punycode) form, and that it leaves an ASCII host name
# alone. Only the labels that are not ASCII get rewritten, so an address
# literal, an already-punycoded name, and a name that isn't a valid IDN all
# reach the resolver as typed. A name typed with combining marks is normalized
# on the way, so it converts the same as its precomposed spelling.
#
# Two daemon connection methods carry the host name out of rsync, so both are
# checked:
# * daemon over a remote shell (what rsync-ssl does): the host is handed to
# the --rsh helper.
# * direct daemon socket: observed through a dummy HTTP proxy (RSYNC_PROXY) on
# loopback, so this part only runs under --use-tcp.
# A plain remote-shell transfer (host:path) is intentionally left alone, since
# that name belongs to the user's ssh.
#
# The daemon side of IDN -- hosts allow/deny matching -- is daemon-access-idn.
import os
import shlex
import socket
import subprocess
import sys
import threading
from rsyncfns import (
RSYNC, SCRATCHDIR, USE_TCP, claim_ports, run_rsync,
test_fail, test_skipped,
)
if '"IDN": true' not in run_rsync('-VV', check=True, capture_output=True).stdout:
test_skipped("rsync built without IDN support")
def find_utf8_locale():
try:
out = subprocess.check_output(['locale', '-a'], text=True,
stderr=subprocess.DEVNULL)
except (OSError, subprocess.CalledProcessError):
return None
avail = out.split()
for want in ('C.UTF-8', 'C.utf8', 'en_US.UTF-8', 'en_US.utf8'):
if want in avail:
return want
for loc in avail:
if loc.lower().replace('-', '').endswith('utf8'):
return loc
return None
utf8_locale = find_utf8_locale()
if not utf8_locale:
test_skipped("no UTF-8 locale available to encode the IDN host")
idn_host = "\u010ci\u010dku.example"
ascii_host = "xn--iku-eqab.example"
# The same name with each caron letter spelled as a plain "c" plus a combining
# caron (U+030C). Unicode calls the two spellings equivalent, so both have to
# come out as the same A-label; libidn2 is what normalizes them.
nfd_host = "c\u030ci" "c\u030cku.example"
env = os.environ.copy()
env['LC_ALL'] = utf8_locale
out_dir = (str(SCRATCHDIR / 'out') + '/').encode()
def run_idn(url, *extra, extra_env=None):
# A bytes argv keeps the UTF-8 host intact regardless of Python's
# filesystem encoding.
e = dict(env)
if extra_env:
e.update(extra_env)
argv = [a.encode() for a in shlex.split(RSYNC)]
argv += [a.encode() for a in extra]
argv += [url.encode('utf-8'), out_dir]
return subprocess.run(argv, capture_output=True, env=e, timeout=30)
# --- daemon over a remote shell (the rsync-ssl mechanism) ------------------
helper = SCRATCHDIR / 'idn-rsh.sh'
helper.write_text('#!/bin/sh\nprintf %s "$1" > "$IDN_RSH_OUT"\nexit 1\n')
helper.chmod(0o755)
hostfile = SCRATCHDIR / 'idn-rsh-host'
def rsh_host(url_host):
"""The host name rsync hands the --rsh helper for rsync://<url_host>/."""
if hostfile.exists():
hostfile.unlink()
run_idn(f"rsync://{url_host}/module/", f"--rsh={helper}",
extra_env={'IDN_RSH_OUT': str(hostfile)})
if not hostfile.exists():
test_fail(f"the --rsh helper never ran for {url_host!r}")
return hostfile.read_bytes().decode('utf-8', 'surrogateescape')
def check_rsh(url_host, want, what):
got = rsh_host(url_host)
if got != want:
test_fail(f"daemon-over-rsh sent host {got!r} for {what} "
f"({url_host!r}), expected {want!r}")
print(f"OK: {what} -> {got}")
# A U-label becomes its A-label, case-folded by the IDNA mapping. An ASCII
# label is handed on byte for byte, case included, since DNS doesn't care.
check_rsh(idn_host, ascii_host, "a Unicode host")
check_rsh(nfd_host, ascii_host, "a decomposed Unicode host")
check_rsh("C\u030cI" "C\u030cKU.Example", "xn--iku-eqab.Example",
"a decomposed mixed-case Unicode host")
check_rsh("ČIČKU.Example", "xn--iku-eqab.Example",
"a mixed-case Unicode host")
check_rsh(ascii_host, ascii_host, "an already-punycoded host")
check_rsh("XN--IKU-EQAB.Example", "XN--IKU-EQAB.Example",
"a mixed-case punycoded host")
# A name that isn't a valid IDN goes out as-is instead of being rewritten into
# some other name (the U+200B one would map to ".example"), so the resolver
# fails on it just as it did before.
check_rsh("xn--0.example", "xn--0.example", "an undecodable A-label")
check_rsh("ـx.example", "ـx.example", "a label with a disallowed character")
check_rsh(".example", ".example", "a label that maps to nothing")
# --- direct daemon socket, observed via a dummy proxy -----------------------
if not USE_TCP:
print("direct-socket proxy check needs --use-tcp; skipping that part")
sys.exit(0)
PROXY_PORT = 13335
claim_ports(PROXY_PORT)
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listener.bind(('127.0.0.1', PROXY_PORT))
listener.listen(1)
captured = {}
def serve_one():
conn, _ = listener.accept()
conn.settimeout(5)
data = b""
try:
while b"\r\n\r\n" not in data and len(data) < 65536:
chunk = conn.recv(8192)
if not chunk:
break
data += chunk
except socket.timeout:
pass
captured['request'] = data
try:
conn.sendall(b"HTTP/1.0 403 Forbidden\r\n\r\n")
conn.shutdown(socket.SHUT_RDWR)
except OSError:
pass
conn.close()
t = threading.Thread(target=serve_one)
t.daemon = True
t.start()
proc = run_idn(f"rsync://{idn_host}:873/whatever/",
extra_env={'RSYNC_PROXY': f'127.0.0.1:{PROXY_PORT}'})
t.join(timeout=15)
listener.close()
if proc.returncode >= 128:
sys.stderr.write(proc.stderr.decode('latin1'))
test_fail(f"rsync killed by signal (status={proc.returncode})")
request = captured.get('request', b'')
if not request:
test_fail("dummy proxy received no CONNECT request from rsync")
if ascii_host.encode() not in request:
sys.stderr.write("proxy received: %r\n" % request.split(b"\r\n", 1)[0])
test_fail(f"expected A-label {ascii_host} in the proxy CONNECT request")
print(f"OK: direct-socket CONNECT host sent as {ascii_host}")
-97
View File
@@ -1,97 +0,0 @@
#!/usr/bin/env python3
import filecmp
import os
import re
import shlex
import shutil
import subprocess
from rsyncfns import SCRATCHDIR, SRCDIR, TOOLDIR, test_fail
make_vars = {}
for line in (TOOLDIR / 'Makefile').read_text().splitlines():
name, separator, value = line.partition('=')
if separator and name in ('prefix', 'exec_prefix', 'bindir',
'EXEEXT', 'STRIP'):
make_vars[name] = value.strip()
if not make_vars.get('STRIP'):
test_fail('configured Makefile does not define STRIP')
source_rsync = TOOLDIR / f"rsync{make_vars.get('EXEEXT', '')}"
if not source_rsync.is_file():
test_fail('cannot find the built rsync binary')
builddir = SCRATCHDIR / 'build'
builddir.mkdir()
makefile_text = (TOOLDIR / 'Makefile').read_text()
if 'install: all\n' not in makefile_text:
test_fail('cannot isolate the install target from the shared build')
if 'Makefile: Makefile.in config.status configure.sh config.h.in\n' not in makefile_text:
test_fail('cannot isolate Makefile regeneration from the shared build')
(builddir / 'Makefile').write_text(
makefile_text
.replace('install: all\n', 'install:\n', 1)
.replace('Makefile: Makefile.in config.status configure.sh config.h.in\n',
'Makefile:\n', 1)
)
for name in (source_rsync.name, 'install-sh', 'rsync-ssl', 'rrsync', 'rsync.1',
'rsync-ssl.1', 'rsyncd.conf.5', 'rrsync.1'):
source = TOOLDIR / name
if not source.is_file():
source = SRCDIR / name
if source.is_file():
shutil.copy2(source, builddir / name)
def expand_make_value(value):
for _ in range(10):
expanded = re.sub(
r'\$\{([^}]+)\}|\$\(([^)]+)\)',
lambda match: make_vars.get(match.group(1) or match.group(2),
match.group(0)),
value,
)
if expanded == value:
return expanded
value = expanded
test_fail(f'cannot expand Makefile value {value!r}')
bindir = expand_make_value(make_vars.get('bindir', ''))
if not bindir or '$' in bindir:
test_fail(f'cannot determine configured bindir from {bindir!r}')
make = shlex.split(os.environ.get('MAKE', 'make'))
tools = SCRATCHDIR / 'tools'
tools.mkdir()
for strip_name in ('strip', 'aarch64-linux-gnu-strip'):
destdir = SCRATCHDIR / 'roots' / strip_name
strip_log = SCRATCHDIR / f'{strip_name}.log'
strip = tools / strip_name
strip.write_text('#!/bin/sh\nprintf \'%s\\n\' "$@" >"$STRIP_LOG"\n')
strip.chmod(0o755)
env = os.environ.copy()
env['STRIP_LOG'] = str(strip_log)
proc = subprocess.run(
[*make, f'DESTDIR={destdir}', f'STRIP={strip}', 'install-strip'],
cwd=builddir, env=env, capture_output=True, text=True,
)
if proc.returncode != 0:
test_fail(f'install-strip failed with {strip_name}:\n'
f'{proc.stdout}{proc.stderr}')
installed = destdir / bindir.lstrip('/') / source_rsync.name
if not installed.is_file():
test_fail(f'install-strip did not install {installed}')
if not strip_log.is_file():
test_fail(f'install-strip did not call {strip_name}')
if strip_log.read_text().splitlines() != [str(installed)]:
test_fail(f'{strip_name} was not called with {installed}')
if not filecmp.cmp(source_rsync, installed, shallow=False):
test_fail(f'{strip_name} unexpectedly changed the installed test binary')
@@ -0,0 +1,9 @@
#!/usr/bin/env python3
from rsyncfns import SCRATCHDIR, rsync_argv
from rsyncfns import expect_fail
expect_fail(
rsync_argv('--max-alloc=0', str(SCRATCHDIR / 'missing-src'), str(SCRATCHDIR / 'missing-dst')),
'max-alloc must be greater than zero',
)
print("max-alloc-zero-rejected: --max-alloc=0 is rejected")
-88
View File
@@ -1,88 +0,0 @@
#!/usr/bin/env python3
"""``--max-alloc=0`` means "the largest limit this build supports".
Three things are asserted, and the second is the reason 0 is worth keeping as a
spelling at all:
1. 0 is accepted, and a transfer using it works.
2. 0 reaches the peer as the literal "0", not as a resolved number. Each side
then resolves it against its own SIZE_MAX. That is what makes 0 the only
value correct for both ends of a mixed-word-size pairing: the ceiling is
SIZE_MAX/2, so any number large enough to be worth setting on a 64-bit
client (over 2047M) is refused as "too large" by a 32-bit daemon.
3. The parser's upper bound is still enforced. Accepting 0 again must not
bring back the unbounded ``size *= atof(size_arg)`` that was fixed in
3.5.0, so an out-of-range value is still rejected rather than wrapping.
The forwarding check in (2) deliberately inspects the argv the remote shell is
handed rather than a transfer outcome: a resolved number also copies files
happily on a same-word-size pair, so an outcome-based assertion would pass on
exactly the configuration this behaviour does not matter for.
"""
import os
import shlex
from rsyncfns import (
SCRATCHDIR, SRCDIR, expect_fail, rsh_cmd, rmtree, rsync_argv,
rsync_path_arg, run_rsync, test_fail,
)
base = SCRATCHDIR / 'max-alloc-zero'
rmtree(base)
src = base / 'from'
dst = base / 'to'
src.mkdir(parents=True)
dst.mkdir(parents=True)
(src / 'file.txt').write_text('hello\n')
# --- 1. 0 is accepted -------------------------------------------------------
run_rsync('-r', '--max-alloc=0', f'{src}/', f'{dst}/')
if (dst / 'file.txt').read_text() != 'hello\n':
test_fail('--max-alloc=0 did not copy the file')
# --- 2. 0 goes on the wire un-normalized ------------------------------------
argv_log = base / 'server-argv'
wrapper = base / 'log-rsh.sh'
wrapper.write_text(
'#!/bin/sh\n'
'# Log the command line built for the peer, then behave like lsh.sh.\n'
f'printf \'%s\\n\' "$*" >> {shlex.quote(str(argv_log))}\n'
f'exec {shlex.quote(str(SRCDIR / "support" / "lsh.sh"))} "$@"\n'
)
wrapper.chmod(0o755)
rmtree(dst)
dst.mkdir()
os.environ['RSYNC_RSH'] = rsh_cmd(str(wrapper))
run_rsync('-r', '--max-alloc=0', f'--rsync-path={rsync_path_arg()}',
f'localhost:{src}/', f'{dst}/')
del os.environ['RSYNC_RSH']
if (dst / 'file.txt').read_text() != 'hello\n':
test_fail('--max-alloc=0 did not copy the file over the remote shell')
logged = argv_log.read_text() if argv_log.exists() else ''
if not logged:
test_fail('the remote-shell wrapper logged no command line')
if '--max-alloc=0' not in f' {logged} '.replace('\n', ' '):
test_fail('--max-alloc=0 was not forwarded verbatim; the peer was sent:\n'
f'{logged}'
'\nA resolved number here would be rejected as "too large" by a '
'peer with a smaller SIZE_MAX.')
# --- 3. the upper bound still holds -----------------------------------------
# 8192P is one step past SIZE_ARG_MAX (SIZE_MAX/2) on a 64-bit build; on a
# 32-bit one the P multiplier alone already exceeds it. Either way: too large.
expect_fail(rsync_argv('--max-alloc=8192P', f'{src}/', f'{dst}/'), 'is too large')
# And the min-value message must keep advertising a spelling that works.
expect_fail(rsync_argv('--max-alloc=1', f'{src}/', f'{dst}/'),
'or 0 for unlimited')
print('max-alloc-zero: 0 is accepted, forwarded verbatim, and the bound holds')
@@ -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")
@@ -37,17 +37,6 @@ if _proto is not None and _proto < 30:
hook_code = r'''
#define _GNU_SOURCE
/* Build the hook itself WITHOUT large-file redirection, whatever the compiler
* defaults to. Debian's armhf/hppa/powerpc gcc predefines
* -D_FILE_OFFSET_BITS=64 -D_TIME_BITS=64 (check with "gcc -v -E -"), and under
* those macros glibc's __REDIRECT renames the DEFINITIONS below -- open()
* becomes open64(), fstatat() becomes __fstatat64_time64() -- which then
* collide with the explicit large-file wrappers further down ("symbol `open64'
* is already defined"). Undefining them here keeps each name declared exactly
* once, so the hook always exports both spellings and interposes whichever set
* the rsync under test was linked against. */
#undef _FILE_OFFSET_BITS
#undef _TIME_BITS
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
@@ -67,9 +56,6 @@ static int (*real_open)(const char *, int, ...);
static int (*real_openat)(int, const char *, int, ...);
static int (*real_fstatat)(int, const char *, struct stat *, int);
static int (*real_fxstatat)(int, int, const char *, struct stat *, int);
static int (*real_fstatat64)(int, const char *, struct stat64 *, int);
static int (*real_fxstatat64)(int, int, const char *, struct stat64 *, int);
static int (*real_fstatat64_time64)(int, const char *, struct stat64 *, int);
/* Resolve on demand rather than trusting our constructor to have run. A
* preloaded open() interposes for the whole process the moment the loader maps
@@ -91,10 +77,6 @@ static void hook_resolve(void)
if (!real_openat) real_openat = dlsym(RTLD_NEXT, "openat");
if (!real_fstatat) real_fstatat = dlsym(RTLD_NEXT, "fstatat");
if (!real_fxstatat) real_fxstatat = dlsym(RTLD_NEXT, "__fxstatat");
if (!real_fstatat64) real_fstatat64 = dlsym(RTLD_NEXT, "fstatat64");
if (!real_fxstatat64) real_fxstatat64 = dlsym(RTLD_NEXT, "__fxstatat64");
if (!real_fstatat64_time64)
real_fstatat64_time64 = dlsym(RTLD_NEXT, "__fstatat64_time64");
hook_resolving = 0;
}
@@ -169,10 +151,6 @@ static int swap_and_deny(void)
# define HOOK_TAKES_MODE(f) ((f) & O_CREAT)
#endif
#ifndef O_LARGEFILE
# define O_LARGEFILE 0
#endif
static int is_victim_write(const char *path, int flags)
{
return path && strcmp(path, "victim") == 0
@@ -253,49 +231,6 @@ int open(const char *path, int flags, ...)
return fd;
}
/* --- large-file spellings -------------------------------------------------
* Which names the RECEIVER calls is settled by its own build: where off_t is
* not already 64 bits, configure's AC_SYS_LARGEFILE adds -D_FILE_OFFSET_BITS=64
* (i386, alpha, ...), and distro CPPFLAGS add it -- along with -D_TIME_BITS=64
* -- on the 64-bit time_t ports, so glibc redirects each open()/openat()/
* fstatat() call to open64()/openat64()/fstatat64()/__fstatat64_time64().
* "objdump -T rsync | grep UND" says which set a given build imports.
*
* Which names THIS HOOK exports is a different question with a different
* answer, settled by whatever the "cc" below it defaults to -- see the #undef
* at the top. Nothing keeps the two in step, so define every spelling and let
* the loader match them up. With only the unsuffixed ones the receiver's opens
* sail straight past the hook, no EACCES is ever injected, and the test reports
* "positive control failed" having exercised nothing at all.
*
* O_LARGEFILE is the only thing open64() adds over open(), so the wrappers
* below can hand the call to the unsuffixed interposer above. */
int open64(const char *path, int flags, ...)
{
mode_t mode = 0;
if (HOOK_TAKES_MODE(flags)) {
va_list ap;
va_start(ap, flags);
mode = (mode_t)va_arg(ap, int);
va_end(ap);
}
return open(path, flags | O_LARGEFILE, mode);
}
int openat64(int dfd, const char *path, int flags, ...)
{
mode_t mode = 0;
if (HOOK_TAKES_MODE(flags)) {
va_list ap;
va_start(ap, flags);
mode = (mode_t)va_arg(ap, int);
va_end(ap);
}
return openat(dfd, path, flags | O_LARGEFILE, mode);
}
/* ona_open() decides via fstatat(..., AT_SYMLINK_NOFOLLOW) and refuses a
* component owned by neither root nor the euid. Model the attacker as a
* different uid so a retry that kept the ownership walk refuses the swap. */
@@ -341,68 +276,6 @@ int __fxstatat(int ver, int dfd, const char *path, struct stat *st, int flags)
return rc;
}
/* The stat family's large-file spellings. st_mode and st_uid sit ahead of the
* timestamps in every glibc struct stat layout, so the time32/time64 variants
* of the buffer are interchangeable for the two fields touched here. */
static void model_foreign_owner64(int rc, const char *path, struct stat64 *st)
{
if (rc == 0 && swapped && path && strcmp(path, "pdir") == 0
&& S_ISLNK(st->st_mode)) {
st->st_uid = geteuid() + 1;
mark(getenv("RSYNC_PARTIAL_RETRY_FOREIGN_MARKER"));
}
}
int fstatat64(int dfd, const char *path, struct stat64 *st, int flags)
{
int rc, saved_errno;
hook_resolve();
if (!real_fstatat64) {
errno = ENOSYS;
return -1;
}
rc = real_fstatat64(dfd, path, st, flags);
saved_errno = errno;
model_foreign_owner64(rc, path, st);
errno = saved_errno;
return rc;
}
int __fxstatat64(int ver, int dfd, const char *path, struct stat64 *st, int flags)
{
int rc, saved_errno;
hook_resolve();
if (!real_fxstatat64) {
errno = ENOSYS;
return -1;
}
rc = real_fxstatat64(ver, dfd, path, st, flags);
saved_errno = errno;
model_foreign_owner64(rc, path, st);
errno = saved_errno;
return rc;
}
/* A 32-bit port built with -D_TIME_BITS=64 (Debian's armhf/armel/hppa/powerpc,
* ...) reaches fstatat() under this third name. */
int __fstatat64_time64(int dfd, const char *path, struct stat64 *st, int flags)
{
int rc, saved_errno;
hook_resolve();
if (!real_fstatat64_time64) {
errno = ENOSYS;
return -1;
}
rc = real_fstatat64_time64(dfd, path, st, flags);
saved_errno = errno;
model_foreign_owner64(rc, path, st);
errno = saved_errno;
return rc;
}
__attribute__((constructor)) static void hook_loaded(void)
{
hook_resolve();
+12 -40
View File
@@ -38,33 +38,12 @@ if run_rsync('-a', '--preallocate', f'{src}/', f'{TODIR}/',
check=False, capture_output=True).returncode != 0:
test_skipped("--preallocate not supported on this platform")
def punch_frees(offset, length, size):
"""True where punching [offset, offset+length) out of a `size`-byte file
really deallocates blocks -- the mechanism do_punch_hole uses for --sparse.
Two separate things can leave st_blocks untouched, so each assertion below
probes the exact shape it relies on. A filesystem may report seek-based
sparseness yet still keep every block on a punch (e.g. where rsync's punch
falls back to writing zeros), which a whole-file probe catches. And a punch
only frees storage in whole allocation units, which are not always 4 KiB: a
tmpfs frees whole pages, 16 KiB on loongarch/loong64 (and 64 KiB on a
64k-page ppc64el or arm64 kernel), and a filesystem may be formatted with a
block size above the page size. An interior run spanning no whole unit is
zeroed rather than deallocated, so st_blocks does not move and an assertion
phrased in st_blocks would report a hole-punching regression that is really
just the filesystem's granularity.
fallocate64() rather than fallocate(): where off_t is 32 bits the latter
takes 32-bit offsets, so ctypes' 64-bit arguments do not line up with what
it reads (on i386 it takes the high half of `offset` as its `length`) and
every probe fails with EINVAL -- which is why every assertion below has
silently done nothing on all the 32-bit ports. fallocate64() takes off64_t
everywhere and is a plain alias of fallocate() where off_t is already 64
bits wide.
The probe data has to be incompressible: a filesystem that compresses
(btrfs with compress=) stores a run of one repeated byte in almost no
blocks, leaving a successful punch with nothing to free."""
def fs_can_punch_holes():
"""True only where the kernel can deallocate blocks via FALLOC_FL_PUNCH_HOLE
-- the mechanism do_punch_hole uses for --sparse. A filesystem may report
seek-based sparseness yet still keep every block on a punch (e.g. where
rsync's punch falls back to writing zeros), so probe the real capability and
assert the hole only where it actually frees blocks."""
import ctypes
import ctypes.util
KEEP_SIZE, PUNCH_HOLE = 0x01, 0x02
@@ -73,12 +52,12 @@ def punch_frees(offset, length, size):
try:
libc = ctypes.CDLL(ctypes.util.find_library('c') or 'libc.so.6',
use_errno=True)
libc.fallocate64.argtypes = [ctypes.c_int, ctypes.c_int,
ctypes.c_longlong, ctypes.c_longlong]
libc.fallocate.argtypes = [ctypes.c_int, ctypes.c_int,
ctypes.c_longlong, ctypes.c_longlong]
fd = os.open(p, os.O_CREAT | os.O_RDWR | os.O_TRUNC, 0o644)
os.write(fd, os.urandom(size))
os.write(fd, b'\xff' * 65536)
before = os.fstat(fd).st_blocks
ret = libc.fallocate64(fd, PUNCH_HOLE | KEEP_SIZE, offset, length)
ret = libc.fallocate(fd, PUNCH_HOLE | KEEP_SIZE, 0, 65536)
return ret == 0 and os.fstat(fd).st_blocks < before
except (OSError, AttributeError, ValueError):
return False
@@ -91,7 +70,7 @@ def punch_frees(offset, length, size):
pass
can_punch = punch_frees(0, 65536, 65536)
can_punch = fs_can_punch_holes()
def seed_plain(size=1_000_000):
@@ -160,13 +139,6 @@ with open(src / deep, 'wb') as source, open(TODIR / deep, 'wb') as dest:
source.write(block)
dest.write(block)
# Only assert the interior punch where the filesystem can free a 24 KiB run
# sitting 4 KiB into a 32 KiB block -- the exact shape written just above.
can_punch_interior = can_punch and punch_frees(4096, 24576, 32768)
if can_punch and not can_punch_interior:
print("preallocate: interior-hole assertion skipped: this filesystem's "
"allocation unit cannot free a 24 KiB run inside a 32 KiB block")
matched_size = os.path.getsize(TODIR / deep)
matched_before = allocated(TODIR / deep)
run_rsync('-a', '--ignore-times', '--inplace', '--sparse', '--no-whole-file',
@@ -174,7 +146,7 @@ run_rsync('-a', '--ignore-times', '--inplace', '--sparse', '--no-whole-file',
assert_same(TODIR / deep, src / deep,
label='--inplace --sparse matched-block content')
matched_after = allocated(TODIR / deep)
if (can_punch_interior and matched_before >= matched_size
if (can_punch and matched_before >= matched_size
and matched_after * 2 >= matched_before):
test_fail(f"--inplace --sparse left matching interior zero runs allocated: "
f"{matched_after} of {matched_before} bytes remain allocated "
+5 -11
View File
@@ -49,17 +49,11 @@ if not _chown_5001(workdir / 'dst'):
if not os.environ.get('RSYNC_UNSHARED'):
unshare = shutil.which('unshare')
if unshare is not None:
try:
probe = subprocess.run(
[unshare, '--user', '--map-root-user',
'--map-users', '5001:100000:1', 'true'],
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
timeout=5,
)
except subprocess.TimeoutExpired:
test_skipped("Can't chown (unshare probe timed out)")
probe = subprocess.run(
[unshare, '--user', '--map-root-user',
'--map-users', '5001:100000:1', 'true'],
capture_output=True,
)
if probe.returncode == 0:
print("Re-running under unshare with UID mapping...")
env = os.environ.copy()
-138
View File
@@ -1,138 +0,0 @@
"""Process substitution /dev/fd/ write pipe pseudo-paths for --log-file must not crash and must successfully write logs, but must be rejected if confined root."""
import shlex
import shutil
import subprocess
import sys
from pathlib import Path
from rsyncfns import (
SCRATCHDIR, makepath, rmtree, rsync_argv, test_fail, test_skipped,
)
if not sys.platform.startswith('linux'):
test_skipped('Kernel pseudo-path string is a Linux-specific procfs feature')
raise SystemExit(0)
# We require bash specifically because standard POSIX /bin/sh does not
# guarantee support for >(...) process substitution syntax.
bash = shutil.which('bash')
if bash is None:
test_skipped('bash is unavailable, cannot test process substitution')
# Verify the host bash actually supports process substitution
probe = subprocess.run(
[bash, '-c', 'echo "probe" > >(cat > /dev/null)'],
capture_output=True
)
if probe.returncode != 0:
test_skipped('bash process substitution is not supported on this system')
base = Path(SCRATCHDIR / 'rsync-pseudo-path').resolve()
src = base / 'src'
dest = base / 'dest'
log_out = base / 'test_log.txt'
log_out_confined = base / 'test_log_confined.txt'
makepath(src, dest)
(src / 'transfer_me.txt').write_text('sync this\n')
rsync_base_cmd = shlex.join(rsync_argv('-a'))
src_path = shlex.quote(str(src) + '/')
dest_path = shlex.quote(str(dest) + '/')
log_path = shlex.quote(str(log_out))
log_path_confined = shlex.quote(str(log_out_confined))
# -------------------------------------------------------------------------
# TEST 1: Unconfined process substitution (Should Succeed)
# -------------------------------------------------------------------------
bash_script = f"{rsync_base_cmd} -v --log-file=>(cat > {log_path}) {src_path} {dest_path}"
try:
proc = subprocess.run(
[bash, '-c', bash_script],
capture_output=True,
text=True,
timeout=10,
)
except subprocess.TimeoutExpired:
rmtree(base)
test_fail('process substitution test timed out')
ctx = f'rc={proc.returncode}, stderr={proc.stderr.strip()!r}'
if proc.returncode != 0:
rmtree(base)
test_fail(f'rsync crashed writing to a pseudo-path log pipe ({ctx})')
if not (dest / 'transfer_me.txt').is_file():
rmtree(base)
test_fail(f'rsync failed to transfer the allowed file ({ctx})')
if not log_out.exists() or log_out.stat().st_size == 0:
rmtree(base)
test_fail(f'rsync survived, but failed to write data to the log pipe ({ctx})')
log_data = log_out.read_text()
if "transfer_me.txt" not in log_data:
rmtree(base)
test_fail(f'Log pipe received data, but is missing expected output: {log_data[:100]}')
print('Test 1 Passed: rsync successfully wrote logs to a process substitution pseudo-path')
# -------------------------------------------------------------------------
# TEST 2: Confined Root (Should Reject Pseudo-path)
# -------------------------------------------------------------------------
bash_script_confined = f"{rsync_base_cmd} --confine-root={dest_path} -v --log-file=>(cat > {log_path_confined}) {src_path} {dest_path}"
try:
proc_confined = subprocess.run(
[bash, '-c', bash_script_confined],
capture_output=True,
text=True,
timeout=10,
)
except subprocess.TimeoutExpired:
rmtree(base)
test_fail('confined process substitution test timed out')
ctx_confined = f'rc={proc_confined.returncode}, stderr={proc_confined.stderr.strip()!r}'
# Rsync considers log-file failure a warning, so it still exits 0.
stderr_lower = proc_confined.stderr.lower()
if "no such file or directory" in stderr_lower and "failed to open" in stderr_lower:
if log_out_confined.exists() and log_out_confined.stat().st_size > 0:
rmtree(base)
test_fail(f'rsync printed an error but still wrote the confined log! ({ctx_confined})')
print('Test 2 Passed: rsync correctly rejected the pseudo-path when confine_root was active')
else:
rmtree(base)
test_fail(f'rsync failed to reject the pseudo-path or had an unexpected error ({ctx_confined})')
# A pseudo-path is valid only when its descriptor number is the final component.
rmtree(dest)
makepath(dest)
trailing_script = (
f'pipe_path=<(printf "transfer_me.txt\\n"); '
f'{rsync_base_cmd} --exclude-from="$pipe_path/trailing" {src_path} {dest_path}'
)
try:
proc_trailing = subprocess.run(
[bash, '-c', trailing_script],
capture_output=True,
text=True,
timeout=10,
)
except subprocess.TimeoutExpired:
rmtree(base)
test_fail('trailing-component pseudo-path test timed out')
if proc_trailing.returncode == 0:
rmtree(base)
test_fail('/dev/fd/N/trailing unexpectedly opened descriptor N')
if (dest / 'transfer_me.txt').exists():
rmtree(base)
test_fail('transfer continued after accepting a trailing pseudo-path component')
rmtree(base)
raise SystemExit(0)
-80
View File
@@ -1,80 +0,0 @@
#!/usr/bin/env python3
"""--read-batch process substitution /dev/fd/ pipe must not crash with strict file-type checks."""
import os
import shlex
import shutil
import subprocess
import tempfile
import sys
from pathlib import Path
from rsyncfns import SCRATCHDIR, makepath, rmtree, rsync_argv, test_fail, test_skipped
# We require bash specifically because standard POSIX /bin/sh does not
# guarantee support for <(...) process substitution syntax.
if not sys.platform.startswith('linux'):
test_skipped('This test requires Linux platform')
bash = shutil.which('bash')
if bash is None:
test_skipped('bash is unavailable, cannot test process substitution')
# Verify the host bash actually supports process substitution
probe = subprocess.run(
[bash, '-c', 'cat <(echo "probe")'],
capture_output=True)
if probe.returncode != 0:
test_skipped('bash process substitution is not supported on this system')
base = Path(SCRATCHDIR / 'rsync-batch-fifo')
src = base / 'src'
dest = base / 'dest'
batch_file = base / 'update.batch'
makepath(src, dest)
# 1. Create dummy data
(src / 'payload.txt').write_text('batch payload data\n')
# 2. Generate a valid batch file so `cat` actually has a real file to read.
# Note: This operation also copies the file to `dest` as a side effect.
subprocess.run([*rsync_argv('-a', f'--write-batch={batch_file}'), f'{src}/', f'{dest}/'], check=True)
# must wipe and recreate the destination directory so the test can
# properly prove that --read-batch recreates the files from scratch.
rmtree(dest)
makepath(dest)
# 3. Now we can test reading it via bash process substitution
rsync_base_cmd = shlex.join(rsync_argv('-a'))
batch_path = shlex.quote(str(batch_file))
dest_path = shlex.quote(str(dest) + '/')
# Construct the bash command: rsync -a --read-batch=<(cat /path/to/batch) /dest/
bash_script = f"{rsync_base_cmd} --read-batch=<(cat {batch_path}) {dest_path}"
try:
proc_read = subprocess.run(
[bash, '-c', bash_script],
capture_output=True,
text=True,
timeout=10,
)
except subprocess.TimeoutExpired:
rmtree(base)
test_fail('process substitution batch test timed out')
ctx = f'rc={proc_read.returncode}, stderr={proc_read.stderr.strip()!r}'
# Evaluate result against the strict S_ISREG check bug
if proc_read.returncode != 0:
rmtree(base)
test_fail(f'rsync crashed reading batch file from pipe ({ctx})')
if not (dest / 'payload.txt').is_file():
rmtree(base)
test_fail(f'rsync exited successfully but payload is missing in target ({ctx})')
rmtree(base)
print('rsync successfully parsed batch stream via process substitution pseudo-path')
raise SystemExit(0)
@@ -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")
-133
View File
@@ -1,133 +0,0 @@
#!/usr/bin/env python3
"""A confined fd pin must remain usable inside a Linux user namespace."""
import os
import shlex
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
from rsyncfns import makepath, rmtree, rsync_argv, test_fail, test_skipped
if not sys.platform.startswith('linux'):
test_skipped('rrsync-userns-procfs is Linux-specific')
if not os.environ.get('RSYNC_USERNS_PROCFS'):
unshare = shutil.which('unshare')
if unshare is None:
test_skipped('unshare is unavailable')
env = os.environ.copy()
env['RSYNC_USERNS_PROCFS'] = '1'
launch_dir = Path(tempfile.mkdtemp(prefix='rsync-userns-launch-'))
launch_dir.chmod(0o755)
testdir = Path(__file__).resolve().parent
child_test = launch_dir / Path(__file__).name
for source in (Path(__file__), testdir / 'rsyncfns.py',
testdir / 'exitcodes.py'):
shutil.copy2(source, launch_dir / source.name)
rsync_cmd = shlex.split(env['RSYNC'])
for i, arg in enumerate(rsync_cmd):
if Path(arg).name in ('rsync', 'rsync.exe') and Path(arg).is_file():
staged_rsync = launch_dir / Path(arg).name
shutil.copy2(arg, staged_rsync)
staged_rsync.chmod(0o755)
rsync_cmd[i] = str(staged_rsync)
break
else:
rmtree(launch_dir)
test_fail(f'cannot locate the rsync executable in {env["RSYNC"]!r}')
env['RSYNC'] = shlex.join(rsync_cmd)
launcher = []
if os.geteuid() == 0:
setpriv = shutil.which('setpriv')
if setpriv is None:
test_skipped('setpriv is unavailable for the root-run testsuite')
launcher = [setpriv, '--reuid=65534', '--regid=65534', '--clear-groups']
unshare_argv = [unshare, '--user', '--map-root-user', '--mount', '--pid',
'--fork', '--mount-proc']
probe = subprocess.run(
launcher + unshare_argv + ['true'],
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
if probe.returncode != 0:
rmtree(launch_dir)
print(f'user namespaces unavailable (rc={probe.returncode})')
raise SystemExit(0)
try:
proc = subprocess.run(
launcher + unshare_argv
+ [sys.executable, str(child_test)],
env=env,
timeout=30,
)
except subprocess.TimeoutExpired:
test_fail('user-namespace regression test timed out')
finally:
rmtree(launch_dir)
if proc.returncode != 0:
test_fail(f'user-namespace regression test failed (rc={proc.returncode})')
print('rrsync fd pin works inside a user namespace')
raise SystemExit(0)
proc_uid = os.lstat('/proc/self').st_uid
if proc_uid in (0, os.geteuid()):
test_skipped('/proc/self does not expose an overflow uid in this namespace')
base = Path(tempfile.mkdtemp(prefix='rsync-userns-procfs-'))
src = base / 'src'
dest = base / 'dest'
outside = base / 'outside'
makepath(src, dest, outside)
(src / 'file').write_text('content\n')
fd_roots = ['/proc/self/fd']
if Path('/dev/fd').exists():
fd_roots.append('/dev/fd')
dest_fd = os.open(dest, os.O_RDONLY | os.O_DIRECTORY)
try:
for index, fd_root in enumerate(fd_roots):
log_file = dest / f'rsync-{index}.log'
proc = subprocess.run(
rsync_argv('-a', f'--confine-root={dest}',
f'--log-file={fd_root}/{dest_fd}/{log_file.name}',
str(src) + '/', str(dest) + '/'),
pass_fds=(dest_fd,),
capture_output=True,
text=True,
)
ctx = (f'fd_root={fd_root!r}, rc={proc.returncode}, '
f'stderr={proc.stderr.strip()[:300]!r}')
if proc.returncode != 0:
test_fail(f'confined transfer through an fd pin failed ({ctx})')
if not log_file.is_file():
test_fail(f'confined log path through an fd pin was rejected ({ctx})')
if (dest / 'file').read_text() != 'content\n':
test_fail(f'confined transfer did not deliver the file ({ctx})')
finally:
os.close(dest_fd)
outside_list = outside / 'files-from'
outside_list.write_text('file\n')
for fd_root in fd_roots:
outside_fd = os.open(outside_list, os.O_RDONLY)
try:
proc = subprocess.run(
rsync_argv('-a', f'--confine-root={dest}',
f'--files-from={fd_root}/{outside_fd}',
str(src) + '/', str(dest) + '/'),
pass_fds=(outside_fd,),
capture_output=True,
text=True,
)
finally:
os.close(outside_fd)
if proc.returncode == 0 or 'failed to open files-from file' not in proc.stderr:
test_fail(f'outside {fd_root} pin was not observably refused: '
f'rc={proc.returncode}, stderr={proc.stderr!r}')
rmtree(base)
-95
View File
@@ -1,95 +0,0 @@
#!/usr/bin/env python3
# rsync-ssl only recognized --type=SSL_TYPE as the FIRST argument, so
# "rsync-ssl --dry-run --type=stunnel host::mod" passed the option through to
# the underlying rsync, which rejected it with "--type=stunnel: unknown option".
# Fix: scan the whole argument list for --type=..., export RSYNC_SSL_TYPE, and
# drop the option before handing the remaining args to rsync.
# A `--` arg stops the wrapper-option scan: `--` and everything after it are
# passed through to rsync verbatim, so an operand such as `--type=stunnel`
# that was protected from option parsing is not consumed by the wrapper.
#
# A fake rsync in PATH records the args it receives and the RSYNC_SSL_TYPE it
# observes; rsync-ssl is run in its normal (non-HELPER) mode with --type= in
# various positions. The recorded args must contain every other option but
# never a --type= token, RSYNC_SSL_TYPE must match what the wrapper consumed,
# and rsync-ssl must exit successfully.
import os
import subprocess
from rsyncfns import SCRATCHDIR, SRCDIR, rmtree, test_fail
base = SCRATCHDIR / 'rsync-ssl-type-opt'
rmtree(base)
base.mkdir(parents=True)
args_capture = base / 'rsync_args'
type_capture = base / 'rsync_ssl_type'
fakebin = base / 'bin'
fakebin.mkdir(parents=True)
fake_rsync = fakebin / 'rsync'
fake_rsync.write_text(
f"#!/usr/bin/env bash\n"
f"printf '%s\\n' \"$@\" > {args_capture}\n"
f"printf '%s\\n' \"${{RSYNC_SSL_TYPE-UNSET}}\" > {type_capture}\n"
f"exit 0\n")
fake_rsync.chmod(0o755)
env = {**os.environ, 'PATH': str(fakebin) + os.pathsep + os.environ.get('PATH', '')}
for v in ('RSYNC_SSL_TYPE', 'RSYNC_SSL_OPENSSL', 'RSYNC_SSL_STUNNEL'):
env.pop(v, None)
def run(args, expect_type):
for capture in (args_capture, type_capture):
if capture.exists():
capture.unlink()
proc = subprocess.run(['bash', str(SRCDIR / 'rsync-ssl')] + args, env=env,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
if proc.returncode != 0:
test_fail(f"rsync-ssl exited {proc.returncode} for args {args!r}:\n{proc.stdout}")
got_args = args_capture.read_text().splitlines() if args_capture.exists() else []
got_type = type_capture.read_text().strip() if type_capture.exists() else 'UNSET'
if got_type != expect_type:
test_fail(f"RSYNC_SSL_TYPE is {got_type!r}, expected {expect_type!r} "
f"for args {args!r}:\n{proc.stdout}")
return got_args
# --- The reported failure: --type= in the middle of the rsync args.
got = run(['--dry-run', '--type=stunnel', 'host::mod'], 'stunnel')
if any(a.startswith('--type=') for a in got):
test_fail(f"--type= was passed through to rsync instead of being consumed:\n{got}")
for want in ('--dry-run', 'host::mod'):
if want not in got:
test_fail(f"missing rsync arg {want!r} after --type= handling:\n{got}")
if not any(a.startswith('--rsh=') for a in got):
test_fail(f"missing the --rsh= helper option:\n{got}")
# --- First, last, and no --type= keep working.
for pos_args, expect_type in ((['--type=stunnel', '--dry-run', 'host::mod'], 'stunnel'),
(['-av', 'host::mod', '--type=openssl'], 'openssl'),
(['-av', 'host::mod'], 'UNSET')):
got = run(pos_args, expect_type)
if any(a.startswith('--type=') for a in got):
test_fail(f"--type= was passed through for args {pos_args!r}:\n{got}")
for want in pos_args:
if want.startswith('--type='):
continue
if want not in got:
test_fail(f"missing rsync arg {want!r} for args {pos_args!r}:\n{got}")
# --- `--` stops the wrapper-option scan: the protected operand is preserved.
rsh_arg = "--rsh='{}' --HELPER".format(SRCDIR / 'rsync-ssl')
got = run(['--', '--type=stunnel', 'host::mod'], 'UNSET')
if got != [rsh_arg, '--', '--type=stunnel', 'host::mod']:
test_fail(f"args after -- must be preserved verbatim (no --type= consumed):\n{got}")
# --- A wrapper option before `--` is still consumed; the protected one is not.
got = run(['--type=openssl', '--', '--type=stunnel', 'host::mod'], 'openssl')
if got != [rsh_arg, '--', '--type=stunnel', 'host::mod']:
test_fail(f"--type= before -- is consumed, args after -- are preserved:\n{got}")
print("rsync-ssl-type-option: --type=SSL_TYPE is consumed in any argument "
"position (until a -- stops the wrapper-option scan), exported as "
"RSYNC_SSL_TYPE, and rsync-ssl exits successfully")
+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
-77
View File
@@ -1,77 +0,0 @@
#!/usr/bin/env python3
"""The receiver must traverse a searchable but unreadable destination parent.
Android exposes /sdcard through such a path, so the race-safe destination walk
must use directory descriptors that require search permission only.
"""
import os
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
from rsyncfns import SCRATCHDIR, rmtree, rsync_argv, test_fail, test_skipped
if not sys.platform.startswith('linux'):
test_skipped('search-only-destination is Linux-specific')
launcher = []
if os.geteuid() == 0:
setpriv = shutil.which('setpriv')
if setpriv is None:
test_skipped('setpriv is unavailable for the root-run testsuite')
launcher = [setpriv, '--reuid=65534', '--regid=65534', '--clear-groups']
external_base = os.geteuid() == 0
if external_base:
base = Path(tempfile.mkdtemp(prefix='rsync-search-only-'))
base.chmod(0o755)
else:
base = SCRATCHDIR / 'search-only-destination'
src = base / 'src'
parent = base / 'search-only'
dest = parent / 'dest'
rmtree(base)
src.mkdir(parents=True)
dest.mkdir(parents=True)
(src / 'probe').write_text('search-only destination\n')
if os.geteuid() == 0:
for path in (src, src / 'probe', dest):
os.chown(path, 65534, 65534)
try:
parent.chmod(0o111)
try:
probe = subprocess.run(
launcher + ['test', '-r', str(parent)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
if probe.returncode == 0:
test_skipped('filesystem does not enforce the search-only test mode')
if probe.returncode != 1:
test_fail(f'search-only permission probe failed with exit {probe.returncode}')
proc = subprocess.run(
launcher + rsync_argv('-a', f'{src}/', f'{dest}/'),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
finally:
parent.chmod(0o755)
copied = (dest / 'probe').read_text() if (dest / 'probe').is_file() else None
finally:
if external_base:
rmtree(base)
if proc.returncode != 0:
test_fail(
'receiver could not enter a destination below a searchable, unreadable '
f'parent (exit {proc.returncode}): {proc.stderr.strip()}'
)
if copied != 'search-only destination\n':
test_fail('receiver did not copy into the search-only destination')
-215
View File
@@ -1,215 +0,0 @@
#!/usr/bin/env python3
"""Known-name operations must not require permission to list parent dirs.
The confined resolver holds directory descriptors to prevent symlink races.
On Linux, those traversal and *at() anchor descriptors can use O_PATH: opening
a known file beneath a searchable directory, or creating one beneath a
writable/searchable directory, does not require directory read permission.
"""
import os
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
from rsyncfns import (
SCRATCHDIR, forced_protocol, rmtree, rsync_argv, test_fail, test_skipped,
)
if not sys.platform.startswith('linux'):
test_skipped('search-only held-dirfd coverage is Linux-specific')
launcher = []
if os.geteuid() == 0:
setpriv = shutil.which('setpriv')
if setpriv is None:
test_skipped('setpriv is unavailable for the root-run testsuite')
launcher = [setpriv, '--reuid=65534', '--regid=65534', '--clear-groups']
external_base = os.geteuid() == 0
base = (
Path(tempfile.mkdtemp(prefix='rsync-search-only-held-dirfd-'))
if external_base
else SCRATCHDIR / 'search-only-held-dirfd'
)
rmtree(base)
src = base / 'src'
xonly = src / 'xonly'
readable = xonly / 'readable'
nested_src = src / 'nested'
exact_dest = base / 'exact-dest'
tree_dest = base / 'tree-dest'
unreadable_dest = base / 'unreadable-dest'
write_only_dest = base / 'write-only-dest'
nested_dest = base / 'nested-dest'
nested_parent = nested_dest / 'nested'
for path in (
readable,
nested_src,
exact_dest,
tree_dest,
unreadable_dest,
write_only_dest,
nested_parent,
):
path.mkdir(parents=True, exist_ok=True)
(xonly / 'exact').write_text('known file beneath search-only parent\n')
(readable / 'nested').write_text('enumerated below search-only ancestor\n')
incoming = src / 'incoming'
incoming.write_text('created beneath write-search-only destination\n')
(nested_src / 'known').write_text(
'created beneath nested write-search-only parent\n'
)
if os.geteuid() == 0:
for root, dirs, files in os.walk(base):
os.chown(root, 65534, 65534)
for name in dirs + files:
os.chown(Path(root) / name, 65534, 65534)
def permission_probe(path, flag, expected, label):
proc = subprocess.run(
launcher + ['test', flag, str(path)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
if proc.returncode != expected:
test_skipped(
f'filesystem does not enforce {label}: test {flag} returned '
f'{proc.returncode}, expected {expected}'
)
failures = []
try:
xonly.chmod(0o111)
write_only_dest.chmod(0o333)
nested_parent.chmod(0o333)
permission_probe(xonly, '-r', 1, 'search-only mode')
permission_probe(xonly, '-x', 0, 'search-only mode')
permission_probe(write_only_dest, '-r', 1, 'write-search-only mode')
permission_probe(write_only_dest, '-w', 0, 'write-search-only mode')
permission_probe(write_only_dest, '-x', 0, 'write-search-only mode')
permission_probe(nested_parent, '-r', 1, 'nested write-search-only mode')
permission_probe(nested_parent, '-w', 0, 'nested write-search-only mode')
permission_probe(nested_parent, '-x', 0, 'nested write-search-only mode')
# Keep received implied dirs usable on systems without a safe fchmodat2.
# The source remains mode 0111, so sender traversal coverage is unchanged.
exact = subprocess.run(
launcher + rsync_argv(
'-aR', '--chmod=Du+rw', 'xonly/exact', f'{exact_dest}/',
),
cwd=src,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
exact_path = exact_dest / 'xonly' / 'exact'
exact_content = exact_path.read_text() if exact_path.is_file() else None
if exact.returncode != 0 or exact_content != (
'known file beneath search-only parent\n'
):
failures.append(
'exact -R source beneath mode 0111 failed: '
f'rc={exact.returncode}, stderr={exact.stderr.strip()!r}, '
f'content={exact_content!r}'
)
tree = subprocess.run(
launcher + rsync_argv(
'-aR', '--chmod=Du+rw', 'xonly/readable/', f'{tree_dest}/',
),
cwd=src,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
tree_path = tree_dest / 'xonly' / 'readable' / 'nested'
tree_content = tree_path.read_text() if tree_path.is_file() else None
if tree.returncode != 0 or tree_content != (
'enumerated below search-only ancestor\n'
):
failures.append(
'readable directory beneath mode 0111 ancestor failed: '
f'rc={tree.returncode}, stderr={tree.stderr.strip()!r}, '
f'content={tree_content!r}'
)
unreadable = subprocess.run(
launcher + rsync_argv(
'-a', 'xonly/', f'{unreadable_dest}/',
),
cwd=src,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
if unreadable.returncode == 0:
failures.append(
'mode 0111 source directory was enumerable without read permission'
)
receiver = subprocess.run(
launcher + rsync_argv(
'-t', str(incoming), f'{write_only_dest}/',
),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
received = write_only_dest / 'incoming'
received_content = received.read_text() if received.is_file() else None
if receiver.returncode != 0 or received_content != (
'created beneath write-search-only destination\n'
):
failures.append(
'known-file creation beneath mode 0333 destination failed: '
f'rc={receiver.returncode}, stderr={receiver.stderr.strip()!r}, '
f'content={received_content!r}'
)
# Protocol 29 rejects this nested -R shape before the resolver is reached.
proto = forced_protocol()
if proto is None or proto >= 30:
nested_receiver = subprocess.run(
launcher + rsync_argv(
'-tR', '--no-implied-dirs', 'nested/known', f'{nested_dest}/',
),
cwd=src,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
nested_received = nested_parent / 'known'
nested_content = (
nested_received.read_text() if nested_received.is_file() else None
)
if nested_receiver.returncode != 0 or nested_content != (
'created beneath nested write-search-only parent\n'
):
failures.append(
'known-file creation beneath nested mode 0333 destination '
f'failed: rc={nested_receiver.returncode}, '
f'stderr={nested_receiver.stderr.strip()!r}, '
f'content={nested_content!r}'
)
finally:
xonly.chmod(0o755)
write_only_dest.chmod(0o755)
nested_parent.chmod(0o755)
for dest in (exact_dest, tree_dest):
copied_xonly = dest / 'xonly'
if copied_xonly.is_dir():
copied_xonly.chmod(0o755)
if external_base:
rmtree(base)
if failures:
test_fail('\n'.join(failures))
@@ -48,10 +48,6 @@ try:
os.utime(inside_file, (st.st_atime, st.st_mtime))
except FileNotFoundError:
pass
except PermissionError:
# Cygwin can report EACCES while the flipper swaps this path.
if not _CYGWIN:
raise
subprocess.run(
rsync_argv('-a', '--remove-source-files', f'{url}src/real/file', str(dest) + '/'),
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, text=True)
-1
View File
@@ -23,7 +23,6 @@ different tests merge cleanly.
| file | contents |
| --- | --- |
| `common.txt` | skipped on every platform that runs the oracle — mostly `require_tcp` / `require_asan` tests, which the default stdio-pipe `make check` cannot satisfy |
| `almalinux-8.txt` | AlmaLinux 8 container additions |
| `linux.txt` | Linux-only additions |
| `macos.txt` | macOS-only additions |
| `cygwin.txt` | Cygwin-only additions |
-9
View File
@@ -1,9 +0,0 @@
# Tests expected to SKIP. One name per line, '#' starts a comment; the file
# must stay sorted and duplicate-free (runtests.py enforces both). Referenced
# from a workflow as RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/<file>[,@...].
# See testsuite/skiplist/README.md.
#
# AlmaLinux 8 container additions to common.txt and linux.txt.
pseudo-paths # Bash process substitution is unavailable in the AlmaLinux 8 container
read-batch-pipe
-1
View File
@@ -11,7 +11,6 @@
checksum-zero-blocklen # the pure-Python receiver needs a real TCP socket; run with --use-tcp
chroot-basis-forge-inner-module # the pure-Python sender needs a real TCP daemon; run with --use-tcp
daemon-access-idn # hosts allow/deny hostname matching needs a real TCP peer
daemon-access-ip # hosts allow/deny address matching needs a real TCP peer
daemon-argv-limit # raw malicious daemon client needs a real TCP daemon; run with --use-tcp
daemon-chroot # daemon chroot path needs the real start_daemon socket flow
+3 -5
View File
@@ -25,6 +25,7 @@ copy-xattrs-symlink-race
daemon-auth-group
daemon-chroot-munge-default
daemon-config-symlink
daemon-max-alloc-zero
daemon-module-chdir-symlink
daemon-module-private-parent
daemon-secrets-file-symlink
@@ -44,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
@@ -51,19 +53,15 @@ partial-protected-regular-retry-linux
partial-protected-regular-retry-policy # deterministic partial EACCES recovery uses dyld interposing
password-file-symlink
protected-regular
pseudo-paths
read-batch-pipe
rename-mixed-parent-transfer
rrsync-sender-leaf-flip
rrsync-sender-parent-pin
rrsync-symlink
rrsync-userns-procfs
search-only-destination
search-only-held-dirfd
sender-remove-source-root-anchor
simd-checksum
source-change-size-continues
symlink-dest-backupdir
symlink-exclude-xattr
symlink-race-dest
symlink-race-relative-dest
temp-dir-symlink-injection
+2 -5
View File
@@ -14,22 +14,19 @@ backup-crossdev-copy
chmod-temp-dir
copy-xattrs-symlink-race
daemon-auth-group
daemon-max-alloc-zero
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
pseudo-paths # dynamically skips on runners lacking bash process substitution
read-batch-pipe
readonly-partial-abort-mode-regression #
rrsync-sender-leaf-flip
rrsync-sender-parent-pin
rrsync-symlink
rrsync-userns-procfs
search-only-destination
search-only-held-dirfd
sender-remove-source-root-anchor
simd-checksum
source-change-size-continues
+2 -14
View File
@@ -50,20 +50,8 @@ dest.mkdir(parents=True)
os.symlink(outside, dest / 'sub') # attacker-owned dest component
os.lchown(dest / 'sub', ATT_UID, ATT_UID)
proc = subprocess.run(
rsync_argv('-a', f'{src}/sub/', f'{dest}/sub/'),
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
text=True,
)
if proc.returncode == 0:
test_fail("attacker-owned destination symlink was not rejected")
if "refusing to follow a symlink owned by an untrusted user" not in proc.stderr:
test_fail(
"untrusted destination symlink failure omitted the actionable "
f"diagnostic: {proc.stderr!r}"
)
subprocess.run(rsync_argv('-a', f'{src}/sub/', f'{dest}/sub/'),
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
escaped = sorted(p.name for p in outside.iterdir())
if escaped:
+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;
-5
View File
@@ -156,11 +156,6 @@ static void print_info_flags(enum logcode f)
#endif
"crtimes",
#ifndef SUPPORT_IDN
"no "
#endif
"IDN",
"*Optimizations",
#ifndef USE_ROLL_SIMD
+50 -584
View File
@@ -24,9 +24,6 @@
#include "ifuncs.h"
#include "itypes.h"
#include "inums.h"
#ifdef SUPPORT_IDN
#include <idn2.h>
#endif
extern int dry_run;
extern int module_id;
@@ -37,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;
@@ -45,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. */
@@ -136,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;
@@ -145,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;
@@ -154,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;
@@ -171,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
@@ -192,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)
@@ -204,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.
@@ -318,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;
@@ -804,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)
@@ -918,110 +480,16 @@ void glob_expand_module(char *base1, char *arg, char ***argv_p, int *argc_p, int
/**
* Convert a string to lower case
*
* Only ASCII is folded. The hosts allow/deny list that calls this can hold
* UTF-8, and a per-byte fold via the locale's ctype would mangle it (in
* ISO-8859-1 the 0xC4 lead byte of "č" is an upper-case 'Ä').
**/
void strlower(char *s)
{
while (*s) {
if (!(*(unsigned char *)s & 0x80) && isUpper(s))
if (isUpper(s))
*s = toLower(s);
s++;
}
}
#ifdef SUPPORT_IDN
/* Does this label hold nothing but the [-a-z0-9] of an A-label? */
static int is_a_label(const char *s)
{
if (!*s)
return 0;
for ( ; *s; s++) {
if (!(*s >= 'a' && *s <= 'z') && !(*s >= '0' && *s <= '9') && *s != '-')
return 0;
}
return 1;
}
/**
* Convert the non-ASCII labels of a host name into their IDNA A-label
* (Punycode) form, putting the result in buf. Returns 1 if buf was filled in,
* or 0 to tell the caller to keep the name it has.
*
* A label that is already ASCII is copied verbatim, so an address, a mask, an
* xn-- name, and any wildmatch characters come out just as they went in. A
* converted label is only used if it comes back as a bare A-label: the IDNA
* mapping folds some non-ASCII characters onto ASCII ones (U+FF0A FULLWIDTH
* ASTERISK becomes '*'), and a hosts allow/deny entry must not pick up a
* wildcard that its author never typed. Anything else leaves the name alone,
* which fails to match instead of matching too much.
*
* Set from_locale for a name that came from the command line, which is in the
* user's locale encoding; the daemon's config file is read as UTF-8.
**/
int idn_to_ascii(const char *name, int from_locale, char *buf, size_t buflen)
{
const char *lab, *end;
size_t len = 0;
int converted = 0;
for (lab = name; ; lab = end + 1) {
char label[256], *idn;
size_t lablen, alen;
int is_ascii = 1;
for (end = lab; *end && *end != '.'; end++) {
if (*(unsigned char *)end & 0x80)
is_ascii = 0;
}
lablen = end - lab;
if (is_ascii) {
if (len + lablen + 2 > buflen)
return 0;
memcpy(buf + len, lab, lablen);
len += lablen;
} else {
/* IDN2_NFC_INPUT has libidn2 normalize the label, so a name
* typed with combining marks folds to the same A-label as
* its composed spelling. IDN2_NONTRANSITIONAL asks for the
* TR46 processing that everything else does these days. */
int flags = IDN2_NFC_INPUT | IDN2_NONTRANSITIONAL;
int rc;
if (lablen >= sizeof label)
return 0;
memcpy(label, lab, lablen);
label[lablen] = '\0';
rc = from_locale ? idn2_lookup_ul(label, &idn, flags)
: idn2_to_ascii_8z(label, &idn, flags);
if (rc != IDN2_OK)
return 0;
alen = strlen(idn);
if (!is_a_label(idn) || len + alen + 2 > buflen) {
idn2_free(idn);
return 0;
}
memcpy(buf + len, idn, alen);
len += alen;
idn2_free(idn);
converted = 1;
}
if (!*end)
break;
buf[len++] = '.';
}
buf[len] = '\0';
return converted;
}
#endif
/**
* Split a string into tokens based (usually) on whitespace & commas. If the
* string starts with a comma (after skipping any leading whitespace), then
@@ -1308,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)
{
@@ -1318,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 */
@@ -1333,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
@@ -1349,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_dirfd(dir);
int dfd = vfs_open_owner_walk(dir, O_RDONLY | O_DIRECTORY, 0, 0);
if (dfd < 0)
return 0;
if (fchdir(dfd) != 0) {
@@ -1380,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_dirfd(nf);
dfd = vfs_open_owner_walk(nf, O_RDONLY | O_DIRECTORY, 0, 0);
if (dfd < 0)
return 0;
if (fchdir(dfd) != 0) {
@@ -1396,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;
@@ -1414,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;
@@ -1437,29 +905,31 @@ 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_dirfd(basedir, dir);
dfd = vfs_resolve_open(basedir, dir,
O_RDONLY | O_DIRECTORY, 0);
if (dfd < 0) {
chdir_failed = 1;
} else {
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_dirfd(curr_dir);
int dfd = vfs_open_owner_walk(vfs.curr_dir,
O_RDONLY | O_DIRECTORY, 0, 0);
if (dfd < 0)
chdir_failed = 1;
else {
@@ -1467,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;
}
@@ -1503,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);
@@ -1540,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 = "/";
@@ -1610,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;
}
Loaded 100 of 117 files, more files were not shown because too many files have changed in this diff. Show more