From 31e1338bd494bac876e21ab0bc3b0d4277e61d28 Mon Sep 17 00:00:00 2001 From: Daniel Tang Date: Fri, 29 Aug 2025 21:06:23 -0400 Subject: [PATCH] Add lsattr/chattr support to --xattrs This is like `bsdtar --fflags`. Repurposing `sys_llistxattr` allows cross-platform interoperability, such as for a transfer from Linux to Mac then from Mac to Linux. It's also simpler by not affecting the rest of the codebase. There is no new cmdline option because #165 wants this to work with just `-avX`. Closes: #165 Closes: #508 --- lib/sysxattrs.c | 163 +++++++++++++++++++++++++++++++++++++++++++++++- rsync.1.md | 2 +- 2 files changed, 161 insertions(+), 4 deletions(-) diff --git a/lib/sysxattrs.c b/lib/sysxattrs.c index 5a6aeaad9..6ba6a4ca3 100644 --- a/lib/sysxattrs.c +++ b/lib/sysxattrs.c @@ -30,29 +30,186 @@ #if defined HAVE_LINUX_XATTRS +#include +#define FS_FL_ATTR "user.rsync.lsattr" +#define FS_FL_ATTR_BUF_SIZE sizeof("-2147483648") + +#ifdef FS_IOC_GETFLAGS + +#define FS_FL_SETTABLE (FS_APPEND_FL|FS_COMPR_FL|FS_DIRSYNC_FL|FS_IMMUTABLE_FL|FS_JOURNAL_DATA_FL|FS_NOATIME_FL|\ + FS_NOCOW_FL|FS_NODUMP_FL|FS_NOTAIL_FL|FS_PROJINHERIT_FL|FS_SECRM_FL|FS_SYNC_FL|FS_TOPDIR_FL|FS_UNRM_FL|\ + FS_CASEFOLD_FL|FS_NOCOMP_FL|FS_PROJINHERIT_FL|FS_DAX_FL) + +static int handle_fs_fl_impl(int lsattr_fd, const char *source, const char *dest, int open_flags, int *lsattr_flags) { + int ret; + struct stat st; + if (source) { + int lsattr_fd_owned = 0; + if (lsattr_fd == -1) { + if ((open_flags & O_NOFOLLOW ? lstat : stat)(source, &st)) { + rsyserr(FERROR_XFER, errno, + "handle_fs_fl_impl: stat(%s) for source failed", + full_fname(source)); + return -1; + } + if (!S_ISREG(st.st_mode) && !S_ISDIR(st.st_mode)) { + return 0; + } + lsattr_fd_owned = 1; + lsattr_fd = open(source, open_flags); + if (lsattr_fd == -1) { + rsyserr(FERROR_XFER, errno, + "handle_fs_fl_impl: open(%s) for source failed", + full_fname(source)); + return -1; + } + } + ret = ioctl(lsattr_fd, FS_IOC_GETFLAGS, lsattr_flags); + if (ret) { + rsyserr(FERROR_XFER, ret, + "handle_fs_fl_impl: FS_IOC_GETFLAGS(%s) for source failed", + full_fname(source)); + if (lsattr_fd_owned) + close(lsattr_fd); + assert(ret < 0); + return ret; + } + *lsattr_flags &= FS_FL_SETTABLE; + if (lsattr_fd_owned) + close(lsattr_fd); + } + if (dest) { + if ((open_flags & O_NOFOLLOW ? lstat : stat)(dest, &st)) { + rsyserr(FERROR_XFER, errno, + "handle_fs_fl_impl: stat(%s) for dest failed", + full_fname(dest)); + return -1; + } + if (!S_ISREG(st.st_mode) && !S_ISDIR(st.st_mode)) { + return 0; + } + const int chattr_fd = open(dest, open_flags); + if (chattr_fd == -1) { + rsyserr(FERROR_XFER, errno, + "handle_fs_fl_impl: open(%s) for dest failed", + full_fname(dest)); + return -1; + } + int chattr_flags = 0; + ret = ioctl(chattr_fd, FS_IOC_GETFLAGS, &chattr_flags); + if (ret) { + rsyserr(FERROR_XFER, ret, + "handle_fs_fl_impl: FS_IOC_GETFLAGS(%s) for dest failed", + full_fname(dest)); + close(chattr_fd); + assert(ret < 0); + return ret; + } + chattr_flags &= ~FS_FL_SETTABLE; + chattr_flags |= *lsattr_flags; + ret = ioctl(chattr_fd, FS_IOC_SETFLAGS, &chattr_flags); + if (ret) { + rsyserr(FERROR_XFER, ret, + "handle_fs_fl_impl: FS_IOC_SETFLAGS(%s) for dest failed", + full_fname(dest)); + close(chattr_fd); + assert(ret < 0); + return ret; + } + close(chattr_fd); + } + return 0; +} + +static int handle_fs_fl(int source_fd, const char *source, const char *dest, int nofollow, char *rsync_lsattr, size_t buf_size) { + if (dest && !rsync_lsattr) + return -1; + int lsattr_flags = 0; + if (dest) + lsattr_flags = atoi(rsync_lsattr); // NOLINT(*-err34-c) + int open_flags = O_RDONLY|O_CLOEXEC|O_NOCTTY|O_NONBLOCK; + if (nofollow) + open_flags |= O_NOFOLLOW; + int ret = handle_fs_fl_impl(source_fd, source, dest, open_flags, &lsattr_flags); + if (ret) + return ret; + if (!source) + return 0; + char rsync_lsattr_tmp[FS_FL_ATTR_BUF_SIZE]; + ret = sprintf(rsync_lsattr_tmp, "%d", lsattr_flags) + 1; + if (ret < 0) + return ret; + if (rsync_lsattr) { + if (buf_size < (size_t) ret) + return -ERANGE; + memcpy(rsync_lsattr, rsync_lsattr_tmp, ret); + } + return ret; +} +#endif + ssize_t sys_lgetxattr(const char *path, const char *name, void *value, size_t size) { +#ifdef FS_IOC_GETFLAGS + if (!strcmp(name, FS_FL_ATTR)) + return handle_fs_fl(-1, path, NULL, 1, value, size); +#endif return lgetxattr(path, name, value, size); } ssize_t sys_fgetxattr(int filedes, const char *name, void *value, size_t size) { +#ifdef FS_IOC_GETFLAGS + if (!strcmp(name, FS_FL_ATTR)) + return handle_fs_fl(filedes, "(fd)", NULL, 0, value, size); +#endif return fgetxattr(filedes, name, value, size); } int sys_lsetxattr(const char *path, const char *name, const void *value, size_t size) { +#ifdef FS_IOC_GETFLAGS + if (!strcmp(name, FS_FL_ATTR)) + return handle_fs_fl(-1, NULL, path, 1, (void *) value, size); +#endif return lsetxattr(path, name, value, size, 0); } +#ifdef FS_IOC_GETFLAGS +static const char FS_FL_ZERO[FS_FL_ATTR_BUF_SIZE] = "0"; +#endif + int sys_lremovexattr(const char *path, const char *name) { +#ifdef FS_IOC_GETFLAGS + if (!strcmp(name, FS_FL_ATTR)) + return handle_fs_fl(-1, NULL, path, 1, (char *) FS_FL_ZERO, strlen(FS_FL_ZERO)); +#endif return lremovexattr(path, name); } -ssize_t sys_llistxattr(const char *path, char *list, size_t size) -{ - return llistxattr(path, list, size); +ssize_t sys_llistxattr(const char *path, char *list, size_t size) { + ssize_t ret; +#ifdef FS_IOC_GETFLAGS + char fs_fl_attr_buf[FS_FL_ATTR_BUF_SIZE]; + ret = handle_fs_fl(-1, path, NULL, 1, fs_fl_attr_buf, FS_FL_ATTR_BUF_SIZE); + if (ret < 0) + return ret; +#endif + ret = llistxattr(path, list, size); +#ifdef FS_IOC_GETFLAGS + if (ret < 0) + return ret; + if (strcmp(fs_fl_attr_buf, FS_FL_ZERO) != 0) { + if (list) { + if (ret + sizeof(FS_FL_ATTR) > size) + return -ERANGE; + memcpy(&list[ret], FS_FL_ATTR, sizeof(FS_FL_ATTR)); + } + ret += sizeof(FS_FL_ATTR); + } +#endif + return ret; } #elif HAVE_OSX_XATTRS diff --git a/rsync.1.md b/rsync.1.md index 2b4b75087..a2ba3bbaf 100644 --- a/rsync.1.md +++ b/rsync.1.md @@ -449,7 +449,7 @@ has its own detailed description later in this manpage. --executability, -E preserve executability --chmod=CHMOD affect file and/or directory permissions --acls, -A preserve ACLs (implies --perms) ---xattrs, -X preserve extended attributes +--xattrs, -X preserve file attributes (including extended attributes) --owner, -o preserve owner (super-user only) --group, -g preserve group --devices preserve device files (super-user only)