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:
Miklos Szeredi
2016-12-16 11:02:56 +01:00
parent 02b69b284c
commit a6c6065511
7 changed files with 195 additions and 30 deletions

View File

@@ -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,