timerfd-epoll
This is yet another technique to widen the race window by injecting a long-running interrupt into the race window. The setup creates one timerfd, then dup()s it thousands of times, adding each dup’d fd to an epoll instance via epoll_ctl(). Each epoll_ctl(ADD) call registers one entry in the timerfd’s wait queue. When the timer fires, the kernel’s hrtimer callback walks this entire wait queue by calling each waiter’s callback. This walk runs in interrupt context, which means the scheduler cannot preempt it. Therefore, whatever thread was running on that CPU is frozen for the entire duration of the walk. If this interrupt fires during the race window, the window is effectively widened by how long the walk takes. Unlike userfaultfd or FUSE which stalls the thread indefinitely (hard stall), this technique only delays it (soft widening)/
For reference : https://projectzero.google/2022/03/racing-against-clock-hitting-tiny.html
I have written a demo race window using getpid() syscall to measure how long would it take with and without timer interrupts. Below is the source code and it’s output.
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#include <sched.h>
#include <sys/epoll.h>
#include <sys/timerfd.h>
#include <sys/resource.h>
#include <sys/syscall.h>
#include <time.h>
#define N_THREADS 180
#define DUPS_PER_THREAD 2500
#define RACE_SYSCALLS 300
#define SAMPLES 30
static int tfd;
static int epfd;
static volatile int threads_ready = 0;
static volatile int stop_threads = 0;
static void raise_limits(void) {
struct rlimit rl = { .rlim_cur = 1500000, .rlim_max = 1500000 };
setrlimit(RLIMIT_NOFILE, &rl);
}
static void *flood_thread(void *arg) {
if (unshare(CLONE_FILES) < 0) return NULL;
struct epoll_event ev = { .events = EPOLLIN };
for (int i = 0; i < DUPS_PER_THREAD; i++) {
int d = dup(tfd);
if (d < 0) break;
ev.data.fd = d;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, d, &ev) < 0) { close(d); break; }
}
__atomic_fetch_add(&threads_ready, 1, __ATOMIC_SEQ_CST);
while (!stop_threads) usleep(5000);
return NULL;
}
static void start_timer(long delay_ns) {
struct itimerspec its = {
.it_value = { .tv_nsec = delay_ns % 1000000000L,
.tv_sec = delay_ns / 1000000000L },
.it_interval = {0}
};
timerfd_settime(tfd, 0, &its, NULL);
}
// the simulated race window
// In real exploits, ioctl() or something that has potental race condition inside kernel space
static long measure_window_ns(void) {
struct timespec t0, t1;
clock_gettime(CLOCK_MONOTONIC, &t0);
for (int i = 0; i < RACE_SYSCALLS; i++) syscall(SYS_getpid);
clock_gettime(CLOCK_MONOTONIC, &t1);
return (t1.tv_sec - t0.tv_sec) * 1000000000L + (t1.tv_nsec - t0.tv_nsec);
}
int main(void) {
raise_limits();
tfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
epfd = epoll_create1(0);
// ── build the wait queue ───────────────────────────────────────────────
printf("[+] building %d wait queue entries\n",N_THREADS * DUPS_PER_THREAD);
pthread_t threads[N_THREADS];
for (int i = 0; i < N_THREADS; i++)
pthread_create(&threads[i], NULL, flood_thread, NULL);
while (threads_ready < N_THREADS) usleep(10000);
// Baseline: NO timer armed
printf("[+] Measuring window with NO timer\n");
long base_sum = 0;
for (int i = 0; i < SAMPLES; i++) {
base_sum += measure_window_ns();
usleep(200);
}
long base_avg = base_sum / SAMPLES;
printf("Baseline window = %ld ns \n\n",base_avg);
// Timer Added
long best_sum = 0;
for (int i = 0; i < SAMPLES; i++) {
start_timer(1);
best_sum += measure_window_ns();
usleep(300);
}
long best_avg = best_sum / SAMPLES;
printf("\n════════════════════════════════════════════\n");
printf(" race window WITHOUT Timer: %7ld ns\n", base_avg);
printf(" race window WITH Timer: %7ld ns\n", best_avg);
printf(" widened by: ~%.1fx\n",(float)best_avg / base_avg);
printf("════════════════════════════════════════════\n");
stop_threads = 1;
for (int i = 0; i < N_THREADS; i++) pthread_join(threads[i], NULL);
close(tfd);
close(epfd);
return 0;
}
$ ./race_demo
[+] building 450000 wait queue entries
[+] Measuring window with NO timer
Baseline window = 60201 ns
════════════════════════════════════════════
race window WITHOUT Timer: 60201 ns
race window WITH Timer: 147189 ns
widened by: ~2.4x
════════════════════════════════════════════