CVE-2026-31519

Severity CVSS v4.0:
Pending analysis
Type:
Unavailable / Other
Publication date:
22/04/2026
Last modified:
22/04/2026

Description

In the Linux kernel, the following vulnerability has been resolved:<br /> <br /> btrfs: set BTRFS_ROOT_ORPHAN_CLEANUP during subvol create<br /> <br /> We have recently observed a number of subvolumes with broken dentries.<br /> ls-ing the parent dir looks like:<br /> <br /> drwxrwxrwt 1 root root 16 Jan 23 16:49 .<br /> drwxr-xr-x 1 root root 24 Jan 23 16:48 ..<br /> d????????? ? ? ? ? ? broken_subvol<br /> <br /> and similarly stat-ing the file fails.<br /> <br /> In this state, deleting the subvol fails with ENOENT, but attempting to<br /> create a new file or subvol over it errors out with EEXIST and even<br /> aborts the fs. Which leaves us a bit stuck.<br /> <br /> dmesg contains a single notable error message reading:<br /> "could not do orphan cleanup -2"<br /> <br /> 2 is ENOENT and the error comes from the failure handling path of<br /> btrfs_orphan_cleanup(), with the stack leading back up to<br /> btrfs_lookup().<br /> <br /> btrfs_lookup<br /> btrfs_lookup_dentry<br /> btrfs_orphan_cleanup // prints that message and returns -ENOENT<br /> <br /> After some detailed inspection of the internal state, it became clear<br /> that:<br /> - there are no orphan items for the subvol<br /> - the subvol is otherwise healthy looking, it is not half-deleted or<br /> anything, there is no drop progress, etc.<br /> - the subvol was created a while ago and does the meaningful first<br /> btrfs_orphan_cleanup() call that sets BTRFS_ROOT_ORPHAN_CLEANUP much<br /> later.<br /> - after btrfs_orphan_cleanup() fails, btrfs_lookup_dentry() returns -ENOENT,<br /> which results in a negative dentry for the subvolume via<br /> d_splice_alias(NULL, dentry), leading to the observed behavior. The<br /> bug can be mitigated by dropping the dentry cache, at which point we<br /> can successfully delete the subvolume if we want.<br /> <br /> i.e.,<br /> btrfs_lookup()<br /> btrfs_lookup_dentry()<br /> if (!sb_rdonly(inode-&gt;vfs_inode)-&gt;vfs_inode)<br /> btrfs_orphan_cleanup(sub_root)<br /> test_and_set_bit(BTRFS_ROOT_ORPHAN_CLEANUP)<br /> btrfs_search_slot() // finds orphan item for inode N<br /> ...<br /> prints "could not do orphan cleanup -2"<br /> if (inode == ERR_PTR(-ENOENT))<br /> inode = NULL;<br /> return d_splice_alias(NULL, dentry) // NEGATIVE DENTRY for valid subvolume<br /> <br /> btrfs_orphan_cleanup() does test_and_set_bit(BTRFS_ROOT_ORPHAN_CLEANUP)<br /> on the root when it runs, so it cannot run more than once on a given<br /> root, so something else must run concurrently. However, the obvious<br /> routes to deleting an orphan when nlinks goes to 0 should not be able to<br /> run without first doing a lookup into the subvolume, which should run<br /> btrfs_orphan_cleanup() and set the bit.<br /> <br /> The final important observation is that create_subvol() calls<br /> d_instantiate_new() but does not set BTRFS_ROOT_ORPHAN_CLEANUP, so if<br /> the dentry cache gets dropped, the next lookup into the subvolume will<br /> make a real call into btrfs_orphan_cleanup() for the first time. This<br /> opens up the possibility of concurrently deleting the inode/orphan items<br /> but most typical evict() paths will be holding a reference on the parent<br /> dentry (child dentry holds parent-&gt;d_lockref.count via dget in<br /> d_alloc(), released in __dentry_kill()) and prevent the parent from<br /> being removed from the dentry cache.<br /> <br /> The one exception is delayed iputs. Ordered extent creation calls<br /> igrab() on the inode. If the file is unlinked and closed while those<br /> refs are held, iput() in __dentry_kill() decrements i_count but does<br /> not trigger eviction (i_count &gt; 0). The child dentry is freed and the<br /> subvol dentry&amp;#39;s d_lockref.count drops to 0, making it evictable while<br /> the inode is still alive.<br /> <br /> Since there are two races (the race between writeback and unlink and<br /> the race between lookup and delayed iputs), and there are too many moving<br /> parts, the following three diagrams show the complete picture.<br /> (Only the second and third are races)<br /> <br /> Phase 1:<br /> Create Subvol in dentry cache without BTRFS_ROOT_ORPHAN_CLEANUP set<br /> <br /> btrfs_mksubvol()<br /> lookup_one_len()<br /> __lookup_slow()<br /> d_alloc_parallel()<br /> __d_alloc() // d_lockref.count = 1<br /> create_subvol(dentry)<br /> // doesn&amp;#39;t touch the bit..<br /> d_instantiate_new(dentry, inode) // dentry in cache with d_lockref.c<br /> ---truncated---

Impact