Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix directory rename hiding files from lower branches #152

Merged
merged 5 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 41 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,45 @@ int copy_directory(const char *path, int branch_ro, int branch_rw) {
res = 1;
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.
bool is_dir = false;
if (branch_contains_path(branch_rw, member, &is_dir) &&
(!is_dir || (branch_contains_path(branch_ro, member, &is_dir) && !is_dir))) {
// 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
74 changes: 66 additions & 8 deletions src/findbranch.c
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
#include "debug.h"
#include "usyslog.h"

static bool branch_contains_path(int branch, const char *path, bool *is_dir) {
bool branch_contains_path(int branch, const char *path, bool *is_dir) {
if (branch < 0 || branch >= uopt.nbranches) {
RETURN(false);
}
Expand Down 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,15 +237,15 @@ 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);

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

// the found branch is writable, good!
// the found branch is writable, good! We don't need to do any cow-copying.
if (uopt.branches[branch_rorw].rw) RETURN(branch_rorw);

// cow is disabled and branch is not writable, so deny write permission
Expand All @@ -265,7 +261,69 @@ 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);

// the found branch is writable and the only branch containing the directory, good!
// We don't need to do any cow-copying.
if (uopt.branches[branch_rorw].rw) {
// Loop through all lower branches and check if the directory exists in any of them
bool only_branch = true;
for (int i = branch_rorw + 1; i < uopt.nbranches; i++) {
bool is_dir = false;
if (branch_contains_path(i, path, &is_dir) && is_dir) {
only_branch = false;
break;
}
}
if (only_branch) RETURN(branch_rorw);
}

// cow is disabled and branch is not writable and only branch, 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
3 changes: 2 additions & 1 deletion src/findbranch.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ typedef enum searchflag {
RWONLY
} searchflag_t;

bool branch_contains_path(int branch, const char *path, bool *is_dir);
bool branch_contains_file_or_parent_dir(int branch, const char *path);
int find_rorw_branch(const char *path);
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
Loading
Loading