Merge tag 'vfs-6.19-rc8.fixes' of git://git.kernel.org/pub/scm/linux/kernel/git/vfs/vfs

Pull vfs fixes from Christian Brauner:

 - Fix the the buggy conversion of fuse_reverse_inval_entry() introduced
   during the creation rework

 - Disallow nfs delegation requests for directories by setting
   simple_nosetlease()

 - Require an opt-in for getting readdir flag bits outside of S_DT_MASK
   set in d_type

 - Fix scheduling delayed writeback work by only scheduling when the
   dirty time expiry interval is non-zero and cancel the delayed work if
   the interval is set to zero

 - Use rounded_jiffies_interval for dirty time work

 - Check the return value of sb_set_blocksize() for romfs

 - Wait for batched folios to be stable in __iomap_get_folio()

 - Use private naming for fuse hash size

 - Fix the stale dentry cleanup to prevent a race that causes a UAF

* tag 'vfs-6.19-rc8.fixes' of git://git.kernel.org/pub/scm/linux/kernel/git/vfs/vfs:
  vfs: document d_dispose_if_unused()
  fuse: shrink once after all buckets have been scanned
  fuse: clean up fuse_dentry_tree_work()
  fuse: add need_resched() before unlocking bucket
  fuse: make sure dentry is evicted if stale
  fuse: fix race when disposing stale dentries
  fuse: use private naming for fuse hash size
  writeback: use round_jiffies_relative for dirtytime_work
  iomap: wait for batched folios to be stable in __iomap_get_folio
  romfs: check sb_set_blocksize() return value
  docs: clarify that dirtytime_expire_seconds=0 disables writeback
  writeback: fix 100% CPU usage when dirtytime_expire_interval is 0
  readdir: require opt-in for d_type flags
  vboxsf: don't allow delegations to be set on directories
  ceph: don't allow delegations to be set on directories
  gfs2: don't allow delegations to be set on directories
  9p: don't allow delegations to be set on directories
  smb/client: properly disallow delegations on directories
  nfs: properly disallow delegation requests on directories
  fuse: fix conversion of fuse_reverse_inval_entry() to start_removing()
This commit is contained in:
Linus Torvalds
2026-01-26 09:30:48 -08:00
15 changed files with 82 additions and 40 deletions

View File

@@ -231,6 +231,8 @@ eventually gets pushed out to disk. This tunable is used to define when dirty
inode is old enough to be eligible for writeback by the kernel flusher threads.
And, it is also used as the interval to wakeup dirtytime_writeback thread.
Setting this to zero disables periodic dirtytime writeback.
dirty_writeback_centisecs
=========================

View File

