summaryrefslogtreecommitdiff
path: root/sdcard
diff options
context:
space:
mode:
authorJeff Sharkey <jsharkey@android.com>2013-07-03 17:08:29 -0700
committerJeff Sharkey <jsharkey@android.com>2013-08-08 17:26:41 -0700
commitdfe0cbab3f9039f34af1dc9e31faf8155737ec2d (patch)
tree5d76170d80704f3578f316401f4fbe6ea8dc0f1d /sdcard
parent9face5cad5b4ffb883b23e1c45aafac73c712fd6 (diff)
Richer SD card permissions through FUSE.
Changes the FUSE daemon to synthesize an Android-specific set of filesystem permissions, even when the underlying media storage is permissionless. This is designed to support several features: First, apps can access their own files in /Android/data/com.example/ without requiring any external storage permissions. This is enabled by allowing o+x on parent directories, and assigning the UID owner based on the directory name (package name). The mapping from package to appId is parsed from packages.list, which is updated when apps are added/removed. Changes are observed through inotify. It creates missing package name directories when requested and valid. Second, support for separate permissions for photos and audio/video content on the device through new GIDs which are assigned based on top-level directory names. Finally, support for multi-user separation on the same physical media through new /Android/user/ directory, which will be bind-mounted into place. It recursively applies the above rules to each secondary user. rwxrwx--x root:sdcard_rw / rwxrwx--- root:sdcard_pics /Pictures rwxrwx--- root:sdcard_av /Music rwxrwx--x root:sdcard_rw /Android rwxrwx--x root:sdcard_rw /Android/data rwxrwx--- u0_a12:sdcard_rw /Android/data/com.example rwxrwx--x root:sdcard_rw /Android/obb/ rwxrwx--- u0_a12:sdcard_rw /Android/obb/com.example rwxrwx--- root:sdcard_all /Android/user rwxrwx--x root:sdcard_rw /Android/user/10 rwxrwx--- u10_a12:sdcard_rw /Android/user/10/Android/data/com.example These derived permissions are disabled by default. Switched option parsing to getopt(). Change-Id: I21bf5d79d13f0f07a6a116122b16395f4f97505b
Diffstat (limited to 'sdcard')
-rw-r--r--sdcard/Android.mk2
-rw-r--r--sdcard/sdcard.c426
2 files changed, 371 insertions, 57 deletions
diff --git a/sdcard/Android.mk b/sdcard/Android.mk
index fb04d6d35..4630db97c 100644
--- a/sdcard/Android.mk
+++ b/sdcard/Android.mk
@@ -6,6 +6,6 @@ LOCAL_SRC_FILES:= sdcard.c
LOCAL_MODULE:= sdcard
LOCAL_CFLAGS := -Wall -Wno-unused-parameter
-LOCAL_SHARED_LIBRARIES := libc
+LOCAL_SHARED_LIBRARIES := libc libcutils
include $(BUILD_EXECUTABLE)
diff --git a/sdcard/sdcard.c b/sdcard/sdcard.c
index bff6e67d5..377c008df 100644
--- a/sdcard/sdcard.c
+++ b/sdcard/sdcard.c
@@ -30,6 +30,10 @@
#include <pthread.h>
#include <sys/time.h>
#include <sys/resource.h>
+#include <sys/inotify.h>
+
+#include <cutils/hashmap.h>
+#include <cutils/multiuser.h>
#include <private/android_filesystem_config.h>
@@ -57,6 +61,30 @@
* - if an op that returns a fuse_entry fails writing the reply to the
* kernel, you must rollback the refcount to reflect the reference the
* kernel did not actually acquire
+ *
+ * This daemon can also derive custom filesystem permissions based on directory
+ * structure when requested. These custom permissions support several features:
+ *
+ * - Apps can access their own files in /Android/data/com.example/ without
+ * requiring any additional GIDs.
+ * - Separate permissions for protecting directories like Pictures and Music.
+ * - Multi-user separation on the same physical device.
+ *
+ * The derived permissions look like this:
+ *
+ * rwxrwx--x root:sdcard_rw /
+ * rwxrwx--- root:sdcard_pics /Pictures
+ * rwxrwx--- root:sdcard_av /Music
+ *
+ * rwxrwx--x root:sdcard_rw /Android
+ * rwxrwx--x root:sdcard_rw /Android/data
+ * rwxrwx--- u0_a12:sdcard_rw /Android/data/com.example
+ * rwxrwx--x root:sdcard_rw /Android/obb/
+ * rwxrwx--- u0_a12:sdcard_rw /Android/obb/com.example
+ *
+ * rwxrwx--- root:sdcard_all /Android/user
+ * rwxrwx--x root:sdcard_rw /Android/user/10
+ * rwxrwx--- u10_a12:sdcard_rw /Android/user/10/Android/data/com.example
*/
#define FUSE_TRACE 0
@@ -89,6 +117,23 @@
* or that a reply has already been written. */
#define NO_STATUS 1
+/* Path to system-provided mapping of package name to appIds */
+static const char* const kPackagesListFile = "/data/system/packages.list";
+
+/* Supplementary groups to execute with */
+static const gid_t kGroups[1] = { AID_PACKAGE_INFO };
+
+/* Permission mode for a specific node. Controls how file permissions
+ * are derived for children nodes. */
+typedef enum {
+ PERM_INHERIT,
+ PERM_ROOT,
+ PERM_ANDROID,
+ PERM_ANDROID_DATA,
+ PERM_ANDROID_OBB,
+ PERM_ANDROID_USER,
+} perm_t;
+
struct handle {
int fd;
};
@@ -97,11 +142,22 @@ struct dirhandle {
DIR *d;
};
+struct package {
+ appid_t appid;
+};
+
struct node {
__u32 refcount;
__u64 nid;
__u64 gen;
+ /* State derived based on current position in hierarchy. */
+ perm_t perm;
+ userid_t userid;
+ uid_t uid;
+ gid_t gid;
+ mode_t mode;
+
struct node *next; /* per-dir sibling list */
struct node *child; /* first contained file by this dir */
struct node *parent; /* containing directory */
@@ -116,14 +172,25 @@ struct node {
char *actual_name;
};
+static int str_hash(void *key) {
+ return hashmapHash(key, strlen(key));
+}
+
+static bool str_equals(void *keyA, void *keyB) {
+ return strcmp(keyA, keyB) == 0;
+}
+
/* Global data structure shared by all fuse handlers. */
struct fuse {
pthread_mutex_t lock;
__u64 next_generation;
int fd;
+ bool derive_perms;
struct node root;
char rootpath[PATH_MAX];
+
+ Hashmap *package_to_appid;
};
/* Private data used by a single fuse handler. */
@@ -258,7 +325,7 @@ static char* find_file_within(const char* path, const char* name,
struct dirent* entry;
DIR* dir = opendir(path);
if (!dir) {
- ERROR("opendir %s failed: %s", path, strerror(errno));
+ ERROR("opendir %s failed: %s\n", path, strerror(errno));
return actual;
}
while ((entry = readdir(dir))) {
@@ -273,9 +340,9 @@ static char* find_file_within(const char* path, const char* name,
return actual;
}
-static void attr_from_stat(struct fuse_attr *attr, const struct stat *s, __u64 nid)
+static void attr_from_stat(struct fuse_attr *attr, const struct stat *s, const struct node* node)
{
- attr->ino = nid;
+ attr->ino = node->nid;
attr->size = s->st_size;
attr->blocks = s->st_blocks;
attr->atime = s->st_atime;
@@ -287,19 +354,89 @@ static void attr_from_stat(struct fuse_attr *attr, const struct stat *s, __u64 n
attr->mode = s->st_mode;
attr->nlink = s->st_nlink;
- /* force permissions to something reasonable:
- * world readable
- * writable by the sdcard group
- */
- if (attr->mode & 0100) {
- attr->mode = (attr->mode & (~0777)) | 0775;
- } else {
- attr->mode = (attr->mode & (~0777)) | 0664;
- }
+ attr->uid = node->uid;
+ attr->gid = node->gid;
- /* all files owned by root.sdcard */
- attr->uid = 0;
- attr->gid = AID_SDCARD_RW;
+ /* Filter requested mode based on underlying file, and
+ * pass through file type. */
+ int owner_mode = s->st_mode & 0700;
+ int filtered_mode = node->mode & (owner_mode | (owner_mode >> 3) | (owner_mode >> 6));
+ attr->mode = (attr->mode & S_IFMT) | filtered_mode;
+}
+
+static void derive_permissions_locked(struct fuse* fuse, struct node *parent,
+ struct node *node) {
+ struct package* package;
+
+ /* By default, each node inherits from its parent */
+ node->perm = PERM_INHERIT;
+ node->userid = parent->userid;
+ node->uid = parent->uid;
+ node->gid = parent->gid;
+ node->mode = parent->mode;
+
+ if (!fuse->derive_perms) {
+ return;
+ }
+
+ /* Derive custom permissions based on parent and current node */
+ switch (parent->perm) {
+ case PERM_INHERIT:
+ /* Already inherited above */
+ break;
+ case PERM_ROOT:
+ if (!strcmp(node->name, "Android")) {
+ /* App-specific directories inside; let anyone traverse */
+ node->perm = PERM_ANDROID;
+ node->mode = 0771;
+ } else if (!strcmp(node->name, "DCIM")
+ || !strcmp(node->name, "Pictures")) {
+ node->gid = AID_SDCARD_PICS;
+ node->mode = 0770;
+ } else if (!strcmp(node->name, "Alarms")
+ || !strcmp(node->name, "Movies")
+ || !strcmp(node->name, "Music")
+ || !strcmp(node->name, "Notifications")
+ || !strcmp(node->name, "Podcasts")
+ || !strcmp(node->name, "Ringtones")) {
+ node->gid = AID_SDCARD_AV;
+ node->mode = 0770;
+ }
+ break;
+ case PERM_ANDROID:
+ if (!strcmp(node->name, "data")) {
+ /* App-specific directories inside; let anyone traverse */
+ node->perm = PERM_ANDROID_DATA;
+ node->mode = 0771;
+ } else if (!strcmp(node->name, "obb")) {
+ /* App-specific directories inside; let anyone traverse */
+ node->perm = PERM_ANDROID_OBB;
+ node->mode = 0771;
+ } else if (!strcmp(node->name, "user")) {
+ /* User directories must only be accessible to system, protected
+ * by sdcard_all. Zygote will bind mount the appropriate user-
+ * specific path. */
+ node->perm = PERM_ANDROID_USER;
+ node->gid = AID_SDCARD_ALL;
+ node->mode = 0770;
+ }
+ break;
+ case PERM_ANDROID_DATA:
+ case PERM_ANDROID_OBB:
+ package = hashmapGet(fuse->package_to_appid, node->name);
+ if (package != NULL) {
+ node->uid = multiuser_get_uid(parent->userid, package->appid);
+ }
+ node->mode = 0770;
+ break;
+ case PERM_ANDROID_USER:
+ /* Root of a secondary user */
+ node->perm = PERM_ROOT;
+ node->userid = atoi(node->name);
+ node->gid = AID_SDCARD_RW;
+ node->mode = 0771;
+ break;
+ }
}
struct node *create_node_locked(struct fuse* fuse,
@@ -330,6 +467,8 @@ struct node *create_node_locked(struct fuse* fuse,
node->namelen = namelen;
node->nid = ptr_to_id(node);
node->gen = fuse->next_generation++;
+
+ derive_permissions_locked(fuse, parent, node);
acquire_node_locked(node);
add_node_to_parent_locked(node, parent);
return node;
@@ -422,18 +561,33 @@ static struct node* acquire_or_create_child_locked(
return child;
}
-static void fuse_init(struct fuse *fuse, int fd, const char *source_path)
+static void fuse_init(struct fuse *fuse, int fd, const char *source_path, bool derive_perms)
{
pthread_mutex_init(&fuse->lock, NULL);
fuse->fd = fd;
fuse->next_generation = 0;
+ fuse->derive_perms = derive_perms;
memset(&fuse->root, 0, sizeof(fuse->root));
fuse->root.nid = FUSE_ROOT_ID; /* 1 */
fuse->root.refcount = 2;
fuse->root.namelen = strlen(source_path);
fuse->root.name = strdup(source_path);
+
+ fuse->root.perm = PERM_ROOT;
+ fuse->root.userid = 0;
+ fuse->root.uid = AID_ROOT;
+ fuse->root.gid = AID_SDCARD_RW;
+ if (derive_perms) {
+ fuse->root.mode = 0771;
+ } else {
+ fuse->root.mode = 0775;
+ }
+
+ if (derive_perms) {
+ fuse->package_to_appid = hashmapCreate(256, str_hash, str_equals);
+ }
}
static void fuse_status(struct fuse *fuse, __u64 unique, int err)
@@ -475,7 +629,36 @@ static int fuse_reply_entry(struct fuse* fuse, __u64 unique,
struct stat s;
if (lstat(path, &s) < 0) {
- return -errno;
+ /* But wait! We'll automatically create a directory if its
+ * a valid package name under data or obb, since apps may not
+ * have enough permissions to create for themselves. */
+ if (errno == ENOENT && (parent->perm == PERM_ANDROID_DATA
+ || parent->perm == PERM_ANDROID_OBB)) {
+ TRACE("automatically creating %s\n", path);
+
+ pthread_mutex_lock(&fuse->lock);
+ bool validPackage = hashmapContainsKey(fuse->package_to_appid, name);
+ pthread_mutex_unlock(&fuse->lock);
+
+ if (!validPackage) {
+ return -ENOENT;
+ }
+ if (mkdir(path, 0775) == -1) {
+ /* We might have raced with ourselves and already created */
+ if (errno != EEXIST) {
+ ERROR("failed to mkdir(%s): %s\n", name, strerror(errno));
+ return -ENOENT;
+ }
+ }
+
+ /* It should exist this time around! */
+ if (lstat(path, &s) < 0) {
+ ERROR("failed to lstat(%s): %s\n", name, strerror(errno));
+ return -errno;
+ }
+ } else {
+ return -errno;
+ }
}
pthread_mutex_lock(&fuse->lock);
@@ -485,7 +668,7 @@ static int fuse_reply_entry(struct fuse* fuse, __u64 unique,
return -ENOMEM;
}
memset(&out, 0, sizeof(out));
- attr_from_stat(&out.attr, &s, node->nid);
+ attr_from_stat(&out.attr, &s, node);
out.attr_valid = 10;
out.entry_valid = 10;
out.nodeid = node->nid;
@@ -495,7 +678,7 @@ static int fuse_reply_entry(struct fuse* fuse, __u64 unique,
return NO_STATUS;
}
-static int fuse_reply_attr(struct fuse* fuse, __u64 unique, __u64 nid,
+static int fuse_reply_attr(struct fuse* fuse, __u64 unique, const struct node* node,
const char* path)
{
struct fuse_attr_out out;
@@ -505,7 +688,7 @@ static int fuse_reply_attr(struct fuse* fuse, __u64 unique, __u64 nid,
return -errno;
}
memset(&out, 0, sizeof(out));
- attr_from_stat(&out.attr, &s, nid);
+ attr_from_stat(&out.attr, &s, node);
out.attr_valid = 10;
fuse_reply(fuse, unique, &out, sizeof(out));
return NO_STATUS;
@@ -567,7 +750,7 @@ static int handle_getattr(struct fuse* fuse, struct fuse_handler* handler,
if (!node) {
return -ENOENT;
}
- return fuse_reply_attr(fuse, hdr->unique, hdr->nodeid, path);
+ return fuse_reply_attr(fuse, hdr->unique, node, path);
}
static int handle_setattr(struct fuse* fuse, struct fuse_handler* handler,
@@ -625,7 +808,7 @@ static int handle_setattr(struct fuse* fuse, struct fuse_handler* handler,
return -errno;
}
}
- return fuse_reply_attr(fuse, hdr->unique, hdr->nodeid, path);
+ return fuse_reply_attr(fuse, hdr->unique, node, path);
}
static int handle_mknod(struct fuse* fuse, struct fuse_handler* handler,
@@ -1206,6 +1389,111 @@ static void* start_handler(void* data)
return NULL;
}
+static bool hashmap_remove(void *key, void *value, void *context) {
+ Hashmap* map = context;
+ hashmapRemove(map, key);
+ free(key);
+ free(value);
+ return true;
+}
+
+static int read_package_list(struct fuse *fuse) {
+ pthread_mutex_lock(&fuse->lock);
+
+ hashmapForEach(fuse->package_to_appid, hashmap_remove, fuse->package_to_appid);
+
+ FILE* file = fopen(kPackagesListFile, "r");
+ if (!file) {
+ ERROR("failed to open package list: %s\n", strerror(errno));
+ pthread_mutex_unlock(&fuse->lock);
+ return -1;
+ }
+
+ char buf[512];
+ while (fgets(buf, sizeof(buf), file) != NULL) {
+ char package_name[512];
+ int appid;
+ if (sscanf(buf, "%s %d", package_name, &appid) == 2) {
+ char* package_name_dup = strdup(package_name);
+ struct package* package = malloc(sizeof(struct package));
+ if (!package_name_dup || !package) {
+ ERROR("cannot allocate package details\n");
+ return -ENOMEM;
+ }
+
+ package->appid = appid;
+ hashmapPut(fuse->package_to_appid, package_name_dup, package);
+ }
+ }
+
+ TRACE("read_package_list: found %d packages\n", hashmapSize(fuse->package_to_appid));
+ fclose(file);
+ pthread_mutex_unlock(&fuse->lock);
+ return 0;
+}
+
+static void watch_package_list(struct fuse* fuse) {
+ struct inotify_event *event;
+ char event_buf[512];
+
+ int nfd = inotify_init();
+ if (nfd < 0) {
+ ERROR("inotify_init failed: %s\n", strerror(errno));
+ return;
+ }
+
+ bool active = false;
+ while (1) {
+ if (!active) {
+ int res = inotify_add_watch(nfd, kPackagesListFile, IN_DELETE_SELF);
+ if (res == -1) {
+ if (errno == ENOENT || errno == EACCES) {
+ /* Framework may not have created yet, sleep and retry */
+ ERROR("missing packages.list; retrying\n");
+ sleep(3);
+ continue;
+ } else {
+ ERROR("inotify_add_watch failed: %s\n", strerror(errno));
+ return;
+ }
+ }
+
+ /* Watch above will tell us about any future changes, so
+ * read the current state. */
+ if (read_package_list(fuse) == -1) {
+ ERROR("read_package_list failed: %s\n", strerror(errno));
+ return;
+ }
+ active = true;
+ }
+
+ int event_pos = 0;
+ int res = read(nfd, event_buf, sizeof(event_buf));
+ if (res < (int) sizeof(*event)) {
+ if (errno == EINTR)
+ continue;
+ ERROR("failed to read inotify event: %s\n", strerror(errno));
+ return;
+ }
+
+ while (res >= (int) sizeof(*event)) {
+ int event_size;
+ event = (struct inotify_event *) (event_buf + event_pos);
+
+ TRACE("inotify event: %08x\n", event->mask);
+ if ((event->mask & IN_IGNORED) == IN_IGNORED) {
+ /* Previously watched file was deleted, probably due to move
+ * that swapped in new data; re-arm the watch and read. */
+ active = false;
+ }
+
+ event_size = sizeof(*event) + event->len;
+ res -= event_size;
+ event_pos += event_size;
+ }
+ }
+}
+
static int ignite_fuse(struct fuse* fuse, int num_threads)
{
struct fuse_handler* handlers;
@@ -1213,7 +1501,7 @@ static int ignite_fuse(struct fuse* fuse, int num_threads)
handlers = malloc(num_threads * sizeof(struct fuse_handler));
if (!handlers) {
- ERROR("cannot allocate storage for threads");
+ ERROR("cannot allocate storage for threads\n");
return -ENOMEM;
}
@@ -1222,16 +1510,25 @@ static int ignite_fuse(struct fuse* fuse, int num_threads)
handlers[i].token = i;
}
- for (i = 1; i < num_threads; i++) {
+ /* When deriving permissions, this thread is used to process inotify events,
+ * otherwise it becomes one of the FUSE handlers. */
+ i = fuse->derive_perms ? 0 : 1;
+ for (; i < num_threads; i++) {
pthread_t thread;
int res = pthread_create(&thread, NULL, start_handler, &handlers[i]);
if (res) {
- ERROR("failed to start thread #%d, error=%d", i, res);
+ ERROR("failed to start thread #%d, error=%d\n", i, res);
goto quit;
}
}
- handle_fuse_requests(&handlers[0]);
- ERROR("terminated prematurely");
+
+ if (fuse->derive_perms) {
+ watch_package_list(fuse);
+ } else {
+ handle_fuse_requests(&handlers[0]);
+ }
+
+ ERROR("terminated prematurely\n");
/* don't bother killing all of the other threads or freeing anything,
* should never get here anyhow */
@@ -1241,14 +1538,17 @@ quit:
static int usage()
{
- ERROR("usage: sdcard [-t<threads>] <source_path> <dest_path> <uid> <gid>\n"
- " -t<threads>: specify number of threads to use, default -t%d\n"
+ ERROR("usage: sdcard [OPTIONS] <source_path> <dest_path>\n"
+ " -u: specify UID to run as\n"
+ " -g: specify GID to run as\n"
+ " -t: specify number of threads to use (default %d)\n"
+ " -d: derive file permissions based on path\n"
"\n", DEFAULT_NUM_THREADS);
return 1;
}
static int run(const char* source_path, const char* dest_path, uid_t uid, gid_t gid,
- int num_threads) {
+ int num_threads, bool derive_perms) {
int fd;
char opts[256];
int res;
@@ -1259,7 +1559,7 @@ static int run(const char* source_path, const char* dest_path, uid_t uid, gid_t
fd = open("/dev/fuse", O_RDWR);
if (fd < 0){
- ERROR("cannot open fuse device (error %d)\n", errno);
+ ERROR("cannot open fuse device: %s\n", strerror(errno));
return -1;
}
@@ -1269,23 +1569,29 @@ static int run(const char* source_path, const char* dest_path, uid_t uid, gid_t
res = mount("/dev/fuse", dest_path, "fuse", MS_NOSUID | MS_NODEV, opts);
if (res < 0) {
- ERROR("cannot mount fuse filesystem (error %d)\n", errno);
+ ERROR("cannot mount fuse filesystem: %s\n", strerror(errno));
+ goto error;
+ }
+
+ res = setgroups(sizeof(kGroups) / sizeof(kGroups[0]), kGroups);
+ if (res < 0) {
+ ERROR("cannot setgroups: %s\n", strerror(errno));
goto error;
}
res = setgid(gid);
if (res < 0) {
- ERROR("cannot setgid (error %d)\n", errno);
+ ERROR("cannot setgid: %s\n", strerror(errno));
goto error;
}
res = setuid(uid);
if (res < 0) {
- ERROR("cannot setuid (error %d)\n", errno);
+ ERROR("cannot setuid: %s\n", strerror(errno));
goto error;
}
- fuse_init(&fuse, fd, source_path);
+ fuse_init(&fuse, fd, source_path, derive_perms);
umask(0);
res = ignite_fuse(&fuse, num_threads);
@@ -1306,33 +1612,41 @@ int main(int argc, char **argv)
uid_t uid = 0;
gid_t gid = 0;
int num_threads = DEFAULT_NUM_THREADS;
+ bool derive_perms = false;
int i;
struct rlimit rlim;
- for (i = 1; i < argc; i++) {
+ int opt;
+ while ((opt = getopt(argc, argv, "u:g:t:d")) != -1) {
+ switch (opt) {
+ case 'u':
+ uid = strtoul(optarg, NULL, 10);
+ break;
+ case 'g':
+ gid = strtoul(optarg, NULL, 10);
+ break;
+ case 't':
+ num_threads = strtoul(optarg, NULL, 10);
+ break;
+ case 'd':
+ derive_perms = true;
+ break;
+ case '?':
+ default:
+ return usage();
+ }
+ }
+
+ for (i = optind; i < argc; i++) {
char* arg = argv[i];
- if (!strncmp(arg, "-t", 2))
- num_threads = strtoul(arg + 2, 0, 10);
- else if (!source_path)
+ if (!source_path) {
source_path = arg;
- else if (!dest_path)
+ } else if (!dest_path) {
dest_path = arg;
- else if (!uid) {
- char* endptr = NULL;
- errno = 0;
- uid = strtoul(arg, &endptr, 10);
- if (*endptr != '\0' || errno != 0) {
- ERROR("Invalid uid");
- return usage();
- }
+ } else if (!uid) {
+ uid = strtoul(arg, NULL, 10);
} else if (!gid) {
- char* endptr = NULL;
- errno = 0;
- gid = strtoul(arg, &endptr, 10);
- if (*endptr != '\0' || errno != 0) {
- ERROR("Invalid gid");
- return usage();
- }
+ gid = strtoul(arg, NULL, 10);
} else {
ERROR("too many arguments\n");
return usage();
@@ -1362,6 +1676,6 @@ int main(int argc, char **argv)
ERROR("Error setting RLIMIT_NOFILE, errno = %d\n", errno);
}
- res = run(source_path, dest_path, uid, gid, num_threads);
+ res = run(source_path, dest_path, uid, gid, num_threads, derive_perms);
return res < 0 ? 1 : 0;
}