Skip to content

add fileflags option for BSD and others#894

Open
tridge wants to merge 11 commits into
RsyncProject:masterfrom
tridge:pr-fileflags
Open

add fileflags option for BSD and others#894
tridge wants to merge 11 commits into
RsyncProject:masterfrom
tridge:pr-fileflags

Conversation

@tridge
Copy link
Copy Markdown
Member

@tridge tridge commented May 19, 2026

This is based on the fileflags.patch in the rsync-patches archive
needs a bit more work before merging

todo:

  • --force option is removed, breaking existing usage
  • need to carefully look at security implications
  • possibly support on linux with chattr (ie. EXT2_IOC_SETFLAGS or similar)
  • need to change path based calls to fd based calls, with RESOLVE_BENEATH

Squash of pr-fileflags work, rebased onto 3.4.3.  Original branch had
12 commits walking through:

  1. apply rsync-patches/fileflags.diff (BSD-only --fileflags option)
  2. testsuite/fileflags.test
  3. CI fixes
  4. portable stat -f %f readback
  5. phase-1 security: fd-based chflags, sender filter, daemon refuse
  6. phase-2 security: dirfd-anchored force_change recovery
  7. phase-3 security: secure_relative_open bound to dest subtree
  8. --force-change rebalance: default to USR_IMMUTABLE only
  9. set_refuse_options POPT_BIT_SET fix
 10. user-visible regressions on non-chflags builds
 11. t_stub fd-based stubs + weak curr_dir
 12. Linux port via FS_IOC_{GET,SET}FLAGS

Squashed and rebased because 3.4.3 (CVE-2026-29518 + family) overlaps
heavily with the security work in phases 1-3 -- master added
do_chmod_at / do_lchown_at / secure_relative_open hardening that
duplicates the bits the fileflags patch needed.  The fileflags work
keeps the original do_chmod / do_lchown signatures (no UNUSED arg
addition), so master's _at variants and call sites are unchanged.

Resulting feature set:

  --fileflags        preserve file flags (chflags on BSD,
                     chattr {+d, +i, +a} on Linux).  SAFE_FILEFLAGS
                     mask (UF_NODUMP|UF_IMMUTABLE|UF_APPEND[|UF_HIDDEN])
                     applied by default; sender-supplied SF_* and
                     UF_NOUNLINK dropped to avoid DoS where a hostile
                     source pins permanent flags on the receiver.
  --unsafe-fileflags widens the mask to the full sender value.
  --force-change     clear USR_IMMUTABLE (UF_*) on dest files being
                     updated/deleted so the op can proceed.
  --force-uchange    alias for --force-change.
  --force-schange    also clear SYS_IMMUTABLE (SF_*); separate opt-in.
  --no-force-{u,s,}change to clear bits.

Implementation:

  - lib/fileflags.c: portable rsync_fchflags / rsync_fgetflags /
    rsync_lgetflags / stat_x_get_fileflags + BSD<->Linux bit
    translation.  Wire format is BSD bits.  Linux side does
    read-modify-write of just LINUX_WIRE_MASK so the kernel doesn't
    reject the call when fs-internal bits like FS_EXTENT_FL would
    otherwise be cleared.
  - stat_x grows a (fileflags, fileflags_cached) pair so the per-file
    open()+ioctl cost on Linux happens at most once per stat_x life.
    init_stat_x() in ifuncs.h zeroes both.
  - syscall.c do_unlink / do_rmdir / do_chmod / do_lchown / do_rename
    grow dirfd-anchored force_change recovery using
    force_change_open_parent / force_change_open_target / fchflags /
    fchmod / fchown / unlinkat / renameat.  The recovery opens via
    secure_relative_open(curr_dir, dirpart, O_RDONLY|O_DIRECTORY|
    O_NOFOLLOW) so RESOLVE_BENEATH (where available) bounds the
    operation to the destination subtree.  Symlinks rejected at open.
  - Daemon mode refuses fileflags / unsafe-fileflags / force-change /
    force-uchange / force-schange / no-force-uchange / no-force-schange
    by default; opt-in per-module via "refuse options = !fileflags".
  - set_refuse_options POPT_BIT_SET / POPT_BIT_CLR fix: the original
    rsync check was `op->argInfo == POPT_ARG_VAL` literal, which
    missed POPT_BIT_SET (= POPT_ARG_VAL|POPT_ARGFLAG_OR).  Refused
    bit-set options slipped through.  Now masks via POPT_ARG_MASK.
  - testsuite/fileflags.test picks chflags(1) or chattr(1) depending
    on what's available; on Linux it parses lsattr down to the
    transferable letters (a, d, i, u).  On Linux non-root the uchg
    portion self-skips (CAP_LINUX_IMMUTABLE required).
  - CI: fileflags removed from RSYNC_EXPECT_SKIPPED on Linux jobs;
    Cygwin keeps it (no chattr).
  - Linux ioctl path is gated behind autoconf check for
    FS_IOC_GETFLAGS / FS_IOC_SETFLAGS / FS_NODUMP_FL / FS_IMMUTABLE_FL
    / FS_APPEND_FL in <linux/fs.h>; falls through to "no fileflags
    support" if absent (older kernels, non-Linux non-BSD).

