You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fileflags: portable BSD chflags + Linux chattr implementation
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>
0 commit comments