Skip to content

Commit

Permalink
Fix directory rename hiding files from lower branches
Browse files Browse the repository at this point in the history
  • Loading branch information
Tuupertunut committed Dec 19, 2024
1 parent 0782274 commit f984cef
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 17 deletions.
58 changes: 56 additions & 2 deletions src/cow.c
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ int path_create_cutlast_cow(const char *path, int nbranch_ro, int nbranch_rw) {
/**
* initiate the cow-copy action
*/
int cow_cp(const char *path, int branch_ro, int branch_rw, bool copy_dir) {
int cow_cp(const char *path, int branch_ro, int branch_rw, bool recursive) {
DBG("%s\n", path);

// create the path to the file
Expand Down Expand Up @@ -94,7 +94,7 @@ int cow_cp(const char *path, int branch_ro, int branch_rw, bool copy_dir) {
res = copy_link(&cow);
break;
case S_IFDIR:
if (copy_dir) {
if (recursive) {
res = copy_directory(path, branch_ro, branch_rw);
} else {
res = path_create_cow(path, branch_ro, branch_rw);
Expand Down Expand Up @@ -145,6 +145,60 @@ int copy_directory(const char *path, int branch_ro, int branch_rw) {
res = 1;
break;
}

char from_member[PATHLEN_MAX], to_member[PATHLEN_MAX];
if (BUILD_PATH(from_member, uopt.branches[branch_ro].path, member)) {
res = -ENAMETOOLONG;
break;
}
if (BUILD_PATH(to_member, uopt.branches[branch_rw].path, member)) {
res = -ENAMETOOLONG;
break;
}

// Generally if the target file already exists, we should not copy
// anything. Directories are a special case as their contents may still
// need to be merged.
struct stat buf;
if (
!lstat(to_member, &buf) &&
(
(buf.st_mode & S_IFMT) != S_IFDIR ||
(!lstat(from_member, &buf) && (buf.st_mode & S_IFMT) != S_IFDIR)
)
) {
// File already exists in target and either source or target is not
// a directory, skip it
DBG("file %s copy skipped, exists in target\n", member);
continue;
}

// If source file is hidden by a higher branch, we should not copy
// anything. We do this by iterating through all higher branches and
// checking if they have a whiteout. This is somewhat inefficient, but
// it is the only simple way to handle this case. A more complex
// solution would be to collect whiteouts to a hashtable, as has been
// done in readdir.
bool skip = false;
for (int i = 0; i < branch_ro; i++) {
// Assuming that only rw branches can have whiteouts
if (uopt.branches[i].rw) {
int hidden = path_hidden(member, i);
if (hidden > 0) {
// File was hidden, skip it
DBG("file %s copy skipped, hidden by layer %i\n", member, i);
skip = true;
break;
} else if (hidden < 0) {
// error in path_hidden
res = hidden;
break;
}
}
}
if (res != 0) break;
if (skip) continue;

res = cow_cp(member, branch_ro, branch_rw, true);
if (res != 0) break;
}
Expand Down
2 changes: 1 addition & 1 deletion src/cow.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

#include <sys/stat.h>

int cow_cp(const char *path, int branch_ro, int branch_rw, bool copy_dir);
int cow_cp(const char *path, int branch_ro, int branch_rw, bool recursive);
int path_create_cow(const char *path, int nbranch_ro, int nbranch_rw);
int path_create_cutlast_cow(const char *path, int nbranch_ro, int nbranch_rw);
int copy_directory(const char *path, int branch_ro, int branch_rw);
Expand Down
55 changes: 49 additions & 6 deletions src/findbranch.c
Original file line number Diff line number Diff line change
Expand Up @@ -229,10 +229,6 @@ int find_rw_branch_cutlast(const char *path) {
RETURN(res);
}

int find_rw_branch_cow(const char *path) {
return find_rw_branch_cow_common(path, false);
}

/**
* copy-on-write
* Find path in a union branch and if this branch is read-only,
Expand All @@ -241,7 +237,7 @@ int find_rw_branch_cow(const char *path) {
* It will definitely fail, when a ro-branch is on top of a rw-branch
* and a directory is to be copied from ro- to rw-branch.
*/
int find_rw_branch_cow_common(const char *path, bool copy_dir) {
int find_rw_branch_cow(const char *path) {
DBG("%s\n", path);

int branch_rorw = find_rorw_branch(path);
Expand All @@ -265,7 +261,54 @@ int find_rw_branch_cow_common(const char *path, bool copy_dir) {
RETURN(-1);
}

if (cow_cp(path, branch_rorw, branch_rw, copy_dir)) RETURN(-1);
if (cow_cp(path, branch_rorw, branch_rw, false)) RETURN(-1);

// remove a file that might hide the copied file
remove_hidden(path, branch_rw);

RETURN(branch_rw);
}

/**
* copy-on-write, recursive version
* Ensure that a directory path and all its contents are copied to a read-write
* branch from every other branch.
*/
int find_rw_branch_cow_recursive(const char *path) {
DBG("%s\n", path);

int branch_rorw = find_rorw_branch(path);

// not found anywhere
if (branch_rorw < 0) RETURN(-1);

// cow is disabled, so deny write permission
if (!uopt.cow_enabled) {
errno = EACCES;
RETURN(-1);
}

// +1 so that branch_rw can be the same as branch_rorw
int branch_rw = find_lowest_rw_branch(branch_rorw + 1);
if (branch_rw < 0) {
// no writable branch found
errno = EACCES;
RETURN(-1);
}

// Directory copy must be performed for branch_rorw and every branch below
// it because every branch may contain different pieces of the directory's
// contents. However branch_rw should not be copied to itself.
int first_copied_branch = branch_rorw == branch_rw ? branch_rorw + 1 : branch_rorw;
for (int i = first_copied_branch; i < uopt.nbranches; i++) {
bool is_dir = false;
if (branch_contains_path(i, path, &is_dir) && is_dir) {
// Recursive copy. File overwriting is not allowed so previously
// copied higher priority branches are not overwritten.
DBG("starting recursive copy from %i to %i\n", i, branch_rw);
if (cow_cp(path, i, branch_rw, true)) RETURN(-1);
}
}

// remove a file that might hide the copied file
remove_hidden(path, branch_rw);
Expand Down
2 changes: 1 addition & 1 deletion src/findbranch.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ int find_lowest_rw_branch(int branch_ro);
int find_rw_branch_cutlast(const char *path);
int __find_rw_branch_cutlast(const char *path, int rw_hint);
int find_rw_branch_cow(const char *path);
int find_rw_branch_cow_common(const char *path, bool copy_dir);
int find_rw_branch_cow_recursive(const char *path);

#endif
23 changes: 16 additions & 7 deletions src/fuse_ops.c
Original file line number Diff line number Diff line change
Expand Up @@ -481,8 +481,6 @@ static int unionfs_release(const char *path, struct fuse_file_info *fi) {

/**
* unionfs rename function
* TODO: If we rename a directory on a read-only branch, we need to copy over
* all files to the renamed directory on the read-write branch.
*/
#if FUSE_USE_VERSION < 30
static int unionfs_rename(const char *from, const char *to) {
Expand Down Expand Up @@ -524,22 +522,33 @@ static int unionfs_rename(const char *from, const char *to, unsigned int flags)
}
}

if (!uopt.branches[i].rw) {
i = find_rw_branch_cow_common(from, true);
if (i == -1) RETURN(-errno);
char f[PATHLEN_MAX], t[PATHLEN_MAX];
if (BUILD_PATH(f, uopt.branches[i].path, from)) RETURN(-ENAMETOOLONG);

filetype_t ftype = path_is_dir(f);
if (ftype == NOT_EXISTING) {
RETURN(-ENOENT);
} else if (ftype == IS_DIR) {
is_dir = true;
}

if (is_dir) {
i = find_rw_branch_cow_recursive(from);
} else if (!uopt.branches[i].rw) {
i = find_rw_branch_cow(from);
}
if (i == -1) RETURN(-errno);

if (i != j) {
USYSLOG(LOG_ERR, "%s: from and to are on different writable branches %d vs %d, which"
"is not supported yet.\n", __func__, i, j);
RETURN(-EXDEV);
}

char f[PATHLEN_MAX], t[PATHLEN_MAX];
if (BUILD_PATH(f, uopt.branches[i].path, from)) RETURN(-ENAMETOOLONG);
if (BUILD_PATH(t, uopt.branches[i].path, to)) RETURN(-ENAMETOOLONG);

filetype_t ftype = path_is_dir(f);
ftype = path_is_dir(f);
if (ftype == NOT_EXISTING) {
RETURN(-ENOENT);
} else if (ftype == IS_DIR) {
Expand Down

0 comments on commit f984cef

Please sign in to comment.