diff --git a/fs/overlayfs/export.c b/fs/overlayfs/export.c index 361174810ce8..092e6e8c9258 100644 --- a/fs/overlayfs/export.c +++ b/fs/overlayfs/export.c @@ -294,6 +294,88 @@ fail: goto out; } +/* + * Lookup an indexed or hashed overlay dentry by real inode. + */ +static struct dentry *ovl_lookup_real_inode(struct super_block *sb, + struct dentry *real, + struct ovl_layer *layer) +{ + struct dentry *this = NULL; + struct inode *inode; + + inode = ovl_lookup_inode(sb, real, !layer->idx); + if (IS_ERR(inode)) + return ERR_CAST(inode); + if (inode) { + this = d_find_any_alias(inode); + iput(inode); + } + + /* TODO: use index when looking up by origin inode */ + if (!this) + return NULL; + + if (WARN_ON(ovl_dentry_real_at(this, layer->idx) != real)) { + dput(this); + this = ERR_PTR(-EIO); + } + + return this; +} + +/* + * Lookup an indexed or hashed overlay dentry, whose real dentry is an + * ancestor of @real. + */ +static struct dentry *ovl_lookup_real_ancestor(struct super_block *sb, + struct dentry *real, + struct ovl_layer *layer) +{ + struct dentry *next, *parent = NULL; + struct dentry *ancestor = ERR_PTR(-EIO); + + if (real == layer->mnt->mnt_root) + return dget(sb->s_root); + + /* Find the topmost indexed or hashed ancestor */ + next = dget(real); + for (;;) { + parent = dget_parent(next); + + /* + * Lookup a matching overlay dentry in inode/dentry + * cache or in index by real inode. + */ + ancestor = ovl_lookup_real_inode(sb, next, layer); + if (ancestor) + break; + + if (parent == layer->mnt->mnt_root) { + ancestor = dget(sb->s_root); + break; + } + + /* + * If @real has been moved out of the layer root directory, + * we will eventully hit the real fs root. This cannot happen + * by legit overlay rename, so we return error in that case. + */ + if (parent == next) { + ancestor = ERR_PTR(-EXDEV); + break; + } + + dput(next); + next = parent; + } + + dput(parent); + dput(next); + + return ancestor; +} + /* * Lookup a connected overlay dentry whose real dentry is @real. * If @real is on upper layer, we lookup a child overlay dentry with the same @@ -306,9 +388,10 @@ static struct dentry *ovl_lookup_real(struct super_block *sb, struct dentry *connected; int err = 0; - /* TODO: use index when looking up by lower real dentry */ + connected = ovl_lookup_real_ancestor(sb, real, layer); + if (IS_ERR(connected)) + return connected; - connected = dget(sb->s_root); while (!err) { struct dentry *next, *this; struct dentry *parent = NULL; @@ -365,11 +448,15 @@ static struct dentry *ovl_lookup_real(struct super_block *sb, * overlay rename of child away from 'connected' parent. * In this case, we need to restart the lookup from the * top, because we cannot trust that 'real_connected' is - * still an ancestor of 'real'. + * still an ancestor of 'real'. There is a good chance + * that the renamed overlay ancestor is now in cache, so + * ovl_lookup_real_ancestor() will find it and we can + * continue to connect exactly from where lookup failed. */ if (err == -ECHILD) { - this = dget(sb->s_root); - err = 0; + this = ovl_lookup_real_ancestor(sb, real, + layer); + err = IS_ERR(this) ? PTR_ERR(this) : 0; } if (!err) { dput(connected); @@ -494,7 +581,7 @@ static struct dentry *ovl_lower_fh_to_d(struct super_block *sb, } else if (is_deleted) { /* Lookup deleted non-dir by origin inode */ if (!d_is_dir(origin.dentry)) - inode = ovl_lookup_inode(sb, origin.dentry); + inode = ovl_lookup_inode(sb, origin.dentry, false); err = -ESTALE; if (!inode || atomic_read(&inode->i_count) == 1) goto out_err; diff --git a/fs/overlayfs/inode.c b/fs/overlayfs/inode.c index 416dc06835db..fcd97b783fa1 100644 --- a/fs/overlayfs/inode.c +++ b/fs/overlayfs/inode.c @@ -614,9 +614,15 @@ static int ovl_inode_set(struct inode *inode, void *data) } static bool ovl_verify_inode(struct inode *inode, struct dentry *lowerdentry, - struct dentry *upperdentry) + struct dentry *upperdentry, bool strict) { - if (S_ISDIR(inode->i_mode)) { + /* + * For directories, @strict verify from lookup path performs consistency + * checks, so NULL lower/upper in dentry must match NULL lower/upper in + * inode. Non @strict verify from NFS handle decode path passes NULL for + * 'unknown' lower/upper. + */ + if (S_ISDIR(inode->i_mode) && strict) { /* Real lower dir moved to upper layer under us? */ if (!lowerdentry && ovl_inode_lower(inode)) return false; @@ -645,15 +651,17 @@ static bool ovl_verify_inode(struct inode *inode, struct dentry *lowerdentry, return true; } -struct inode *ovl_lookup_inode(struct super_block *sb, struct dentry *origin) +struct inode *ovl_lookup_inode(struct super_block *sb, struct dentry *real, + bool is_upper) { - struct inode *inode, *key = d_inode(origin); + struct inode *inode, *key = d_inode(real); inode = ilookup5(sb, (unsigned long) key, ovl_inode_test, key); if (!inode) return NULL; - if (!ovl_verify_inode(inode, origin, NULL)) { + if (!ovl_verify_inode(inode, is_upper ? NULL : real, + is_upper ? real : NULL, false)) { iput(inode); return ERR_PTR(-ESTALE); } @@ -704,7 +712,8 @@ struct inode *ovl_get_inode(struct super_block *sb, struct dentry *upperdentry, * Verify that the underlying files stored in the inode * match those in the dentry. */ - if (!ovl_verify_inode(inode, lowerdentry, upperdentry)) { + if (!ovl_verify_inode(inode, lowerdentry, upperdentry, + true)) { iput(inode); inode = ERR_PTR(-ESTALE); goto out; diff --git a/fs/overlayfs/overlayfs.h b/fs/overlayfs/overlayfs.h index a5d415aec131..bf17bf97c50f 100644 --- a/fs/overlayfs/overlayfs.h +++ b/fs/overlayfs/overlayfs.h @@ -323,7 +323,8 @@ int ovl_update_time(struct inode *inode, struct timespec *ts, int flags); bool ovl_is_private_xattr(const char *name); struct inode *ovl_new_inode(struct super_block *sb, umode_t mode, dev_t rdev); -struct inode *ovl_lookup_inode(struct super_block *sb, struct dentry *origin); +struct inode *ovl_lookup_inode(struct super_block *sb, struct dentry *real, + bool is_upper); struct inode *ovl_get_inode(struct super_block *sb, struct dentry *upperdentry, struct dentry *lowerdentry, struct dentry *index, unsigned int numlower);