Verified on Linux/ext4: 59 passed / 2 skipped / 0 failed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tridge and others added 10 commits May 20, 2026 12:22
…ect#4

RsyncProject#1 (high, security): set_fileflags() in rsync.c was using bare
open(fname, O_NOFOLLOW), which only protects the *final* path
component.  Parent components could still be swapped to symlinks
between calls -- the exact attack class CVE-2026-29518 was about,
just on the --fileflags / --force-change code path that the previous
hardening missed.

Switch to secure_relative_open(curr_dir, fname, ...) for relative
paths so the full path chain is bounded by RESOLVE_BENEATH
(openat2 on Linux 5.6+, openat() with O_RESOLVE_BENEATH on FreeBSD
13+ / macOS 15+, per-component O_NOFOLLOW walk elsewhere).  On the
receiver, curr_dir is the destination root.  Absolute paths still
fall back to a plain open(O_NOFOLLOW) -- no basedir context for
them and they don't occur on the normal receiver path.

RsyncProject#4: drop the --force-delete alias entirely.  Operators who already
have "refuse options = force" in rsyncd.conf would silently fail to
refuse --force-delete (it sets the same force_delete variable but
under a different popt name).  --force is and stays the documented
name, so the alias was only ever a back-compat hold-over from the
original fileflags patch's renaming.  Remove from both options.c
and the man-page detailed entry header.

Verified on Linux/ext4: full suite 59 passed / 2 skipped / 0 failed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ect#3

RsyncProject#2 (medium, correctness): the do_*_at() wrappers used by the receiver
side bypassed the force_change EPERM-recovery I'd added in plain
do_unlink / do_rmdir / do_chmod / do_lchown / do_rename.

- do_unlink_at / do_rmdir_at:
  - Secure dirfd path: on EPERM + force_change, call new shared
    helper force_change_retry_unlinkat() which uses the existing
    dirfd to openat(O_NOFOLLOW) the target, fchflags-clear via the
    fd, retry unlinkat with the same dirfd, restore on failure.
  - Non-secure fallback paths (non-daemon / chrooted / no-parent /
    absolute / AT_FDCWD missing) were calling bare unlink() / rmdir();
    rerouted through do_unlink() / do_rmdir() so the recovery there
    still fires.
- do_chmod_at / do_lchown_at:
  - Secure dirfd path: inline the same fd-based recovery on EPERM
    +force_change.  fd was opened O_NOFOLLOW so fchmod/fchown is
    safe (not a symlink).
  - Non-secure fallback already went through do_chmod / do_lchown
    which have recovery -- unchanged.
- do_rename_at: secure-path recovery is involved (two-side make-
  mutable plus restore-on-the-renamed-inode bookkeeping that do_rename
  already implements), so on EPERM + force_change just fall back to
  do_rename(old_path, new_path).  The fallback's
  force_change_open_parent uses secure_relative_open(curr_dir, ...)
  which on the receiver bounds to the same destination subtree as the
  dirfds we just closed -- error-recovery path only.

RsyncProject#3 (medium, security): make_mutable() calls in delete.c were not
paired with undo_make_mutable() on the paths where the delete didn't
actually take place (max-delete cap, non-empty dir, do_rmdir EPERM
after recovery, etc.), leaking files in a less-protected state.

- delete_dir_contents per-entry make_mutable: track entry_unmuted +
  entry_saved_flags; if delete_item returns anything other than
  DR_SUCCESS, undo_make_mutable on the entry.