@@ -242,6 +242,7 @@ const struct file_operations v9fs_dir_operations = {
.iterate_shared = v9fs_dir_readdir,
.open = v9fs_file_open,
.release = v9fs_dir_release,
.setlease = simple_nosetlease,
};
const struct file_operations v9fs_dir_operations_dotl = {
@@ -251,4 +252,5 @@ const struct file_operations v9fs_dir_operations_dotl = {
.open = v9fs_file_open,
.release = v9fs_dir_release,
.fsync = v9fs_file_fsync_dotl,
.setlease = simple_nosetlease,
};

View File

@@ -2214,6 +2214,7 @@ const struct file_operations ceph_dir_fops = {
.fsync = ceph_fsync,
.lock = ceph_lock,
.flock = ceph_flock,
.setlease = simple_nosetlease,
};
const struct file_operations ceph_snapdir_fops = {
@@ -2221,6 +2222,7 @@ const struct file_operations ceph_snapdir_fops = {
.llseek = ceph_dir_llseek,
.open = ceph_open,
.release = ceph_release,
.setlease = simple_nosetlease,
};
const struct inode_operations ceph_dir_iops = {

View File

@@ -1104,6 +1104,16 @@ struct dentry *d_find_alias_rcu(struct inode *inode)
return de;
}
/**
* d_dispose_if_unused - move unreferenced dentries to shrink list
* @dentry: dentry in question
* @dispose: head of shrink list
*
* If dentry has no external references, move it to shrink list.
*
* NOTE!!! The caller is responsible for preventing eviction of the dentry by
* holding dentry->d_inode->i_lock or equivalent.
*/
void d_dispose_if_unused(struct dentry *dentry, struct list_head *dispose)
{
spin_lock(&dentry->d_lock);

View File

@@ -2492,7 +2492,9 @@ static void wakeup_dirtytime_writeback(struct work_struct *w)
wb_wakeup(wb);
}
rcu_read_unlock();
schedule_delayed_work(&dirtytime_work, dirtytime_expire_interval * HZ);
if (dirtytime_expire_interval)
schedule_delayed_work(&dirtytime_work,
round_jiffies_relative(dirtytime_expire_interval * HZ));
}
static int dirtytime_interval_handler(const struct ctl_table *table, int write,
@@ -2501,8 +2503,12 @@ static int dirtytime_interval_handler(const struct ctl_table *table, int write,
int ret;
ret = proc_dointvec_minmax(table, write, buffer, lenp, ppos);
if (ret == 0 && write)
mod_delayed_work(system_percpu_wq, &dirtytime_work, 0);
if (ret == 0 && write) {
if (dirtytime_expire_interval)
mod_delayed_work(system_percpu_wq, &dirtytime_work, 0);
else
cancel_delayed_work_sync(&dirtytime_work);
}
return ret;
}
@@ -2519,7 +2525,9 @@ static const struct ctl_table vm_fs_writeback_table[] = {
static int __init start_dirtytime_writeback(void)
{
schedule_delayed_work(&dirtytime_work, dirtytime_expire_interval * HZ);
if (dirtytime_expire_interval)
schedule_delayed_work(&dirtytime_work,
round_jiffies_relative(dirtytime_expire_interval * HZ));
register_sysctl_init("vm", vm_fs_writeback_table);
return 0;
}

View File

@@ -32,9 +32,9 @@ struct dentry_bucket {
spinlock_t lock;
};
#define HASH_BITS 5
#define HASH_SIZE (1 << HASH_BITS)
static struct dentry_bucket dentry_hash[HASH_SIZE];
#define FUSE_HASH_BITS 5
#define FUSE_HASH_SIZE (1 << FUSE_HASH_BITS)
static struct dentry_bucket dentry_hash[FUSE_HASH_SIZE];
struct delayed_work dentry_tree_work;
/* Minimum invalidation work queue frequency */
@@ -83,7 +83,7 @@ MODULE_PARM_DESC(inval_wq,
static inline struct dentry_bucket *get_dentry_bucket(struct dentry *dentry)
{
int i = hash_ptr(dentry, HASH_BITS);
int i = hash_ptr(dentry, FUSE_HASH_BITS);
return &dentry_hash[i];
}
@@ -164,25 +164,31 @@ static void fuse_dentry_tree_work(struct work_struct *work)
struct rb_node *node;
int i;
for (i = 0; i < HASH_SIZE; i++) {
for (i = 0; i < FUSE_HASH_SIZE; i++) {
spin_lock(&dentry_hash[i].lock);
node = rb_first(&dentry_hash[i].tree);
while (node) {
fd = rb_entry(node, struct fuse_dentry, node);
if (time_after64(get_jiffies_64(), fd->time)) {
rb_erase(&fd->node, &dentry_hash[i].tree);
RB_CLEAR_NODE(&fd->node);
if (!time_before64(fd->time, get_jiffies_64()))
break;
rb_erase(&fd->node, &dentry_hash[i].tree);
RB_CLEAR_NODE(&fd->node);
spin_lock(&fd->dentry->d_lock);
/* If dentry is still referenced, let next dput release it */
fd->dentry->d_flags |= DCACHE_OP_DELETE;
spin_unlock(&fd->dentry->d_lock);
d_dispose_if_unused(fd->dentry, &dispose);
if (need_resched()) {
spin_unlock(&dentry_hash[i].lock);
d_dispose_if_unused(fd->dentry, &dispose);
cond_resched();
spin_lock(&dentry_hash[i].lock);
} else
break;
}
node = rb_first(&dentry_hash[i].tree);
}
spin_unlock(&dentry_hash[i].lock);
shrink_dentry_list(&dispose);
}
shrink_dentry_list(&dispose);
if (inval_wq)
schedule_delayed_work(&dentry_tree_work,
@@ -213,7 +219,7 @@ void fuse_dentry_tree_init(void)
{
int i;
for (i = 0; i < HASH_SIZE; i++) {
for (i = 0; i < FUSE_HASH_SIZE; i++) {
spin_lock_init(&dentry_hash[i].lock);
dentry_hash[i].tree = RB_ROOT;
}
@@ -227,7 +233,7 @@ void fuse_dentry_tree_cleanup(void)
inval_wq = 0;
cancel_delayed_work_sync(&dentry_tree_work);
for (i = 0; i < HASH_SIZE; i++)
for (i = 0; i < FUSE_HASH_SIZE; i++)
WARN_ON_ONCE(!RB_EMPTY_ROOT(&dentry_hash[i].tree));
}
@@ -479,18 +485,12 @@ static int fuse_dentry_init(struct dentry *dentry)
return 0;
}
static void fuse_dentry_prune(struct dentry *dentry)
static void fuse_dentry_release(struct dentry *dentry)
{
struct fuse_dentry *fd = dentry->d_fsdata;
if (!RB_EMPTY_NODE(&fd->node))
fuse_dentry_tree_del_node(dentry);
}
static void fuse_dentry_release(struct dentry *dentry)
{
struct fuse_dentry *fd = dentry->d_fsdata;
kfree_rcu(fd, rcu);
}
@@ -527,7 +527,6 @@ const struct dentry_operations fuse_dentry_operations = {
.d_revalidate = fuse_dentry_revalidate,
.d_delete = fuse_dentry_delete,
.d_init = fuse_dentry_init,
.d_prune = fuse_dentry_prune,
.d_release = fuse_dentry_release,
.d_automount = fuse_dentry_automount,
};
@@ -1584,8 +1583,8 @@ int fuse_reverse_inval_entry(struct fuse_conn *fc, u64 parent_nodeid,
{
int err = -ENOTDIR;
struct inode *parent;
struct dentry *dir;
struct dentry *entry;
struct dentry *dir = NULL;
struct dentry *entry = NULL;
parent = fuse_ilookup(fc, parent_nodeid, NULL);
if (!parent)
@@ -1598,11 +1597,19 @@ int fuse_reverse_inval_entry(struct fuse_conn *fc, u64 parent_nodeid,
dir = d_find_alias(parent);
if (!dir)
goto put_parent;
entry = start_removing_noperm(dir, name);
dput(dir);
if (IS_ERR(entry))
goto put_parent;
while (!entry) {
struct dentry *child = try_lookup_noperm(name, dir);
if (!child || IS_ERR(child))
goto put_parent;
entry = start_removing_dentry(dir, child);
dput(child);
if (IS_ERR(entry))
goto put_parent;
if (!d_same_name(entry, dir, name)) {
end_removing(entry);
entry = NULL;
}
}
fuse_dir_changed(parent);
if (!(flags & FUSE_EXPIRE_ONLY))
@@ -1640,6 +1647,7 @@ int fuse_reverse_inval_entry(struct fuse_conn *fc, u64 parent_nodeid,
end_removing(entry);
put_parent:
dput(dir);
iput(parent);
return err;
}

View File

@@ -1608,6 +1608,7 @@ const struct file_operations gfs2_dir_fops = {
.lock = gfs2_lock,
.flock = gfs2_flock,
.llseek = default_llseek,
.setlease = simple_nosetlease,
.fop_flags = FOP_ASYNC_LOCK,
};

View File

@@ -851,6 +851,7 @@ static struct folio *__iomap_get_folio(struct iomap_iter *iter,
}
folio_get(folio);
folio_wait_stable(folio);
return folio;
}

View File

@@ -66,6 +66,7 @@ const struct file_operations nfs_dir_operations = {
.open = nfs_opendir,
.release = nfs_closedir,
.fsync = nfs_fsync_dir,
.setlease = simple_nosetlease,
};
const struct address_space_operations nfs_dir_aops = {

View File

@@ -431,8 +431,6 @@ void nfs42_ssc_unregister_ops(void)
static int nfs4_setlease(struct file *file, int arg, struct file_lease **lease,
void **priv)
{
if (!S_ISREG(file_inode(file)->i_mode))
return -EINVAL;
return nfs4_proc_setlease(file, arg, lease, priv);
}

View File

@@ -316,6 +316,7 @@ SYSCALL_DEFINE3(getdents, unsigned int, fd,
struct getdents_callback buf = {
.ctx.actor = filldir,
.ctx.count = count,
.ctx.dt_flags_mask = FILLDIR_FLAG_NOINTR,
.current_dir = dirent
};
int error;
@@ -400,6 +401,7 @@ SYSCALL_DEFINE3(getdents64, unsigned int, fd,
struct getdents_callback64 buf = {
.ctx.actor = filldir64,
.ctx.count = count,
.ctx.dt_flags_mask = FILLDIR_FLAG_NOINTR,
.current_dir = dirent
};
int error;
@@ -569,6 +571,7 @@ COMPAT_SYSCALL_DEFINE3(getdents, unsigned int, fd,
struct compat_getdents_callback buf = {
.ctx.actor = compat_filldir,
.ctx.count = count,
.ctx.dt_flags_mask = FILLDIR_FLAG_NOINTR,
.current_dir = dirent,
};
int error;

View File

@@ -458,7 +458,10 @@ static int romfs_fill_super(struct super_block *sb, struct fs_context *fc)
#ifdef CONFIG_BLOCK
if (!sb->s_mtd) {
sb_set_blocksize(sb, ROMBSIZE);
if (!sb_set_blocksize(sb, ROMBSIZE)) {
errorf(fc, "romfs: unable to set blocksize\n");
return -EINVAL;
}
} else {
sb->s_blocksize = ROMBSIZE;
sb->s_blocksize_bits = blksize_bits(ROMBSIZE);

View File

@@ -1149,9 +1149,6 @@ cifs_setlease(struct file *file, int arg, struct file_lease **lease, void **priv
struct inode *inode = file_inode(file);
struct cifsFileInfo *cfile = file->private_data;
if (!S_ISREG(inode->i_mode))
return -EINVAL;
/* Check if file is oplocked if this is request for new lease */
if (arg == F_UNLCK ||
((arg == F_RDLCK) && CIFS_CACHE_READ(CIFS_I(inode))) ||
@@ -1712,6 +1709,7 @@ const struct file_operations cifs_dir_ops = {
.remap_file_range = cifs_remap_file_range,
.llseek = generic_file_llseek,
.fsync = cifs_dir_fsync,
.setlease = simple_nosetlease,
};
static void

View File

@@ -186,6 +186,7 @@ const struct file_operations vboxsf_dir_fops = {
.release = vboxsf_dir_release,
.read = generic_read_dir,
.llseek = generic_file_llseek,
.setlease = simple_nosetlease,
};
/*

View File

@@ -1855,6 +1855,8 @@ struct dir_context {
* INT_MAX unlimited
*/
int count;
/* @actor supports these flags in d_type high bits */
unsigned int dt_flags_mask;
};
/* If OR-ed with d_type, pending signals are not checked */
@@ -3524,7 +3526,9 @@ static inline bool dir_emit(struct dir_context *ctx,
const char *name, int namelen,
u64 ino, unsigned type)
{
return ctx->actor(ctx, name, namelen, ctx->pos, ino, type);
unsigned int dt_mask = S_DT_MASK | ctx->dt_flags_mask;
return ctx->actor(ctx, name, namelen, ctx->pos, ino, type & dt_mask);
}
static inline bool dir_emit_dot(struct file *file, struct dir_context *ctx)
{