August 2, 2026
CVE-2026–64600 “RefluXFS” — Rooting Linux Through an XFS Race Condition (Explained in Code)
With nothing more than a regular user account, an attacker can exploit a single timing bug in the Linux kernel’s XFS filesystem to tamper…
By Guidancewhite
5 min read
With nothing more than a regular user account, an attacker can exploit a single timing bug in the Linux kernel's XFS filesystem to tamper with /etc/passwd or a SUID-root binary and escalate to root. The bug has existed since kernel 4.11 (2017) and was disclosed by Qualys TRU on July 22, 2026.
| Item | Detail |
| — -| — -|
| CVE ID | CVE-2026–64600 |
| Nickname | RefluXFS |
| Type | Local Privilege Escalation (LPE) |
| Root cause | Race condition in the XFS Copy-on-Write path |
| Affected scope | Any distro running XFS with reflink=1 (default mkfs.xfs setting since 2019 on many enterprise distros: RHEL, Oracle Linux, Amazon Linux, Fedora, and others) |
| Exposure window | Kernel 4.11 (2017) through the patch |
| Notable trait | Leaves no kernel log entry, and the tampered file survives a reboot |
— -
Why This One Needs Code to Understand
This isn't a classic memory-corruption bug like a buffer overflow. It's a timing gap between two pieces of otherwise-correct kernel logic running concurrently. Without walking through the logic step by step, "so what's actually broken here?" doesn't land. So let's start with one concept.
reflink is an XFS feature that lets a file be "cloned" without actually copying its data (
cp — reflink=always) — both files simply point at the same disk blocks. Only when one of the files is later written to does the kernel perform an actual copy (Copy-on-Write, CoW) to separate the two.
The bug lives inside exactly that "separate the blocks when a write happens" logic.
— -
The Correct CoW Write Path (Pseudocode)
Assuming no bug, here's a simplified version of how XFS handles a write to a reflinked file.
// Simplified pseudocode for illustration only.
// Does not match real kernel source function names/implementation 1:1.
write(file, data) {
lock(file.inode); // block other processes from touching it
if (block.refcount > 1) {
// this block is shared with another file -> can't write directly
new_block = allocate_private_block();
copy(block, new_block); // actual CoW copy happens here
remap(file, new_block); // point the file at the new block
block.refcount — ; // drop the original block's ref count
target = new_block;
} else {
// this block is now exclusively mine -> safe to write directly
target = block;
}
write_to_disk(target, data);
unlock(file.inode);
}// Simplified pseudocode for illustration only.
// Does not match real kernel source function names/implementation 1:1.
write(file, data) {
lock(file.inode); // block other processes from touching it
if (block.refcount > 1) {
// this block is shared with another file -> can't write directly
new_block = allocate_private_block();
copy(block, new_block); // actual CoW copy happens here
remap(file, new_block); // point the file at the new block
block.refcount — ; // drop the original block's ref count
target = new_block;
} else {
// this block is now exclusively mine -> safe to write directly
target = block;
}
write_to_disk(target, data);
unlock(file.inode);
}Nothing wrong here — as long as the lock stays held from start to finish.
— -
What the Kernel Actually Does: It Drops the Lock Midway
The problem shows up when transaction log space runs low. XFS is a journaling filesystem, so every metadata change must be logged first. If log space is tight, the write has to wait — and to avoid a deadlock while waiting, the kernel briefly releases the inode lock.
write(file, data) {
lock(file.inode);
if (block.refcount > 1) {
// trouble starts here, when log space is low
if (log_space_low()) {
unlock(file.inode); // ← (1) lock dropped to avoid deadlock
wait_for_log_space(); // anything can happen during this wait
lock(file.inode); // ← (2) lock re-acquired on return
}
// problem: what if refcount was already read before the lock was dropped?
if (block.refcount > 1) { // ← (3) this value may no longer be current
…
} else {
target = block; // decides to write directly to the ORIGINAL block!
}
}
write_to_disk(target, data); // ← by now, acting on a stale judgment
unlock(file.inode);
}write(file, data) {
lock(file.inode);
if (block.refcount > 1) {
// trouble starts here, when log space is low
if (log_space_low()) {
unlock(file.inode); // ← (1) lock dropped to avoid deadlock
wait_for_log_space(); // anything can happen during this wait
lock(file.inode); // ← (2) lock re-acquired on return
}
// problem: what if refcount was already read before the lock was dropped?
if (block.refcount > 1) { // ← (3) this value may no longer be current
…
} else {
target = block; // decides to write directly to the ORIGINAL block!
}
}
write_to_disk(target, data); // ← by now, acting on a stale judgment
unlock(file.inode);
}That's half the bug: a window exists where the lock is released.
— -
How an Attacker Exploits That Window
Now a second thread (the attacker, Thread B) pushes its own write to the same reflinked file during that exact window.
// Thread A (the victim process, or a system process): running write() above
// Thread B (attacker, unprivileged user): writing to the same file concurrently
// Timeline:
// t0: Thread A takes the lock and enters write()
// t1: Thread A releases the lock while waiting on log space (step (1) above)
// t2: In that gap, Thread B takes the lock and completes its OWN CoW
Thread_B: {
lock(file.inode);
new_block = allocate_private_block();
copy(block, new_block);
remap(file, new_block);
block.refcount — ; // refcount drops from 2 to 1!
unlock(file.inode);
}
// t3: Thread A re-acquires the lock and resumes (step (2) above)
// But Thread A already judged at t0 that "this block has refcount > 1" —
// or, if it re-reads refcount after t2, it now sees refcount == 1 and
// mistakenly concludes "this must be my own private block now"
// — when in reality, Thread B just separated it
// t4: Thread A believes it's safely writing to a private block, and
// write_to_disk() proceeds directly against the ORIGINAL physical block// Thread A (the victim process, or a system process): running write() above
// Thread B (attacker, unprivileged user): writing to the same file concurrently
// Timeline:
// t0: Thread A takes the lock and enters write()
// t1: Thread A releases the lock while waiting on log space (step (1) above)
// t2: In that gap, Thread B takes the lock and completes its OWN CoW
Thread_B: {
lock(file.inode);
new_block = allocate_private_block();
copy(block, new_block);
remap(file, new_block);
block.refcount — ; // refcount drops from 2 to 1!
unlock(file.inode);
}
// t3: Thread A re-acquires the lock and resumes (step (2) above)
// But Thread A already judged at t0 that "this block has refcount > 1" —
// or, if it re-reads refcount after t2, it now sees refcount == 1 and
// mistakenly concludes "this must be my own private block now"
// — when in reality, Thread B just separated it
// t4: Thread A believes it's safely writing to a private block, and
// write_to_disk() proceeds directly against the ORIGINAL physical blockThe core issue: the refcount dropped to 1 not because "I just separated it," but because "someone else quietly did," and the kernel can't tell the difference. As a result, Thread A believes it's writing to its own private copy, but it's actually writing directly into the physical block still shared with the original file.
There's one more twist that seals the deal: O_DIRECT completely bypasses the kernel's page cache, so there's no cache-layer revalidation step to catch the mistake. The corrupted write goes straight through to disk, unchecked.
— -
Why This Leads All the Way to Root
This single primitive — "I can overwrite the real disk blocks of any file I can read" — chains directly into root:
1. The attacker creates a reflink copy of /etc/passwd or a SUID-root
binary (e.g. /usr/bin/su) inside a directory they own
(creating a reflink often only requires read access to the source)
2. The attacker repeatedly triggers the race condition described above,
so that "writing to my reflink copy" actually becomes
"writing to the ORIGINAL physical blocks (i.e. the real /etc/passwd data)"
3. The root account's password field in /etc/passwd is cleared,
or the su binary's code is tampered with to spawn a shell
4. This change:
— leaves file ownership/permissions/setuid bits completely untouched
(the inode itself is never modified)
— leaves nothing in the kernel log
— survives a reboot (it was written directly at the block layer)1. The attacker creates a reflink copy of /etc/passwd or a SUID-root
binary (e.g. /usr/bin/su) inside a directory they own
(creating a reflink often only requires read access to the source)
2. The attacker repeatedly triggers the race condition described above,
so that "writing to my reflink copy" actually becomes
"writing to the ORIGINAL physical blocks (i.e. the real /etc/passwd data)"
3. The root account's password field in /etc/passwd is cleared,
or the su binary's code is tampered with to spawn a shell
4. This change:
— leaves file ownership/permissions/setuid bits completely untouched
(the inode itself is never modified)
— leaves nothing in the kernel log
— survives a reboot (it was written directly at the block layer)In effect, the attacker swaps out the file's contents while leaving its metadata completely alone — so a tampered SUID-root binary keeps its setuid bit and keeps running as root indefinitely.
— -
Why Existing Defenses Don't Stop It
| Defense | Why it fails to help |
| — -| — -|
| KASLR / SMEP / SMAP | Designed against memory-corruption exploitation, not applicable to the block layer at all |
| SELinux (Enforcing) | Only normal syscalls (write, ioctl) are used, so no policy is ever violated |
| Kernel Lockdown | Doesn't restrict O_DIRECT or reflink creation (FICLONE) for unprivileged users |
| Seccomp | A typical profile allowing write/ioctl passes right through |
| Container isolation (namespaces, etc.) | The XFS reflink logic behaves identically inside a container's own volume |
Because this bug is triggered by calling permitted syscalls in a permitted way — just at a specific moment in time — syscall filtering and permission-based defenses can't stop it by design. The only real fix is a change to the kernel's logic itself.
— -
Conditions for Exposure (all three must apply)
- Kernel v4.11 or later (2017 onward), unpatched
- An XFS filesystem with
reflink=1on the superblock (themkfs.xfsdefault since 2019) - The attacker has write access to some directory on the same XFS volume as the sensitive target file
Many enterprise distributions — RHEL, CentOS Stream, Oracle Linux, Rocky/AlmaLinux, CloudLinux, Amazon Linux, Fedora Server — meet these conditions by default. Debian/Ubuntu/SUSE don't default to XFS, but remain equally exposed if XFS + reflink was chosen manually at install time.
— -
Mitigation
- Applying the kernel patch and rebooting is the only reliable fix. (Merged upstream on July 16, 2026; distro backports are in progress.)
- No configuration-level workaround is currently known.
- On systems that may have been exposed, it's worth verifying the actual contents of
/etc/passwdand SUID-root binaries against a trusted backup — metadata alone can't reveal whether tampering occurred.
— -
Summary
RefluXFS isn't a "memory corruption" bug — it's a logic flaw that exploits the split-second moment two threads hand a lock back and forth. A few lines of ordering — release lock → wait → re-acquire lock → act on a stale value — turn into a full path to root, and it's a solid reminder of why race conditions remain one of the hardest vulnerability classes to defend against.
The code in this post is conceptual pseudocode meant to illustrate the vulnerability's logic flow. It is not working exploit code or an attack tool.
— -