- delete_item per-directory make_mutable (the eager clear before
  delete_dir_contents recursion): track dir_unmuted + dir_saved_flags
  hoisted to function scope; check_ret restores on the !DR_SUCCESS
  path.  Changed the early `return DR_AT_LIMIT` to `goto check_ret`
  so the max-delete skip also flows through the restore.

Verified on Linux/ext4: 59 passed / 2 skipped / 0 failed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three follow-ups after the round-1 codex review caught new issues
introduced by the round-1 fixes.

RsyncProject#3 (medium, correctness): stat_x_get_fileflags(&sx, fname) in
generator.c:1481 was being called without checking statret == 0.
For missing destinations sx.st has not been populated; on BSD,
rsync_lgetflags() returns hint->st_flags, which is uninitialized
stack memory.  That garbage was being stashed in F_FFLAGS(file) and
later applied/restored by make_mutable / undo_make_mutable on the
freshly-created dir.  Gate on statret == 0, zero F_FFLAGS otherwise.

Also tightened the recv_generator dir-creation make_mutable call from
truthy-check to `> 0` so a make_mutable failure (-1) doesn't
incorrectly flag need_retouch_dir_perms and trigger a bogus
undo_make_mutable in touch_up_dirs.

RsyncProject#1 (high, security): the do_rename_at force_change EPERM-recovery
that fell back to do_rename(old_path, new_path) reopened the
symlink-race attack class on daemon "use chroot = no": do_rename's
first action is bare rename(old, new) with raw paths, before its own
recovery runs, so an attacker who swaps a parent dir between the
secure dirfd close and the fallback rename can redirect the retry
outside the module.

Replaced with a proper two-side recovery that stays entirely on the
already-open secure dirfds: openat(O_NOFOLLOW) the old-side target,
fchflags-clear, renameat through the held dirfds; if that fails, do
the same on the new-side target.  On success, restore the original
flags via old_fd (the fd survives the rename and still refers to the
inode that now lives at new_bname).  Mirrors do_rename's recovery
logic but with every operation anchored on the secure dirfds.

RsyncProject#2 (medium/high, security): delete_dir_contents's per-entry
make_mutable cleared immutable flags before delete_item ran, but
delete_item's backup path (make_backup -> do_rename_at or
do_link_at + unlink) moves the inode out from under us -- and my
undo_make_mutable(fname, ...) is path-based so it can only restore
flags at the now-empty original path, never at the backup location
the inode actually ended up at.  Net effect: a successful backed-up
delete of a uchg'd file silently strips the uchg from the backup
copy.

Fix: only pre-clear flags on SUB-DIRECTORIES (where the +i on the
dir blocks delete_dir_contents from recursing).  Regular files,
symlinks, etc. are left alone -- the underlying do_unlink_at /
do_rmdir_at recovery uses fd-based fchflags, and that fd survives a
rename or hardlink performed by make_backup, so the recovery
correctly preserves the immutable flag on the inode after it lands at
its backup location.

Verified on Linux/ext4: full suite still 59 passed / 2 skipped /
0 failed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three independent CI failures on PR RsyncProject#894 commit ac1d6a9:

1. Ubuntu / Ubuntu 22.04 (5 test failures each in make check30):
   testsuite/rsync.fns quoted "$RSYNC" when probing -VV to detect
   whether the build supports fileflags.  $RSYNC is a command STRING
   (e.g. "/path/to/rsync --protocol=30"), not a single binary path --
   so the quoted form tries to exec a file whose name contains a
   space, fails silently, and the all_plus/dots widths fall through
   to the 9-char non-fileflags defaults.  On Linux+chattr the rsync
   build emits 10-char (12-column %i) output, so devices, exclude,
   itemize, etc. mismatched their expected output.  Fix: drop the
   quotes so shell word-splitting separates the binary from --protocol.
   Local check30 now clean.

2. NetBSD / OpenBSD (fileflags FAIL):
   set_fileflags() opened with O_RDONLY|O_NOFOLLOW|O_NONBLOCK and
   then fchflags'd.  Both NetBSD and OpenBSD reject open() with
   O_NONBLOCK on a directory, returning EISDIR even though POSIX
   allows opening directories O_RDONLY.  Drop O_NONBLOCK: it isn't
   needed -- rsync_fchflags() rejects everything except S_IFREG and
   S_IFDIR, and callers already short-circuit on S_ISLNK, so the
   open target is always a plain file or directory.  Neither blocks
   on plain O_RDONLY.

