From a6c6065511411c57167a6cdae0c33263fb662b51 Mon Sep 17 00:00:00 2001 From: Miklos Szeredi Date: Fri, 16 Dec 2016 11:02:56 +0100 Subject: [PATCH] ovl: redirect on rename-dir 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, simpler 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 --- Documentation/filesystems/overlayfs.txt | 21 +++- fs/overlayfs/copy_up.c | 20 ++-- fs/overlayfs/dir.c | 138 +++++++++++++++++++++--- fs/overlayfs/overlayfs.h | 4 + fs/overlayfs/ovl_entry.h | 1 + fs/overlayfs/super.c | 12 +++ fs/overlayfs/util.c | 29 +++++ 7 files changed, 195 insertions(+), 30 deletions(-) diff --git a/Documentation/filesystems/overlayfs.txt b/Documentation/filesystems/overlayfs.txt index 7aeb8e8d80cf..fb6f3070c6fe 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 --------------- @@ -189,8 +206,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 f18c1a616e9e..e191c631b17f 100644 --- a/fs/overlayfs/copy_up.c +++ b/fs/overlayfs/copy_up.c @@ -324,17 +324,11 @@ out_cleanup: /* * 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) @@ -346,7 +340,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)) @@ -372,8 +365,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 f24b6b967901..c1de84c1c5ec 100644 --- a/fs/overlayfs/dir.c +++ b/fs/overlayfs/dir.c @@ -15,6 +15,7 @@ #include #include #include +#include #include "overlayfs.h" void ovl_cleanup(struct inode *wdir, struct dentry *wdentry) @@ -757,6 +758,104 @@ 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); +} + +#define OVL_REDIRECT_MAX 256 + +static char *ovl_get_redirect(struct dentry *dentry, bool samedir) +{ + char *buf, *ret; + struct dentry *d, *tmp; + int buflen = OVL_REDIRECT_MAX + 1; + + if (samedir) { + ret = kstrndup(dentry->d_name.name, dentry->d_name.len, + GFP_KERNEL); + goto out; + } + + buf = ret = kmalloc(buflen, GFP_TEMPORARY); + if (!buf) + goto out; + + buflen--; + buf[buflen] = '\0'; + for (d = dget(dentry); !IS_ROOT(d);) { + const char *name; + int thislen; + + spin_lock(&d->d_lock); + name = ovl_dentry_get_redirect(d); + if (name) { + thislen = strlen(name); + } else { + name = d->d_name.name; + thislen = d->d_name.len; + } + + /* If path is too long, fall back to userspace move */ + if (thislen + (name[0] != '/') > buflen) { + ret = ERR_PTR(-EXDEV); + spin_unlock(&d->d_lock); + goto out_put; + } + + buflen -= thislen; + memcpy(&buf[buflen], name, thislen); + tmp = dget_dlock(d->d_parent); + spin_unlock(&d->d_lock); + + dput(d); + d = tmp; + + /* Absolute redirect: finished */ + if (buf[buflen] == '/') + break; + buflen--; + buf[buflen] = '/'; + } + ret = kstrdup(&buf[buflen], GFP_KERNEL); +out_put: + dput(d); + kfree(buf); +out: + return ret ? ret : ERR_PTR(-ENOMEM); +} + +static int ovl_set_redirect(struct dentry *dentry, bool samedir) +{ + int err; + const char *redirect = ovl_dentry_get_redirect(dentry); + + if (redirect && (samedir || redirect[0] == '/')) + return 0; + + redirect = ovl_get_redirect(dentry, samedir); + if (IS_ERR(redirect)) + return PTR_ERR(redirect); + + err = ovl_do_setxattr(ovl_dentry_upper(dentry), OVL_XATTR_REDIRECT, + redirect, strlen(redirect), 0); + if (!err) { + spin_lock(&dentry->d_lock); + ovl_dentry_set_redirect(dentry, redirect); + spin_unlock(&dentry->d_lock); + } else { + kfree(redirect); + if (err == -EOPNOTSUPP) + ovl_clear_redirect_dir(dentry->d_sb); + else + pr_warn_ratelimited("overlay: failed to set redirect (%i)\n", err); + /* Fall back to userspace copy-up */ + err = -EXDEV; + } + return err; +} + static int ovl_rename(struct inode *olddir, struct dentry *old, struct inode *newdir, struct dentry *new, unsigned int flags) @@ -773,6 +872,7 @@ static int ovl_rename(struct inode *olddir, struct dentry *old, bool overwrite = !(flags & RENAME_EXCHANGE); bool is_dir = d_is_dir(old); bool new_is_dir = d_is_dir(new); + bool samedir = olddir == newdir; struct dentry *opaquedir = NULL; const struct cred *old_cred = NULL; @@ -784,9 +884,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 +937,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,18 +979,29 @@ 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, samedir); + 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, samedir); + 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); + } } err = ovl_do_rename(old_upperdir->d_inode, olddentry, diff --git a/fs/overlayfs/overlayfs.h b/fs/overlayfs/overlayfs.h index e76d9d529e64..bdda37fa3f67 100644 --- a/fs/overlayfs/overlayfs.h +++ b/fs/overlayfs/overlayfs.h @@ -157,6 +157,10 @@ 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); +void ovl_clear_redirect_dir(struct super_block *sb); +const char *ovl_dentry_get_redirect(struct dentry *dentry); +void ovl_dentry_set_redirect(struct dentry *dentry, const char *redirect); 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 eb29882b6a54..d14bca1850d9 100644 --- a/fs/overlayfs/ovl_entry.h +++ b/fs/overlayfs/ovl_entry.h @@ -13,6 +13,7 @@ struct ovl_config { char *upperdir; char *workdir; bool default_permissions; + bool redirect_dir; }; /* private information held for overlayfs's superblock */ diff --git a/fs/overlayfs/super.c b/fs/overlayfs/super.c index 4e44e865b716..520f9ab0e9ef 100644 --- a/fs/overlayfs/super.c +++ b/fs/overlayfs/super.c @@ -226,6 +226,8 @@ enum { OPT_UPPERDIR, OPT_WORKDIR, OPT_DEFAULT_PERMISSIONS, + OPT_REDIRECT_DIR_ON, + OPT_REDIRECT_DIR_OFF, OPT_ERR, }; @@ -234,6 +236,8 @@ static const match_table_t ovl_tokens = { {OPT_UPPERDIR, "upperdir=%s"}, {OPT_WORKDIR, "workdir=%s"}, {OPT_DEFAULT_PERMISSIONS, "default_permissions"}, + {OPT_REDIRECT_DIR_ON, "redirect_dir=on"}, + {OPT_REDIRECT_DIR_OFF, "redirect_dir=off"}, {OPT_ERR, NULL} }; @@ -298,6 +302,14 @@ static int ovl_parse_opt(char *opt, struct ovl_config *config) config->default_permissions = true; break; + case OPT_REDIRECT_DIR_ON: + config->redirect_dir = true; + break; + + case OPT_REDIRECT_DIR_OFF: + config->redirect_dir = false; + break; + default: pr_err("overlayfs: unrecognized mount option \"%s\" or missing value\n", p); return -EINVAL; diff --git a/fs/overlayfs/util.c b/fs/overlayfs/util.c index 0d45a84468d2..260b215852a3 100644 --- a/fs/overlayfs/util.c +++ b/fs/overlayfs/util.c @@ -176,6 +176,35 @@ 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->config.redirect_dir; +} + +void ovl_clear_redirect_dir(struct super_block *sb) +{ + struct ovl_fs *ofs = sb->s_fs_info; + + ofs->config.redirect_dir = false; +} + +const char *ovl_dentry_get_redirect(struct dentry *dentry) +{ + struct ovl_entry *oe = dentry->d_fsdata; + + return oe->redirect; +} + +void ovl_dentry_set_redirect(struct dentry *dentry, const char *redirect) +{ + struct ovl_entry *oe = dentry->d_fsdata; + + kfree(oe->redirect); + oe->redirect = redirect; +} + void ovl_dentry_update(struct dentry *dentry, struct dentry *upperdentry) { struct ovl_entry *oe = dentry->d_fsdata; -- 2.20.1