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 a file is always (666 & umask) while
        the real mode of a directory is always (777 & umask).

  rdev  devices and special files are created as zero-length
        normal files.

  uid   the real owner is always left unchanged.

  gid   the real group is always left unchanged.

A daemon can set "fake super = yes" in the rsync.conf file for any module
that you'd like to run without root perms while pretending it has them (the
client cannot affect this).

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/

For a local copy where you want to affect only one side or the other,
you'll need to turn the copy into a remote copy to localhost.

After applying this patch, run these commands for a successful build:

    ./prepare-source
    ./configure
    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            fake root by storing/reading ownership/etc in EAs\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
@@ -197,7 +197,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 +208,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 +226,28 @@ 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)) {
+		mode_t mode = sxp->st.st_mode & ~CHMOD_BITS;
+		mode |= (S_ISDIR(mode) ? 0777 : 0666) & (~orig_umask | S_IRWXU);
+		if (sxp->st.st_mode != mode)
+			do_chmod(fname, 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 +261,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/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,100 @@ 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;
+
+	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;
+
+	/* This is the mode that the file will be. */
+	fst.st_mode &= ~CHMOD_BITS;
+	fst.st_mode |= (S_ISDIR(file->mode) ? 0777 : 0666) & ~orig_umask;
+	if (!IS_DEVICE(fst.st_mode) && !IS_SPECIAL(fst.st_mode))
+		fst.st_rdev = 0; /* just in case */
+
+	if (fst.st_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 */