3. AlmaLinux 8 (RSYNC_EXPECT_SKIPPED mismatch):
   The base image doesn't have chattr(1), so the fileflags test
   self-skips with "No chflags(1) or chattr(1) command on this host"
   even though the rsync build has FS_IOC_GETFLAGS support.  Add
   fileflags back to the AlmaLinux skip-allowlist; comment notes
   the reason.  (Could alternatively dnf install e2fsprogs in the
   workflow, but the skip is also fine -- Ubuntu / FreeBSD / macOS
   cover the test on every other CI job.)

The remaining 4 CI jobs (Cygwin, FreeBSD, Solaris, macOS) were green
on the round-2 push and unaffected by any of these fixes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three more issues caught by codex post-round-2.

RsyncProject#1 (high, security): the force_change EPERM-recovery in
do_unlink / do_unlink_at / do_rmdir / do_rmdir_at / do_rename_at
cleared the target's immutable flags, performed the unlink/rename,
then closed the fd without restoring flags.  For an inode with
st_nlink > 1, the unlink/rename only removed one of several
references -- the inode itself survives via its other hardlinks,
and those hardlinks were left with the cleared flags.

Same shape for rename's replaced-destination inode: renameat()
unlinks the old new-side inode; if it had st_nlink > 1, the
remaining links to it carry forward whatever flag state we put on
it during the make_mutable_fd().

Fix: after a successful unlink/rename, check st_nlink on the
pre-operation stat.  If > 1, the fd still refers to a surviving
inode; restore the original flags via the fd before closing.
For the AT_REMOVEDIR (rmdir) case the check is skipped -- a dir's
st_nlink counts "." and child-".." references, never additional
hardlinks, and after rmdir the inode is gone for good.

Applied to:
  - force_change_retry_unlinkat (do_unlink_at / do_rmdir_at)
  - do_unlink dirfd-anchored recovery
  - do_rename_at secure-path recovery (the new_st.st_nlink > 1 case)

RsyncProject#2 (high, correctness): the --fileflags / --crtimes protocol
gate at compat.c:759 only ran inside the `else if
(protocol_version >= 30)` branch.  For protocol < 30
(e.g. --protocol=29) xfer_flags_as_varint stays 0 and the check
never runs, so --fileflags --protocol=29 would silently let
flist.c write the extra fileflags word (and use the
XMIT_SAME_FLAGS bit in the 1<<16 position that requires varint
flag encoding), desyncing the file-list stream.

Move the --fileflags rejection out of the protocol-30 block to
right after the if/else if cascade, so it fires for every
negotiated protocol where xfer_flags_as_varint is still 0.
(The --crtimes gate stays in the protocol-30 block: crtimes
support also requires HAVE_GETATTRLIST or __CYGWIN__ and the
v-flag dance, which only happens in the >= 30 path.)

RsyncProject#3 (medium, correctness): the same uninitialized-stat_x.st bug
I fixed for the directory path in generator.c:1481 still
existed for the non-directory path at generator.c:1584.
stat_x_get_fileflags(&sx, fname) was being called without a
statret == 0 guard; for missing destinations sx.st has not been
populated, and on BSD rsync_lgetflags() returns hint->st_flags
which is uninitialized stack memory.  Add the same
`statret == 0 ? flags : 0` guard at the non-dir site.

Verified on Linux/ext4: 59 passed / 2 skipped / 0 failed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex round 4 RsyncProject#1 highlighted a contradiction: my man page promised
that --fileflags would not make receiver-immutable files alterable
without --force-change, but the code (preserved from the original
rsync-patches/fileflags.diff) makes destination flags match source
flags within SAFE_FILEFLAGS in both directions -- so a source without
uchg can clear uchg on a destination that has it.

Resolved by matching the manpage to the long-standing BSD code rather
than rewriting the receiver-side semantics:

  - --fileflags is "make dest match source," same as -p / -o / -g.
  - This is justified because the daemon-default refuse list rejects
    --fileflags out of the box; a daemon admin has to opt in
    per-module before clients can hand the daemon's filesystem
    sender-controlled st_flags.  Without that opt-in, the
    "receiver-immutable bits silently cleared" foot-gun isn't
    reachable from a hostile remote sender.
  - SAFE_FILEFLAGS still drops SF_* and UF_NOUNLINK; the
    --unsafe-fileflags opt-in widens to the full sender value (its
    own detailed entry now exists too).

