add fileflags option for BSD and others#894
Open
tridge wants to merge 11 commits into
Open
Conversation
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>
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is based on the fileflags.patch in the rsync-patches archive
needs a bit more work before merging
todo: