diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c index 9d20f8fa5c3f92..a58e586754ef58 100644 --- a/fs/fuse/dir.c +++ b/fs/fuse/dir.c @@ -139,6 +139,26 @@ static void fuse_dir_changed(struct inode *dir) inode_maybe_inc_iversion(dir, false); } +/* + * Note in the parent directory that one of its entries was invalidated. + * + * This bumps the same change counter as fuse_dir_changed(), which doubles as a + * generation for the parent's entry cache: fuse_dentry_revalidate() samples it + * before sending its LOOKUP and refuses to re-arm the entry timeout if it moved + * meanwhile. + * + * Only for invalidations coming from the server - a plain + * fuse_invalidate_entry_cache() (a negative lookup, say) must not bump the + * counter, or it would needlessly drop the readdir cache as well. + */ +void fuse_entry_invalidated(struct dentry *entry) +{ + /* d_lock keeps d_parent stable; a live child pins its parent inode */ + spin_lock(&entry->d_lock); + inode_maybe_inc_iversion(d_inode(entry->d_parent), false); + spin_unlock(&entry->d_lock); +} + /* * Mark the attributes as stale due to an atime change. Avoid the invalidate if * atime is not used. @@ -276,7 +296,7 @@ static int fuse_dentry_revalidate(struct dentry *entry, unsigned int flags) struct fuse_lookupx_out ext_out; struct fuse_statx sx; struct fuse_forget_link *forget; - u64 attr_version; + u64 attr_version, dir_version; uint32_t lookupx_flags = FUSE_LOOKUPX_FOR_REVALIDATE; /* For negative dentries, always do a fresh lookup */ @@ -299,6 +319,19 @@ static int fuse_dentry_revalidate(struct dentry *entry, unsigned int flags) lookupx_flags |= FUSE_LOOKUPX_TARGET_WAS_DIR; parent = dget_parent(entry); + /* + * Unlike ->lookup() and the create paths, revalidate runs + * without the parent's i_rwsem, so it does not exclude + * fuse_reverse_inval_entry(). An invalidation for this name + * can therefore land while the LOOKUP is in flight, and + * re-arming the entry timeout from the reply below would + * silently undo it - permanently so for FUSE_EXPIRE_ONLY and + * for fuse_prune_aliases(), which leave the dentry hashed. + * + * Sample the parent's entry-cache generation here and compare + * it once the reply is in. + */ + dir_version = inode_query_iversion(d_inode(parent)); ret = fuse_do_lookupx(fm, get_node_id(d_inode(parent)), &entry->d_name, &ext_out, lookupx_flags); @@ -330,7 +363,11 @@ static int fuse_dentry_revalidate(struct dentry *entry, unsigned int flags) fuse_change_attributes(inode, &ext_out.entry.attr, &sx, ATTR_TIMEOUT(&ext_out.entry), attr_version); - fuse_change_entry_timeout(entry, &ext_out.entry); + + parent = dget_parent(entry); + if (inode_peek_iversion(d_inode(parent)) == dir_version) + fuse_change_entry_timeout(entry, &ext_out.entry); + dput(parent); } else if (inode) { fi = get_fuse_inode(inode); if (flags & LOOKUP_RCU) { @@ -701,6 +738,7 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir, struct fuse_entry_out outentry; struct fuse_inode *fi; struct fuse_file *ff; + u64 evict_ctr; bool trunc = flags & O_TRUNC; /* Userspace expects S_IFREG in create mode */ @@ -750,6 +788,8 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir, if (err) goto out_free_ff; + evict_ctr = fuse_get_evict_ctr(fm->fc); + err = fuse_simple_idmap_request(idmap, fm, &args); free_ext_value(&args); if (err) @@ -764,7 +804,7 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir, ff->nodeid = outentry.nodeid; ff->open_flags = outopenp->open_flags; inode = fuse_iget(dir->i_sb, outentry.nodeid, outentry.generation, - &outentry.attr, ATTR_TIMEOUT(&outentry), 0, 0); + &outentry.attr, ATTR_TIMEOUT(&outentry), 0, evict_ctr); if (!inode) { flags &= ~(O_CREAT | O_EXCL | O_TRUNC); fuse_sync_release(NULL, ff, flags); @@ -862,6 +902,7 @@ static int create_new_entry(struct mnt_idmap *idmap, struct fuse_mount *fm, struct dentry *d; int err; struct fuse_forget_link *forget; + u64 evict_ctr; if (fuse_is_bad(dir)) return -EIO; @@ -882,6 +923,8 @@ static int create_new_entry(struct mnt_idmap *idmap, struct fuse_mount *fm, goto out_put_forget_req; } + evict_ctr = fuse_get_evict_ctr(fm->fc); + err = fuse_simple_idmap_request(idmap, fm, args); free_ext_value(args); if (err) @@ -895,7 +938,7 @@ static int create_new_entry(struct mnt_idmap *idmap, struct fuse_mount *fm, goto out_put_forget_req; inode = fuse_iget(dir->i_sb, outarg.nodeid, outarg.generation, - &outarg.attr, ATTR_TIMEOUT(&outarg), 0, 0); + &outarg.attr, ATTR_TIMEOUT(&outarg), 0, evict_ctr); if (!inode) { fuse_queue_forget(fm->fc, forget, outarg.nodeid, 1); return -ENOMEM; diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h index 99eda51ba1bc0e..0b942a18591e48 100644 --- a/fs/fuse/fuse_i.h +++ b/fs/fuse/fuse_i.h @@ -1048,9 +1048,6 @@ struct fuse_conn { /** Version counter for attribute changes */ atomic64_t attr_version; - /** Waitqueue for attr_version initialization */ - wait_queue_head_t attr_version_waitq; - /** Version counter for evict inode */ atomic64_t evict_ctr; @@ -1432,6 +1429,8 @@ void fuse_invalidate_attr_mask(struct inode *inode, u32 mask); void fuse_invalidate_entry_cache(struct dentry *entry); +void fuse_entry_invalidated(struct dentry *entry); + void fuse_invalidate_atime(struct inode *inode); u64 fuse_time_to_jiffies(u64 sec, u32 nsec); diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c index 54bc38c29d0ff0..ed5042781e367b 100644 --- a/fs/fuse/inode.c +++ b/fs/fuse/inode.c @@ -233,6 +233,26 @@ static ino_t fuse_squash_ino(u64 ino64) return ino; } +/* + * May attributes returned by a request be marked valid, i.e. cleared + * from inval_mask? + * + * Not if they come from a fuse_iget() call whose request raced with an + * evict or a reverse invalidation - either would have invalidated the + * result if the inode's attr_version would've been preserved. + * + * !evict_ctr -> this is not fuse_iget() + * fi->attr_version != 0 -> this is not a new inode + * evict_ctr == fuse_get_evict_ctr() -> no evicts or missed + * invalidations during the request + */ +static bool fuse_may_validate_attrs(struct fuse_conn *fc, + struct fuse_inode *fi, u64 evict_ctr) +{ + return !evict_ctr || fi->attr_version || + evict_ctr == fuse_get_evict_ctr(fc); +} + /* * Handle statx-specific attribute updates with partial attribute support. */ @@ -248,18 +268,8 @@ static void fuse_change_attributes_common_sx(struct inode *inode, lockdep_assert_held(&fi->lock); - /* - * Clear returned basic stats from invalid mask. - * - * Don't do this if this is coming from a fuse_iget() call and there - * might have been a racing evict which would've invalidated the result - * if the attr_version would've been preserved. - * - * !evict_ctr -> this is create - * fi->attr_version != 0 -> this is not a new inode - * evict_ctr == fuse_get_evict_ctr() -> no evicts while during request - */ - if (!evict_ctr || fi->attr_version || evict_ctr == fuse_get_evict_ctr(fc)) + /* Clear returned basic stats from invalid mask */ + if (fuse_may_validate_attrs(fc, fi, evict_ctr)) set_mask_bits(&fi->inval_mask, returned_attrs, 0); fi->attr_version = atomic64_inc_return(&fc->attr_version); @@ -386,22 +396,11 @@ void fuse_change_attributes_common(struct inode *inode, struct fuse_attr *attr, evict_ctr); } - /* - * Clear basic stats from invalid mask. - * - * Don't do this if this is coming from a fuse_iget() call and there - * might have been a racing evict which would've invalidated the result - * if the attr_version would've been preserved. - * - * !evict_ctr -> this is create - * fi->attr_version != 0 -> this is not a new inode - * evict_ctr == fuse_get_evict_ctr() -> no evicts while during request - */ - if (!evict_ctr || fi->attr_version || evict_ctr == fuse_get_evict_ctr(fc)) + /* Clear basic stats from invalid mask */ + if (fuse_may_validate_attrs(fc, fi, evict_ctr)) set_mask_bits(&fi->inval_mask, STATX_BASIC_STATS, 0); fi->attr_version = atomic64_inc_return(&fc->attr_version); - wake_up_all(&fc->attr_version_waitq); fi->i_time = attr_valid; fi->i_perm_time = attr_valid; @@ -852,6 +851,7 @@ static void fuse_prune_aliases(struct inode *inode) spin_lock(&inode->i_lock); hlist_for_each_entry(dentry, &inode->i_dentry, d_u.d_alias) { + fuse_entry_invalidated(dentry); fuse_invalidate_entry_cache(dentry); } spin_unlock(&inode->i_lock); @@ -868,6 +868,7 @@ static void fuse_invalidate_inode_entry(struct inode *inode) dentry = d_find_alias(inode); if (dentry) { d_invalidate(dentry); + fuse_entry_invalidated(dentry); fuse_invalidate_entry_cache(dentry); dput(dentry); } @@ -879,6 +880,7 @@ static void fuse_invalidate_inode_entry(struct inode *inode) if (!d_unhashed(dentry)) __d_drop(dentry); spin_unlock(&dentry->d_lock); + fuse_entry_invalidated(dentry); fuse_invalidate_entry_cache(dentry); } spin_unlock(&inode->i_lock); @@ -945,18 +947,26 @@ int fuse_reverse_inval_inode(struct fuse_conn *fc, u64 nodeid, uint64_t lock_end; inode = fuse_ilookup(fc, nodeid, NULL); - if (!inode) - return -ENOENT; + if (!inode) { + /* + * Not in the icache, but a LOOKUP/READDIRPLUS reply carrying + * pre-invalidation attributes may be about to create it. Bump + * evict_ctr so that such a reply leaves the new inode's + * attributes invalid (see fuse_change_attributes_common()), + * then look up again in case the inode was hashed while we + * were bumping. ilookup5() and iget5_locked() serialize on + * the inode hash lock, so if the second lookup still misses, + * the creator inserted after it and is guaranteed to observe + * the bumped counter. + */ + atomic64_inc(&fc->evict_ctr); + inode = fuse_ilookup(fc, nodeid, NULL); + if (!inode) + return -ENOENT; + } fi = get_fuse_inode(inode); - spin_lock(&fi->lock); - while (fi->attr_version == 0) { - spin_unlock(&fi->lock); - wait_event(fc->attr_version_waitq, READ_ONCE(fi->attr_version) != 0); - spin_lock(&fi->lock); - } - fi->attr_version = atomic64_inc_return(&fc->attr_version); spin_unlock(&fi->lock); @@ -1457,7 +1467,6 @@ void fuse_conn_init(struct fuse_conn *fc, struct fuse_mount *fm, refcount_set(&fc->count, 1); atomic_set(&fc->dev_count, 1); init_waitqueue_head(&fc->blocked_waitq); - init_waitqueue_head(&fc->attr_version_waitq); fuse_iqueue_init(&fc->iq, fiq_ops, fiq_priv); INIT_LIST_HEAD(&fc->bg_queue); INIT_LIST_HEAD(&fc->entry);