Depends-On-Patch: acls.diff Depends-On-Patch: xattrs.diff This patch adds a new option: --fake-super, which tells rsync to copy in a fake super-user mode that stores various file attributes in an extended- attribute value instead of as real file-system attributes. The items affected are: mode the real mode of the file always has the special-permission bits cleared (u-s,g-s,o-t) and full owner access is always enabled (u+rw for files and u+rwx for directories). The former makes the files safe if the user and/or group info was not really preserved, and the latter ensures that our fake-super process can always read & write & scan the files and directories. rdev devices and special files are created as zero-length normal files (with all the attributes preserved in the xattr-stat). uid the real owner will be the executor of the receiving rsync. gid the real group will be the default group of the executor. The --fake-super option only affects the side where the option is used. To affect the remote side of a remote-shell connection, specify an rsync path: rsync -av --rsync-path='rsync --fake-super' /src/ host:/dest/ The --fake-super option affects both sides of a local copy, so if you want to affect only one side or the other, you'll need to turn the copy into a remote copy to/from localhost. However, it's always safe to copy from some non-fake-super files into some fake-super files using a normal local copy since the non-fake source files will just have their normal attributes. A daemon can set "fake super = yes" in the rsync.conf file for any module that you'd like to be able to preserve all attributes without having it run as root (the client cannot affect this setting on the daemon). After applying this patch, run these commands for a successful build: ./prepare-source ./configure --enable-xattr-support make or, if you want ACL support too: ./prepare-source ./configure --enable-acl-support --enable-xattr-support make --- old/backup.c +++ new/backup.c @@ -129,7 +129,7 @@ static int make_bak_dir(char *fullpath) if (p >= rel) { /* Try to transfer the directory settings of the * actual dir that the files are coming from. */ - if (do_stat(rel, &sx.st) < 0) { + if (x_stat(rel, &sx.st, NULL) < 0) { rsyserr(FERROR, errno, "make_bak_dir stat %s failed", full_fname(rel)); @@ -200,7 +200,7 @@ static int keep_backup(char *fname) int ret_code; /* return if no file to keep */ - if (do_lstat(fname, &sx.st) < 0) + if (x_lstat(fname, &sx.st, NULL) < 0) return 1; #ifdef SUPPORT_ACLS sx.acc_acl = sx.def_acl = NULL; --- old/clientserver.c +++ new/clientserver.c @@ -625,6 +625,11 @@ static int rsync_module(int f_in, int f_ ret = parse_arguments(&argc, (const char ***) &argv, 0); quiet = 0; /* Don't let someone try to be tricky. */ + if (lp_fake_super(i)) + am_root = -1; + else if (am_root < 0) /* Treat --fake-super from client as --super. */ + am_root = 2; + if (filesfrom_fd == 0) filesfrom_fd = f_in; --- old/flist.c +++ new/flist.c @@ -181,7 +181,7 @@ static int readlink_stat(const char *pat } return 0; #else - return do_stat(path, stp); + return x_stat(path, stp, NULL); #endif } @@ -189,17 +189,17 @@ int link_stat(const char *path, STRUCT_S { #ifdef SUPPORT_LINKS if (copy_links) - return do_stat(path, stp); - if (do_lstat(path, stp) < 0) + return x_stat(path, stp, NULL); + if (x_lstat(path, stp, NULL) < 0) return -1; if (follow_dirlinks && S_ISLNK(stp->st_mode)) { STRUCT_STAT st; - if (do_stat(path, &st) == 0 && S_ISDIR(st.st_mode)) + if (x_stat(path, &st, NULL) == 0 && S_ISDIR(st.st_mode)) *stp = st; } return 0; #else - return do_stat(path, stp); + return x_stat(path, stp, NULL); #endif } @@ -793,7 +793,7 @@ struct file_struct *make_file(char *fnam if (save_errno == ENOENT) { #ifdef SUPPORT_LINKS /* Avoid "vanished" error if symlink points nowhere. */ - if (copy_links && do_lstat(thisname, &st) == 0 + if (copy_links && x_lstat(thisname, &st, NULL) == 0 && S_ISLNK(st.st_mode)) { io_error |= IOERR_GENERAL; rprintf(FERROR, "symlink has no referent: %s\n", @@ -963,7 +963,7 @@ struct file_struct *make_file(char *fnam int save_mode = file->mode; file->mode = S_IFDIR; /* Find a directory with our name. */ if (flist_find(the_file_list, file) >= 0 - && do_stat(thisname, &st2) == 0 && S_ISDIR(st2.st_mode)) { + && x_stat(thisname, &st2, NULL) == 0 && S_ISDIR(st2.st_mode)) { file->modtime = st2.st_mtime; file->length = st2.st_size; file->mode = st2.st_mode; --- old/generator.c +++ new/generator.c @@ -1510,13 +1510,14 @@ void generate_files(int f_out, struct fi recv_generator(fbuf, file, i, itemizing, maybe_ATTRS_REPORT, code, f_out); - /* We need to ensure that any dirs we create have writeable + /* We need to ensure that any dirs we create have rwx * permissions during the time we are putting files within * them. This is then fixed after the transfer is done. */ #ifdef HAVE_CHMOD - if (!am_root && S_ISDIR(file->mode) && !(file->mode & S_IWUSR) + if (am_root <= 0 && S_ISDIR(file->mode) + && (file->mode & S_IRWXU) != S_IRWXU && dir_tweaking) { - mode_t mode = file->mode | S_IWUSR; /* user write */ + mode_t mode = file->mode | S_IRWXU; /* user rwx */ char *fname = local_name ? local_name : fbuf; if (do_chmod(fname, mode) < 0) { rsyserr(FERROR, errno, --- old/loadparm.c +++ new/loadparm.c @@ -150,6 +150,7 @@ typedef struct int syslog_facility; int timeout; + BOOL fake_super; BOOL ignore_errors; BOOL ignore_nonreadable; BOOL list; @@ -197,6 +198,7 @@ static service sDefault = /* syslog_facility; */ LOG_DAEMON, /* timeout; */ 0, + /* fake_super; */ False, /* ignore_errors; */ False, /* ignore_nonreadable; */ False, /* list; */ True, @@ -298,6 +300,7 @@ static struct parm_struct parm_table[] = {"dont compress", P_STRING, P_LOCAL, &sDefault.dont_compress, NULL,0}, {"exclude from", P_STRING, P_LOCAL, &sDefault.exclude_from, NULL,0}, {"exclude", P_STRING, P_LOCAL, &sDefault.exclude, NULL,0}, + {"fake super", P_BOOL, P_LOCAL, &sDefault.fake_super, NULL,0}, {"filter", P_STRING, P_LOCAL, &sDefault.filter, NULL,0}, {"gid", P_STRING, P_LOCAL, &sDefault.gid, NULL,0}, {"hosts allow", P_STRING, P_LOCAL, &sDefault.hosts_allow, NULL,0}, @@ -412,6 +415,7 @@ FN_LOCAL_INTEGER(lp_max_connections, max FN_LOCAL_INTEGER(lp_max_verbosity, max_verbosity) FN_LOCAL_INTEGER(lp_timeout, timeout) +FN_LOCAL_BOOL(lp_fake_super, fake_super) FN_LOCAL_BOOL(lp_ignore_errors, ignore_errors) FN_LOCAL_BOOL(lp_ignore_nonreadable, ignore_nonreadable) FN_LOCAL_BOOL(lp_list, list) @@ -816,7 +820,7 @@ BOOL lp_load(char *pszFname, int globals if (pszFname) pstrcpy(n2,pszFname); - else if (am_server && !am_root) + else if (am_server && am_root <= 0) pstrcpy(n2,RSYNCD_USERCONF); else pstrcpy(n2,RSYNCD_SYSCONF); --- old/options.c +++ new/options.c @@ -73,7 +73,7 @@ int protocol_version = PROTOCOL_VERSION; int sparse_files = 0; int do_compression = 0; int def_compress_level = Z_DEFAULT_COMPRESSION; -int am_root = 0; +int am_root = 0; /* 0 = normal, 1 = super, 2 = --super, -1 = --fake-super */ int am_server = 0; int am_sender = 0; int am_generator = 0; @@ -330,6 +330,7 @@ void usage(enum logcode F) rprintf(F," -t, --times preserve times\n"); rprintf(F," -O, --omit-dir-times omit directories when preserving times\n"); rprintf(F," --super receiver attempts super-user activities\n"); + rprintf(F," --fake-super store/recover privileged attrs using xattrs\n"); rprintf(F," -S, --sparse handle sparse files efficiently\n"); rprintf(F," -n, --dry-run show what would have been transferred\n"); rprintf(F," -W, --whole-file copy files whole (without rsync algorithm)\n"); @@ -454,6 +455,7 @@ static struct poptOption long_options[] {"modify-window", 0, POPT_ARG_INT, &modify_window, OPT_MODIFY_WINDOW, 0, 0 }, {"super", 0, POPT_ARG_VAL, &am_root, 2, 0, 0 }, {"no-super", 0, POPT_ARG_VAL, &am_root, 0, 0, 0 }, + {"fake-super", 0, POPT_ARG_VAL, &am_root, -1, 0, 0 }, {"owner", 'o', POPT_ARG_VAL, &preserve_uid, 1, 0, 0 }, {"no-owner", 0, POPT_ARG_VAL, &preserve_uid, 0, 0, 0 }, {"no-o", 0, POPT_ARG_VAL, &preserve_uid, 0, 0, 0 }, --- old/receiver.c +++ new/receiver.c @@ -528,7 +528,7 @@ int recv_files(int f_in, struct file_lis if (fd1 == -1) { st.st_mode = 0; st.st_size = 0; - } else if (do_fstat(fd1,&st) != 0) { + } else if (x_fstat(fd1, &st, NULL) != 0) { rsyserr(FERROR, errno, "fstat %s failed", full_fname(fnamecmp)); discard_receive_data(f_in, file->length); --- old/rsync.c +++ new/rsync.c @@ -49,7 +49,6 @@ extern int preserve_gid; extern int inplace; extern int keep_dirlinks; extern int make_backups; -extern mode_t orig_umask; extern struct stats stats; extern struct chmod_mode_struct *daemon_chmod_modes; @@ -197,7 +196,9 @@ int set_file_attrs(char *fname, struct f (long)sxp->st.st_gid, (long)file->gid); } } - if (do_lchown(fname, + if (am_root < 0) + ; + else if (do_lchown(fname, change_uid ? file->uid : sxp->st.st_uid, change_gid ? file->gid : sxp->st.st_gid) != 0) { /* shouldn't have attempted to change uid or gid @@ -206,7 +207,7 @@ int set_file_attrs(char *fname, struct f change_uid ? "chown" : "chgrp", full_fname(fname)); goto cleanup; - } + } else /* a lchown had been done - we have to re-stat if the * destination had the setuid or setgid bits set due * to the side effect of the chown call */ @@ -224,6 +225,24 @@ int set_file_attrs(char *fname, struct f if (preserve_xattrs && set_xattr(fname, file, sxp) == 0) updated = 1; #endif + + if (am_root < 0 && !S_ISLNK(file->mode)) { + switch (set_stat_xattr(fname, file)) { + case 0: + break; + case -1: + rsyserr(FERROR, errno, + "write of stat xattr failed for %s", + full_fname(fname)); + break; + case -2: + rsyserr(FERROR, errno, + "delete of stat xattr failed for %s", + full_fname(fname)); + break; + } + } + #ifdef SUPPORT_ACLS /* It's OK to call set_acl() now, even for a dir, as the generator * will enable owner-writability using chmod, if necessary. @@ -237,7 +256,7 @@ int set_file_attrs(char *fname, struct f #ifdef HAVE_CHMOD if ((sxp->st.st_mode & CHMOD_BITS) != (new_mode & CHMOD_BITS)) { - int ret = do_chmod(fname, new_mode); + int ret = am_root < 0 ? 0 : do_chmod(fname, new_mode); if (ret < 0) { rsyserr(FERROR, errno, "failed to set permissions on %s", --- old/rsync.yo +++ new/rsync.yo @@ -333,6 +333,7 @@ to the detailed description below for a -t, --times preserve times -O, --omit-dir-times omit directories when preserving times --super receiver attempts super-user activities + --fake-super store/recover privileged attrs using xattrs -S, --sparse handle sparse files efficiently -n, --dry-run show what would have been transferred -W, --whole-file copy files whole (without rsync algorithm) @@ -899,6 +900,31 @@ also for ensuring that you will get erro being running as the super-user. To turn off super-user activities, the super-user can use bf(--no-super). +dit(bf(--fake-super)) When this option is enabled, privileged attributes +are stored and recovered via a special extended attribute that is attached +to each file (as needed). This includes the file's owner and group (if it +is not the default), the file's device info (device & special files are +created as empty text files), and any permission bits that we won't allow +to be set on the real file (e.g. the real file gets u-s,g-s,o-t for safety) +or that would limit the owner's access (since the real super user can +always access a file or directory, the files we create can always be +accessed by the creating user too). + +The bf(--fake-super) option only affects the side where the option is used. +To affect the remote side of a remote-shell connection, specify an rsync +path: + +quote(tt( rsync -av --rsync-path="rsync --fake-super" /src/ host:/dest/)) + +The bf(--fake-super) option affects both sides of a em(local) copy, so if +you want to affect only one side or the other, you'll need to turn the copy +into a remote copy to/from localhost. However, it's always safe to copy +from some non-fake-super files into some fake-super files using a normal +local copy since the non-fake source files will just have their normal +attributes. + +See also the "fake super" setting in the daemon's rsyncd.conf file. + dit(bf(-S, --sparse)) Try to handle sparse files efficiently so they take up less space on the destination. Conflicts with bf(--inplace) because it's not possible to overwrite data in a sparse fashion. --- old/rsyncd.conf.yo +++ new/rsyncd.conf.yo @@ -226,6 +226,11 @@ file transfers to and from that module s was run as root. This complements the "uid" option. The default is gid -2, which is normally the group "nobody". +dit(bf(fake super)) Setting "fake super = yes" for a module causes the +daemon side to behave as if the bf(--fake-user) command-line option had +been specified. This allows the full attributes of a file to be stored +without having to have the daemon actually running as root. + dit(bf(filter)) The "filter" option allows you to specify a space-separated list of filter rules that the daemon will not allow to be read or written. This is only superficially equivalent to the client specifying these --- old/syscall.c +++ new/syscall.c @@ -28,6 +28,7 @@ #endif extern int dry_run; +extern int am_root; extern int read_only; extern int list_only; extern int preserve_perms; @@ -79,6 +80,15 @@ int do_mknod(char *pathname, mode_t mode { if (dry_run) return 0; RETURN_ERROR_IF_RO_OR_LO; + + /* For --fake-super, we create a normal file with mode 0600. */ + if (am_root < 0) { + int fd = open(pathname, O_WRONLY|O_CREAT|O_TRUNC, S_IWUSR|S_IRUSR); + if (fd < 0 || close(fd) < 0) + return -1; + return 0; + } + #if !defined MKNOD_CREATES_FIFOS && defined HAVE_MKFIFO if (S_ISFIFO(mode)) return mkfifo(pathname, mode); --- old/t_unsafe.c +++ new/t_unsafe.c @@ -24,7 +24,11 @@ #include "rsync.h" -int dry_run, read_only, list_only, verbose; +int dry_run = 0; +int am_root = 0; +int read_only = 0; +int list_only = 0; +int verbose = 0; int preserve_perms = 0; int --- old/tls.c +++ new/tls.c @@ -39,6 +39,7 @@ /* These are to make syscall.o shut up. */ int dry_run = 0; +int am_root = 0; /* TODO: add option to set this to -1. */ int read_only = 1; int list_only = 0; int preserve_perms = 0; --- old/trimslash.c +++ new/trimslash.c @@ -23,6 +23,7 @@ /* These are to make syscall.o shut up. */ int dry_run = 0; +int am_root = 0; int read_only = 1; int list_only = 0; int preserve_perms = 0; --- old/xattr.c +++ new/xattr.c @@ -26,11 +26,15 @@ #ifdef SUPPORT_XATTRS extern int dry_run; +extern int am_root; +extern mode_t orig_umask; extern unsigned int file_struct_len; #define RSYNC_XAL_INITIAL 5 #define RSYNC_XAL_LIST_INITIAL 100 +#define FAKE_XATTR "user.rsync%stat" + typedef struct { char *name; char *datum; @@ -130,9 +134,15 @@ static int rsync_xal_get(const char *fna if (name_size == 0) return 0; for (left = name_size, name = namebuf; left > 0 ; left -= len, name += len) { - rsync_xa *rxas = EXPAND_ITEM_LIST(xalp, rsync_xa, RSYNC_XAL_INITIAL); + rsync_xa *rxas; len = strlen(name) + 1; + if (am_root < 0 && len == sizeof FAKE_XATTR + && name[10] == '%' && strcmp(name, FAKE_XATTR) == 0) + continue; + + rxas = EXPAND_ITEM_LIST(xalp, rsync_xa, RSYNC_XAL_INITIAL); + datum_size = sys_lgetxattr(fname, name, NULL, 0); if (datum_size < 0) { if (errno == ENOTSUP) @@ -285,10 +295,19 @@ void receive_xattr(struct file_struct *f out_of_memory("receive_xattr"); read_buf(f, ptr, name_len); read_buf(f, ptr + name_len, datum_len); + + if (am_root < 0 && name_len == sizeof FAKE_XATTR + && ptr[10] == '%' && strcmp(ptr, FAKE_XATTR) == 0) { + free(ptr); + temp_xattr.count--; + continue; + } + rxa->name_len = name_len; rxa->datum_len = datum_len; rxa->name = ptr; rxa->datum = ptr + name_len; + #ifdef HAVE_OSX_XATTRS if (strncmp(rxa->name, UNIQUE_PREFIX, UPRE_LEN) == 0) { rxa->name_len -= UPRE_LEN; @@ -365,4 +384,103 @@ int set_xattr(const char *fname, const s return rsync_xal_set(fname, lst + ndx); /* TODO: This needs to return 1 if no xattrs changed! */ } +int get_stat_xattr(const char *fname, int fd, STRUCT_STAT *st) +{ + int mode, rdev_major, rdev_minor, uid, gid, len; + char buf[256]; + + if (fname) + len = sys_lgetxattr(fname, FAKE_XATTR, buf, sizeof buf - 1); + else + len = sys_fgetxattr(fd, FAKE_XATTR, buf, sizeof buf - 1); + if (len < 0 || len >= (int)sizeof buf) { + if (errno == ENOTSUP || errno == ENOATTR) + return -1; + return -1; + } + buf[len] = '\0'; + + if (sscanf(buf, "%o %d,%d %d:%d", + &mode, &rdev_major, &rdev_minor, &uid, &gid) != 5) { + errno = EINVAL; + return -1; + } + + st->st_mode = mode; + st->st_rdev = MAKEDEV(rdev_major, rdev_minor); + st->st_uid = uid; + st->st_gid = gid; + + return 0; +} + +int set_stat_xattr(const char *fname, struct file_struct *file) +{ + STRUCT_STAT fst, xst; + dev_t rdev; + mode_t mode; + + if (dry_run) + return 0; + + if (x_stat(fname, &fst, &xst) < 0) + return -1; + + if (IS_DEVICE(file->mode) || IS_SPECIAL(file->mode)) + rdev = file->u.rdev; + else + rdev = 0; + + /* Dump the special permissions and enable full owner access. */ + mode = (fst.st_mode & ~CHMOD_BITS) | (file->mode & ACCESSPERMS) + | (S_ISDIR(fst.st_mode) ? 0700 : 0600); + if (fst.st_mode != mode) + do_chmod(fname, mode); + if (!IS_DEVICE(fst.st_mode) && !IS_SPECIAL(fst.st_mode)) + fst.st_rdev = 0; /* just in case */ + + if (mode == file->mode && fst.st_rdev == rdev + && fst.st_uid == file->uid && fst.st_gid == file->gid) { + /* xst.st_mode will be 0 if there's no current stat xattr */ + if (xst.st_mode && sys_lremovexattr(fname, FAKE_XATTR) < 0) + return -2; + return 0; + } + + if (xst.st_mode != file->mode || xst.st_rdev != rdev + || xst.st_uid != file->uid || xst.st_gid != file->gid) { + char buf[256]; + int len = snprintf(buf, sizeof buf, "%o %u,%u %u:%u", + (int)file->mode, + (int)major(rdev), (int)minor(rdev), + (int)file->uid, (int)file->gid); + return sys_lsetxattr(fname, FAKE_XATTR, buf, len, 0); + } + return 0; +} + +int x_stat(const char *fname, STRUCT_STAT *fst, STRUCT_STAT *xst) +{ + int ret = do_stat(fname, fst); + if (get_stat_xattr(fname, -1, xst? xst : fst) != 0 && xst) + xst->st_mode = 0; + return ret; +} + +int x_lstat(const char *fname, STRUCT_STAT *fst, STRUCT_STAT *xst) +{ + int ret = do_lstat(fname, fst); + if (get_stat_xattr(fname, -1, xst? xst : fst) != 0 && xst) + xst->st_mode = 0; + return ret; +} + +int x_fstat(int fd, STRUCT_STAT *fst, STRUCT_STAT *xst) +{ + int ret = do_fstat(fd, fst); + if (get_stat_xattr(NULL, fd, xst? xst : fst) != 0 && xst) + xst->st_mode = 0; + return ret; +} + #endif /* SUPPORT_XATTRS */