forked from Minki/linux
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 <mszeredi@redhat.com>
This commit is contained in:
parent
02b69b284c
commit
a6c6065511
@ -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
|
||||
---------------------------------
|
||||
|
@ -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;
|
||||
|
@ -15,6 +15,7 @@
|
||||
#include <linux/posix_acl.h>
|
||||
#include <linux/posix_acl_xattr.h>
|
||||
#include <linux/atomic.h>
|
||||
#include <linux/ratelimit.h>
|
||||
#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,
|
||||
|
@ -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);
|
||||
|
@ -13,6 +13,7 @@ struct ovl_config {
|
||||
char *upperdir;
|
||||
char *workdir;
|
||||
bool default_permissions;
|
||||
bool redirect_dir;
|
||||
};
|
||||
|
||||
/* private information held for overlayfs's superblock */
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user