Skip to content

Commit

Permalink
ovl: redirect on rename-dir
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
Miklos Szeredi committed Dec 16, 2016
1 parent 02b69b2 commit a6c6065
Show file tree
Hide file tree
Showing 7 changed files with 195 additions and 30 deletions.
21 changes: 19 additions & 2 deletions Documentation/filesystems/overlayfs.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
---------------
Expand Down Expand Up @@ -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
---------------------------------
Expand Down
20 changes: 6 additions & 14 deletions fs/overlayfs/copy_up.c
Original file line number Diff line number Diff line change
Expand Up @@ -324,17 +324,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)
Expand All @@ -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))
Expand All @@ -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;
Expand Down
138 changes: 124 additions & 14 deletions fs/overlayfs/dir.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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;

Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions fs/overlayfs/overlayfs.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions fs/overlayfs/ovl_entry.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ struct ovl_config {
char *upperdir;
char *workdir;
bool default_permissions;
bool redirect_dir;
};

/* private information held for overlayfs's superblock */
Expand Down
12 changes: 12 additions & 0 deletions fs/overlayfs/super.c
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,8 @@ enum {
OPT_UPPERDIR,
OPT_WORKDIR,
OPT_DEFAULT_PERMISSIONS,
OPT_REDIRECT_DIR_ON,
OPT_REDIRECT_DIR_OFF,
OPT_ERR,
};

Expand All @@ -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}
};

Expand Down Expand Up @@ -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;
Expand Down
29 changes: 29 additions & 0 deletions fs/overlayfs/util.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit a6c6065

Please sign in to comment.