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->vfs_inode)->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->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 > 0). The child dentry is freed and the<br />
subvol dentry&#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&#39;t touch the bit..<br />
d_instantiate_new(dentry, inode) // dentry in cache with d_lockref.c<br />
---truncated---
Impact
References to Advisories, Solutions, and Tools
- https://git.kernel.org/stable/c/2ec578e6452138ab76f6c9a9c18711fcd197649f
- https://git.kernel.org/stable/c/5131fa077f9bb386a1b901bf5b247041f0ec8f80
- https://git.kernel.org/stable/c/696683f214495db3cdacab9a713efaaced8660f8
- https://git.kernel.org/stable/c/a41a9b8d19a98b45591528c6e54d31cc66271d1e
- https://git.kernel.org/stable/c/c57276ced3c3207f42182dfa2f0d8e860357e111
- https://git.kernel.org/stable/c/d43da8de0ed376abafbad8a245a1835e8f66cb0f