New testsuite/daemon-refuse-fileflags.test enforces the daemon-default
refuse contract:
  - --fileflags / --unsafe-fileflags refused on a default-policy
    module (download) by name in the error.
  - --force-uchange / --force-schange refused on a default-policy
    module (upload, since the --force-* family only propagates over
    the wire from a sender via server_options).
  - --fileflags accepted on a module with "refuse options = !fileflags".

Verified Linux/ext4 full suite 60 passed / 2 skipped / 0 failed (was
59 / 2 / 0; +1 for the new test).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…(defer dir immutable, batch flag)

RsyncProject#5 (medium): batch.c didn't record preserve_fileflags, so
--write-batch --fileflags emits an extra word per file in the
file-list stream (XMIT_SAME_FLAGS bit + fileflags word) that
--read-batch wouldn't expect without the matching option.  Added
preserve_fileflags at slot 15 of flag_ptr/flag_name, marked
"protocol 30+, requires varint flags" so the bit-position remains
stable.  Older rsync readers ignore unknown high bits, so this is
backwards-readable; newer readers of an old batch see bit 15
unset and correctly leave preserve_fileflags at 0.

RsyncProject#4 (medium): if a source directory has immutable / append bits in
SAFE_FILEFLAGS (e.g. chflags uchg dir/), set_file_attrs in
recv_generator applied them to the dest dir BEFORE the children
inside the dir were populated -- and the +i then blocked rsync
from creating those children at all.  Pass ATTRS_DELAY_IMMUTABLE
in the recv_generator set_file_attrs call for dirs, mirroring
how finish_transfer already defers immutable bits for temp-then-
rename files.  touch_up_dirs now re-applies the deferred bits
after all children have been processed.

RsyncProject#2 (high): the early `continue` in touch_up_dirs at "no times to
retouch and no perms to fix" skipped the per-directory force_change
restore (undo_make_mutable) too -- so --force-change --omit-dir-times
on a +i source dir was leaving the dest dir permanently mutable.
Same issue would now affect the new --fileflags deferred-apply path
without the fix.  Split the continue condition: never skip if there's
a force_change restore pending OR a deferred --fileflags apply
pending OR the existing times/perms work.

