[PATCH 3/3] ovl: redirect on rename-dir
From: Miklos Szeredi
Date: Tue Oct 25 2016 - 03:35:26 EST
Current code returns EXDEV when a directory would need to be copied up to
move. We could copy up the directory tree in this case, but there's
another solution: point to old lower directory from moved upper directory.
This is achieved with a "trusted.overlay.redirect" xattr storing the path
relative to the root of the overlay. After such attribute has been set,
the directory can be moved without further actions required.
This is a backward incompatible feature, old kernels won't be able to
correctly mount an overlay containing redirected directories.
Signed-off-by: Miklos Szeredi <mszeredi@xxxxxxxxxx>
---
Documentation/filesystems/overlayfs.txt | 21 ++++++-
fs/overlayfs/copy_up.c | 20 ++-----
fs/overlayfs/dir.c | 86 +++++++++++++++++++---------
fs/overlayfs/namei.c | 99 ++++++++++++++++++++++++++++++---
fs/overlayfs/overlayfs.h | 4 ++
fs/overlayfs/ovl_entry.h | 4 ++
fs/overlayfs/super.c | 25 +++++----
fs/overlayfs/util.c | 19 +++++++
8 files changed, 217 insertions(+), 61 deletions(-)
diff --git a/Documentation/filesystems/overlayfs.txt b/Documentation/filesystems/overlayfs.txt
index 5108425157ac..fae48ab3b36b 100644
--- a/Documentation/filesystems/overlayfs.txt
+++ b/Documentation/filesystems/overlayfs.txt
@@ -130,6 +130,23 @@ directory.
Readdir on directories that are not merged is simply handled by the
underlying directory (upper or lower).
+renaming directories
+--------------------
+
+When renaming a directory that is on the lower layer or merged (i.e. the
+directory was not created on the upper layer to start with) overlayfs can
+handle it in two different ways:
+
+1) return EXDEV error: this error is returned by rename(2) when trying to
+ move a file or directory across filesystem boundaries. Hence
+ applications are usually prepared to hande this error (mv(1) for example
+ recursively copies the directory tree). This is the default behavior.
+
+2) If the "redirect_dir" feature is enabled, then the directory will be
+ copied up (but not the contents). Then the "trusted.overlay.redirect"
+ extended attribute is set to the path of the original location from the
+ root of the overlay. Finally the directory is moved to the new
+ location.
Non-directories
---------------
@@ -201,8 +218,8 @@ If a file with multiple hard links is copied up, then this will
"break" the link. Changes will not be propagated to other names
referring to the same inode.
-Directory trees are not copied up. If rename(2) is performed on a directory
-which is on the lower layer or is merged, then -EXDEV will be returned.
+Unless "redirect_dir" feature is enabled, rename(2) on a lower or merged
+directory will fail with EXDEV.
Changes to underlying filesystems
---------------------------------
diff --git a/fs/overlayfs/copy_up.c b/fs/overlayfs/copy_up.c
index 49f6158bb04c..0d7075208099 100644
--- a/fs/overlayfs/copy_up.c
+++ b/fs/overlayfs/copy_up.c
@@ -323,17 +323,11 @@ static int ovl_copy_up_locked(struct dentry *workdir, struct dentry *upperdir,
/*
* Copy up a single dentry
*
- * Directory renames only allowed on "pure upper" (already created on
- * upper filesystem, never copied up). Directories which are on lower or
- * are merged may not be renamed. For these -EXDEV is returned and
- * userspace has to deal with it. This means, when copying up a
- * directory we can rely on it and ancestors being stable.
- *
- * Non-directory renames start with copy up of source if necessary. The
- * actual rename will only proceed once the copy up was successful. Copy
- * up uses upper parent i_mutex for exclusion. Since rename can change
- * d_parent it is possible that the copy up will lock the old parent. At
- * that point the file will have already been copied up anyway.
+ * All renames start with copy up of source if necessary. The actual
+ * rename will only proceed once the copy up was successful. Copy up uses
+ * upper parent i_mutex for exclusion. Since rename can change d_parent it
+ * is possible that the copy up will lock the old parent. At that point
+ * the file will have already been copied up anyway.
*/
int ovl_copy_up_one(struct dentry *parent, struct dentry *dentry,
struct path *lowerpath, struct kstat *stat)
@@ -345,7 +339,6 @@ int ovl_copy_up_one(struct dentry *parent, struct dentry *dentry,
struct path parentpath;
struct dentry *lowerdentry = lowerpath->dentry;
struct dentry *upperdir;
- struct dentry *upperdentry;
const char *link = NULL;
if (WARN_ON(!workdir))
@@ -371,8 +364,7 @@ int ovl_copy_up_one(struct dentry *parent, struct dentry *dentry,
pr_err("overlayfs: failed to lock workdir+upperdir\n");
goto out_unlock;
}
- upperdentry = ovl_dentry_upper(dentry);
- if (upperdentry) {
+ if (ovl_dentry_upper(dentry)) {
/* Raced with another copy-up? Nothing to do, then... */
err = 0;
goto out_unlock;
diff --git a/fs/overlayfs/dir.c b/fs/overlayfs/dir.c
index 2c1057d747cb..065e0211f9b6 100644
--- a/fs/overlayfs/dir.c
+++ b/fs/overlayfs/dir.c
@@ -757,6 +757,40 @@ static bool ovl_type_merge_or_lower(struct dentry *dentry)
return OVL_TYPE_MERGE(type) || !OVL_TYPE_UPPER(type);
}
+static bool ovl_can_move(struct dentry *dentry)
+{
+ return ovl_redirect_dir(dentry->d_sb) ||
+ !d_is_dir(dentry) || !ovl_type_merge_or_lower(dentry);
+}
+
+static int ovl_set_redirect(struct dentry *dentry)
+{
+ char *buf;
+ char *path;
+ int err;
+
+ if (ovl_dentry_is_redirect(dentry))
+ return 0;
+
+ buf = __getname();
+ if (!buf)
+ return -ENOMEM;
+
+ path = dentry_path_raw(dentry, buf, PATH_MAX);
+ err = PTR_ERR(path);
+ if (IS_ERR(path))
+ goto putname;
+
+ err = ovl_do_setxattr(ovl_dentry_upper(dentry), OVL_XATTR_REDIRECT,
+ path, strlen(path), 0);
+putname:
+ __putname(buf);
+ if (!err)
+ ovl_dentry_set_redirect(dentry);
+
+ return err;
+}
+
static int ovl_rename(struct inode *olddir, struct dentry *old,
struct inode *newdir, struct dentry *new,
unsigned int flags)
@@ -784,9 +818,9 @@ static int ovl_rename(struct inode *olddir, struct dentry *old,
/* Don't copy up directory trees */
err = -EXDEV;
- if (is_dir && ovl_type_merge_or_lower(old))
+ if (!ovl_can_move(old))
goto out;
- if (!overwrite && new_is_dir && ovl_type_merge_or_lower(new))
+ if (!overwrite && !ovl_can_move(new))
goto out;
err = ovl_want_write(old);
@@ -837,7 +871,6 @@ static int ovl_rename(struct inode *olddir, struct dentry *old,
trap = lock_rename(new_upperdir, old_upperdir);
-
olddentry = lookup_one_len(old->d_name.name, old_upperdir,
old->d_name.len);
err = PTR_ERR(olddentry);
@@ -880,31 +913,34 @@ static int ovl_rename(struct inode *olddir, struct dentry *old,
if (WARN_ON(olddentry->d_inode == newdentry->d_inode))
goto out_dput;
- if (is_dir && !old_opaque && ovl_lower_positive(new)) {
- err = ovl_set_opaque(olddentry);
- if (err)
- goto out_dput;
- ovl_dentry_set_opaque(old, true);
+ if (is_dir) {
+ if (ovl_type_merge_or_lower(old)) {
+ err = ovl_set_redirect(old);
+ if (err)
+ goto out_dput;
+ } else if (!old_opaque && ovl_lower_positive(new)) {
+ err = ovl_set_opaque(olddentry);
+ if (err)
+ goto out_dput;
+ ovl_dentry_set_opaque(old, true);
+ }
}
- if (!overwrite &&
- new_is_dir && !new_opaque && ovl_lower_positive(old)) {
- err = ovl_set_opaque(newdentry);
- if (err)
- goto out_dput;
- ovl_dentry_set_opaque(new, true);
+ if (!overwrite && new_is_dir) {
+ if (ovl_type_merge_or_lower(new)) {
+ err = ovl_set_redirect(new);
+ if (err)
+ goto out_dput;
+ } else if (!new_opaque && ovl_lower_positive(old)) {
+ err = ovl_set_opaque(newdentry);
+ if (err)
+ goto out_dput;
+ ovl_dentry_set_opaque(new, true);
+ }
}
- if (old_opaque || new_opaque) {
- err = ovl_do_rename(old_upperdir->d_inode, olddentry,
- new_upperdir->d_inode, newdentry,
- flags);
- } else {
- /* No debug for the plain case */
- BUG_ON(flags & ~RENAME_EXCHANGE);
- err = vfs_rename(old_upperdir->d_inode, olddentry,
- new_upperdir->d_inode, newdentry,
- NULL, flags);
- }
+ err = ovl_do_rename(old_upperdir->d_inode, olddentry,
+ new_upperdir->d_inode, newdentry,
+ flags);
if (err)
goto out_dput;
diff --git a/fs/overlayfs/namei.c b/fs/overlayfs/namei.c
index f4057fcb0246..c7cacbb8ce09 100644
--- a/fs/overlayfs/namei.c
+++ b/fs/overlayfs/namei.c
@@ -8,6 +8,7 @@
*/
#include <linux/fs.h>
+#include <linux/mount.h>
#include <linux/namei.h>
#include <linux/xattr.h>
#include "overlayfs.h"
@@ -48,6 +49,28 @@ static bool ovl_is_opaquedir(struct dentry *dentry)
return false;
}
+static int ovl_check_redirect(struct dentry *dentry, char **bufp)
+{
+ int res;
+
+ res = vfs_getxattr(dentry, OVL_XATTR_REDIRECT, NULL, 0);
+ if (res > 0) {
+ char *buf = kzalloc(res + 1, GFP_KERNEL);
+
+ if (!buf)
+ return -ENOMEM;
+
+ res = vfs_getxattr(dentry, OVL_XATTR_REDIRECT, buf, res);
+ if (res < 0)
+ return res;
+
+ kfree(*bufp);
+ *bufp = buf;
+ }
+
+ return 0;
+}
+
/*
* Returns next layer in stack starting from top.
* Returns -1 if this is the last layer.
@@ -80,11 +103,13 @@ struct dentry *ovl_lookup(struct inode *dir, struct dentry *dentry,
unsigned int ctr = 0;
struct inode *inode = NULL;
bool upperopaque = false;
+ bool upperredirect = false;
bool stop = false;
bool isdir = false;
struct dentry *this;
unsigned int i;
int err;
+ char *redirect = NULL;
old_cred = ovl_override_creds(dentry->d_sb);
upperdir = ovl_upperdentry_dereference(poe);
@@ -110,11 +135,23 @@ struct dentry *ovl_lookup(struct inode *dir, struct dentry *dentry,
isdir = true;
if (poe->numlower && ovl_is_opaquedir(this))
stop = upperopaque = true;
+ else if (ovl_redirect_dir(dentry->d_sb)) {
+ err = ovl_check_redirect(this,
+ &redirect);
+ if (err)
+ goto out;
+
+ if (redirect)
+ upperredirect = true;
+ }
}
}
upperdentry = this;
}
+ if (redirect)
+ poe = dentry->d_sb->s_root->d_fsdata;
+
if (!stop && poe->numlower) {
err = -ENOMEM;
stack = kcalloc(poe->numlower, sizeof(struct path), GFP_KERNEL);
@@ -125,15 +162,39 @@ struct dentry *ovl_lookup(struct inode *dir, struct dentry *dentry,
for (i = 0; !stop && i < poe->numlower; i++) {
struct path lowerpath = poe->lowerstack[i];
- this = ovl_lookup_real(lowerpath.dentry, &dentry->d_name);
- err = PTR_ERR(this);
- if (IS_ERR(this)) {
- /*
- * If it's positive, then treat ENAMETOOLONG as ENOENT.
- */
- if (err == -ENAMETOOLONG && (upperdentry || ctr))
- continue;
- goto out_put;
+ if (!redirect) {
+ this = ovl_lookup_real(lowerpath.dentry, &dentry->d_name);
+ err = PTR_ERR(this);
+ if (IS_ERR(this)) {
+ /*
+ * If it's positive, then treat ENAMETOOLONG as ENOENT.
+ */
+ if (err == -ENAMETOOLONG && (upperdentry || ctr))
+ continue;
+ goto out_put;
+ }
+ } else {
+ struct path thispath;
+
+ err = vfs_path_lookup(lowerpath.dentry, lowerpath.mnt,
+ redirect, 0, &thispath);
+
+ if (err) {
+ if (err == -ENOENT || err == -ENAMETOOLONG)
+ this = NULL;
+ } else {
+ this = thispath.dentry;
+ mntput(thispath.mnt);
+ if (!this->d_inode) {
+ dput(this);
+ this = NULL;
+ } else if (ovl_dentry_weird(this)) {
+ dput(this);
+ err = -EREMOTE;
+ }
+ }
+ if (err)
+ goto out_put;
}
if (!this)
continue;
@@ -162,6 +223,23 @@ struct dentry *ovl_lookup(struct inode *dir, struct dentry *dentry,
stack[ctr].dentry = this;
stack[ctr].mnt = lowerpath.mnt;
ctr++;
+
+ if (!stop && i != poe->numlower - 1 &&
+ d_is_dir(this) && ovl_redirect_dir(dentry->d_sb)) {
+ err = ovl_check_redirect(this, &redirect);
+ if (err)
+ goto out_put;
+
+ if (redirect && poe != dentry->d_sb->s_root->d_fsdata) {
+ poe = dentry->d_sb->s_root->d_fsdata;
+
+ for (i = 0; i < poe->numlower; i++)
+ if (poe->lowerstack[i].mnt == lowerpath.mnt)
+ break;
+ if (WARN_ON(i == poe->numlower))
+ break;
+ }
+ }
}
oe = ovl_alloc_entry(ctr);
@@ -192,9 +270,11 @@ struct dentry *ovl_lookup(struct inode *dir, struct dentry *dentry,
revert_creds(old_cred);
oe->opaque = upperopaque;
+ oe->redirect = upperredirect;
oe->__upperdentry = upperdentry;
memcpy(oe->lowerstack, stack, sizeof(struct path) * ctr);
kfree(stack);
+ kfree(redirect);
dentry->d_fsdata = oe;
d_add(dentry, inode);
@@ -209,6 +289,7 @@ struct dentry *ovl_lookup(struct inode *dir, struct dentry *dentry,
out_put_upper:
dput(upperdentry);
out:
+ kfree(redirect);
revert_creds(old_cred);
return ERR_PTR(err);
}
diff --git a/fs/overlayfs/overlayfs.h b/fs/overlayfs/overlayfs.h
index d61d5b9d0d91..9d80ce367ad8 100644
--- a/fs/overlayfs/overlayfs.h
+++ b/fs/overlayfs/overlayfs.h
@@ -20,6 +20,7 @@ enum ovl_path_type {
#define OVL_XATTR_PREFIX XATTR_TRUSTED_PREFIX "overlay."
#define OVL_XATTR_OPAQUE OVL_XATTR_PREFIX "opaque"
#define OVL_XATTR_FEATURES OVL_XATTR_PREFIX "features"
+#define OVL_XATTR_REDIRECT OVL_XATTR_PREFIX "redirect"
#define OVL_ISUPPER_MASK 1UL
@@ -157,6 +158,9 @@ void ovl_set_dir_cache(struct dentry *dentry, struct ovl_dir_cache *cache);
bool ovl_dentry_is_opaque(struct dentry *dentry);
bool ovl_dentry_is_whiteout(struct dentry *dentry);
void ovl_dentry_set_opaque(struct dentry *dentry, bool opaque);
+bool ovl_redirect_dir(struct super_block *sb);
+bool ovl_dentry_is_redirect(struct dentry *dentry);
+void ovl_dentry_set_redirect(struct dentry *dentry);
void ovl_dentry_update(struct dentry *dentry, struct dentry *upperdentry);
void ovl_inode_init(struct inode *inode, struct inode *realinode,
bool is_upper);
diff --git a/fs/overlayfs/ovl_entry.h b/fs/overlayfs/ovl_entry.h
index 3b7ba59ad27e..2b22645535ff 100644
--- a/fs/overlayfs/ovl_entry.h
+++ b/fs/overlayfs/ovl_entry.h
@@ -26,6 +26,9 @@ struct ovl_fs {
struct ovl_config config;
/* creds of process who forced instantiation of super block */
const struct cred *creator_cred;
+
+ /* Check for "redirect" on directories */
+ bool redirect_dir;
};
/* private information held for every overlayfs dentry */
@@ -36,6 +39,7 @@ struct ovl_entry {
struct {
u64 version;
bool opaque;
+ bool redirect;
};
struct rcu_head rcu;
};
diff --git a/fs/overlayfs/super.c b/fs/overlayfs/super.c
index d6dc8d905d00..fc22a8a2e0d0 100644
--- a/fs/overlayfs/super.c
+++ b/fs/overlayfs/super.c
@@ -397,7 +397,7 @@ static struct dentry *ovl_workdir_create(struct vfsmount *mnt,
goto out_unlock;
}
-static int ovl_check_features(struct dentry *root)
+static int ovl_check_features(struct ovl_fs *ufs, struct dentry *root)
{
int res;
char *buf, *tmp, *p;
@@ -421,8 +421,12 @@ static int ovl_check_features(struct dentry *root)
res = 0;
tmp = buf;
while ((p = strsep(&tmp, ",")) != NULL) {
- res = -EINVAL;
- pr_err("overlayfs: feature '%s' not supported\n", p);
+ if (strcmp(p, "redirect_dir") == 0) {
+ ufs->redirect_dir = true;
+ } else {
+ res = -EINVAL;
+ pr_err("overlayfs: feature '%s' not supported\n", p);
+ }
}
out_free:
kfree(buf);
@@ -494,8 +498,8 @@ static int ovl_mount_dir(const char *name, struct path *path)
return err;
}
-static int ovl_lower_dir(const char *name, struct path *path, long *namelen,
- int *stack_depth, bool *remote)
+static int ovl_lower_dir(const char *name, struct path *path,
+ struct ovl_fs *ufs, int *stack_depth, bool *remote)
{
int err;
struct kstatfs statfs;
@@ -504,7 +508,7 @@ static int ovl_lower_dir(const char *name, struct path *path, long *namelen,
if (err)
goto out;
- err = ovl_check_features(path->dentry);
+ err = ovl_check_features(ufs, path->dentry);
if (err)
goto out_put;
@@ -513,7 +517,7 @@ static int ovl_lower_dir(const char *name, struct path *path, long *namelen,
pr_err("overlayfs: statfs failed on '%s'\n", name);
goto out_put;
}
- *namelen = max(*namelen, statfs.f_namelen);
+ ufs->lower_namelen = max(ufs->lower_namelen, statfs.f_namelen);
*stack_depth = max(*stack_depth, path->mnt->mnt_sb->s_stack_depth);
if (ovl_dentry_remote(path->dentry))
@@ -730,7 +734,7 @@ static int ovl_fill_super(struct super_block *sb, void *data, int silent)
goto out_put_upperpath;
}
- err = ovl_check_features(upperpath.dentry);
+ err = ovl_check_features(ufs, upperpath.dentry);
if (err)
goto out_put_upperpath;
@@ -771,9 +775,8 @@ static int ovl_fill_super(struct super_block *sb, void *data, int silent)
lower = lowertmp;
for (numlower = 0; numlower < stacklen; numlower++) {
- err = ovl_lower_dir(lower, &stack[numlower],
- &ufs->lower_namelen, &sb->s_stack_depth,
- &remote);
+ err = ovl_lower_dir(lower, &stack[numlower], ufs,
+ &sb->s_stack_depth, &remote);
if (err)
goto out_put_lowerpath;
diff --git a/fs/overlayfs/util.c b/fs/overlayfs/util.c
index 0d45a84468d2..06dae4cabd53 100644
--- a/fs/overlayfs/util.c
+++ b/fs/overlayfs/util.c
@@ -176,6 +176,25 @@ void ovl_dentry_set_opaque(struct dentry *dentry, bool opaque)
oe->opaque = opaque;
}
+bool ovl_redirect_dir(struct super_block *sb)
+{
+ struct ovl_fs *ofs = sb->s_fs_info;
+
+ return ofs->redirect_dir;
+}
+
+bool ovl_dentry_is_redirect(struct dentry *dentry)
+{
+ struct ovl_entry *oe = dentry->d_fsdata;
+ return oe->redirect;
+}
+
+void ovl_dentry_set_redirect(struct dentry *dentry)
+{
+ struct ovl_entry *oe = dentry->d_fsdata;
+ oe->redirect = true;
+}
+
void ovl_dentry_update(struct dentry *dentry, struct dentry *upperdentry)
{
struct ovl_entry *oe = dentry->d_fsdata;
--
2.5.5