Side effect of the RsyncProject#4 fix: set_fileflags() is no longer static
because the new touch_up_dirs path calls it directly (cleaner
than overloading undo_make_mutable's name).

Testsuite addition: fileflags.test now exercises the deferred-
immutable-on-dir scenario when the test user can set uchg (only on
BSD/macOS or as root on Linux).  Creates a uchg source dir with
children, rsyncs, asserts the children are present AND the dest dir
ends up with uchg set -- which means rsync deferred the bit until
after the children populated.

Verified Linux/ext4 full suite 60 passed / 2 skipped / 0 failed
(the new test block self-skips on non-root Linux since chattr +i
needs CAP_LINUX_IMMUTABLE).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ncProject#6 (chmod mode arg)

These two small syscall.c fixes were made at the start of the round-4
work but got dropped on the floor when I split the commit -- only the
docs (RsyncProject#1) and the bigger RsyncProject#2/RsyncProject#4/RsyncProject#5 deferred-immutable-dir series ended
up landed.  The tree was left dirty.

RsyncProject#3: do_rename (the non-_at variant) was missing the hardlink-aware
restore I added to do_rename_at last round.  Same shape -- when
renameat replaces a destination inode that had st_nlink > 1, the
remaining hardlinks survive carrying the cleared flags.  Restore via
new_fd before close (the fd still refers to the surviving inode).

RsyncProject#6: do_chmod and do_chmod_at force_change recovery were calling
make_mutable_fd(fd, mode, ...) where mode was the caller-supplied
chmod-target mode -- some callers (notably xattrs.c's set_xattr
recovery path) pass perm bits only, no S_IFREG / S_IFDIR, so on Linux
rsync_fchflags rejects the call as neither regular file nor directory
and recovery silently fails.  Use st.st_mode from the freshly-fstatted
target instead, which always has the right S_IFx bits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five findings from another codex pass:

  1. set_fileflags() and force_change_open_parent() were anchoring
     relative paths at curr_dir-as-a-path-string via
     secure_relative_open(curr_dir, ...).  A path-string basedir is
     re-traversed component-by-component on every call -- which
     reopens the same symlink-race attack window the phase-3 work
     was supposed to close, this time for the daemon
     "use chroot = no" case where module-root parents are reachable
     by other users.  Switch both call sites to
     secure_relative_open(NULL, ...), which anchors at AT_FDCWD: a
     stable kernel-held reference the receiver set up via
     change_dir() at startup, not a path that gets re-walked.

  2. --force-change without --fileflags promises in the manpage
     that the dest's original flags are restored after the
     transfer, but finish_transfer() only re-applied flags when
     preserve_fileflags was set.  For the common single-link case
     this silently dropped immutable bits after every update.
     The F_FFLAGS() stash the generator does in recv_generator()
     isn't visible to the receiver process, so capture the dest's
     flags ourselves via do_lstat() at finish_transfer entry
     (before any rename) and re-apply the force_change-affected
     subset after the rename succeeds.  Add testsuite coverage.

  3. set_file_attrs() chmods first and then calls set_fileflags(),
     which open(O_RDONLY)s the file -- a non-root receiver
     transferring a mode-000 source with chflags+immutable would
     fail at the open with EACCES.  Add an EACCES retry that
     do_lstat()s the path, temporarily widens the mode to add
     S_IRUSR via path-chmod, re-opens, then restores via fchmod
     through the fd we just acquired (before the fchflags so the
     restore isn't blocked by a freshly-set immutable bit).

  4. force_change_open_target() still set O_NONBLOCK even though
     set_fileflags() had already dropped it after the NetBSD/OpenBSD
     EISDIR trap was discovered in round 2.  The same trap fires
     on the recovery path for directories (rmdir/chmod force_change).
     Drop O_NONBLOCK and document why neither site needs it.

  5. The %i itemize manpage prose still said "11 letters long" and
     never documented the new 'f' (fileflags) attribute despite
     the example already being updated to `YXcstpoguaxf`.  Update
     the prose to 12 letters and add an 'f' bullet next to 'a'/'x'.

Also fix a misleading-indentation lint that snuck in with the
do_rename_at hardlink-restore commit: the rename_fail: label was
indented two spaces instead of being at column 0.

Linux: 60 passed / 2 skipped / 0 failed.
FreeBSD: 51 passed / 8 skipped / 0 failed (uchg block self-skips on
ZFS as before).
macOS: fileflags test passes including the new --force-change restore
subtest ("ok: --force-change restored flags=2 after rename").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four classes of failure surfaced after the round-5 push:

NetBSD/OpenBSD ("Is a directory" on set_fileflags for dirs):
secure_relative_open()'s per-component-walk fallback (used on hosts
without RESOLVE_BENEATH-equivalent kernel support) returns EISDIR
when the resolved path is a directory and the caller didn't pass
O_DIRECTORY -- it doesn't second-guess the caller's intent.
set_fileflags() is happy with either a regular file or a directory
(rsync_fchflags handles both), so introduce an open_for_fileflags()
helper that retries with O_DIRECTORY when the first open returns
EISDIR.  Linux/FreeBSD/macOS take the kernel RESOLVE_BENEATH path
which allows the first call through, so the retry is dead on those
hosts and only fires where the fallback is in use.  The same helper
is reused on the EACCES retry path so the round-5 mode-000 fix
covers both regular files and directories correctly.

Ubuntu/Ubuntu 22.04 (protocol incompatibility under check29):
--fileflags requires the varint flag encoding which is only
negotiated at protocol >= 30 (CF_VARINT_FLIST_FLAGS); under
make check29 (--protocol=29) the compat.c handshake correctly
aborts.  The test scripts already had the right behaviour at
runtime -- they just weren't recognising that the abort under
proto 29 was expected.  Have fileflags.test and
daemon-refuse-fileflags.test inspect $RSYNC for "--protocol=2N"
where N <= 9 and self-skip cleanly.  Update the Ubuntu CI
workflows so the check29 step expects these two tests to skip
(check and check30 are unchanged).

Cygwin (daemon-refuse-fileflags newly-skipped, not in expected
list): Cygwin builds without fileflags support, so the new
daemon-refuse-fileflags test now self-skips with the
file_flags=true probe just like fileflags.test does.  Add it to
the Cygwin RSYNC_EXPECT_SKIPPED list.

Linux check / check29 / check30 all clean locally.
FreeBSD check29 clean: SKIPs both fileflags tests as expected.
macOS fileflags test still PASS, including the new --force-change
restore subtest from round 5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant