Skip to content
Exploit for Use After Free in Linux Linux_Kernel

Exploit for Use After Free in Linux Linux_Kernel

Sploitus • September 18, 2026

vivo S18 (PD2323 / V2323A) CVE-2026-43499 (GhostLock) Privilege Escalation Research Archive — A Complete Record of the Approach, Payload Iterations, and Pitfalls

- Device: vivo S18 (PD2323 / V2323A), Qualcomm SM7550 (Snapdragon 7 Gen 3)

- Kernel: **5.15.197-ga82566cae4ed-dirty** (Firmware PD2323_A_16.2.10.2.W10, SPL 2026-07-01)

- BL: Unlocked (verifiedbootstate=green), SELinux enforcing, research baseline uid=2000 (shell)

- Workload: 2 days of intensive sessions (September 16–18), 8 payload versions, 4 rounds of physical device testing, full kernel disassembly project

**Related Post**: [249707737/vivo-S18-SM7550-Kernel-5.15.178-Research-Archive]( vivo-S18-SM7550-Kernel-5.15.178-Research-Archive) (same device model, kernel 5.15.178 / firmware 16.2.9.0; primarily static analysis). The two archives were studied independently and cross-validated to confirm convergence—see §8.

**The slide/pselect disclosure vector for CVE-2026-43499 (GhostLock) is structurally infeasible on this device (5.15.197).**

1. **Stack Geometry Deadlock**: The leak write (`[rb_left] = rb_parent_color` in rb_erase Case 2) requires the **tree_entry of the stale waiter (o=0x00-0x10, depth K-0x2F8~K-0x308) ** to be overwritten by user-controllable data. A full scan (1,254 reachable roots, 851 `copy_from_user` landing points, re-sifting span by span) + ldtr/get_user-style supplementary scans + sigreturn special cases—**not a single carrier reaches o=0x00** — The sigreturn FPSIMD/SVE staging vector (original to this project, D=K-0x2F0/0x300) **is off by the last 8 bytes**.

2. **Source-code-level verification of the punch path**: The `sched_setattr → rt_mutex_adjust_pi → adjust_prio_chain(orig_lock=NULL)` path exists, but at the first hop, `rt_mutex_waiter_equal(stale, task_to_waiter(fair task)=stale itself)` ** is always equal, causing the function to exit**; the residual value of `tree_entry`, `{0x1, NULL, NULL}` (the determined state of a single-waiter root node after being dequeued by `live`), ensures that `rb_erase` performs a leak-free write.

3. **Convergence confirmed via cross-verification with Archive 178* *: Independent research on the same machine with different patch levels (178/197); full syscall scans, defense matrices, and dead-end lists are completely consistent (§8).

**Confidence Level**: Line-by-line source code analysis + full-scale scanning + 4 rounds of real-machine validation data using 8 payloads. This is not merely “tried a few times without success,” but rather “exhaustively tested all known carrier classes.”

`rtmutex.c remove_waiter()` uses `current` instead of `waiter->task` to clear `pi_blocked_on` during futex PI proxy rollback (-EDEADLK) (introduced in 2011; affected versions: 2.6.39 → 7.1-rc1; mainline fix commit 3bfdc63936dd). Consequence: **The `pi_blocked_on` of the waiter task is a dangling pointer to a waiter on the stack that has been discarded following the return from `futex_wait_requeue_pi`**—resulting in a stack UAF.

Exploit Model (GhostLock): After the waiter thread returns to user mode, **overwrite the dead waiter** (the stack reclamation vehicle) with user-controllable data from the same thread’s stack, then trigger a PI adjustment for that task— —the kernel traverses the chain via the dangling `pi_blocked_on` and performs an `rb_erase` on the stale waiter—**all node fields (tree_entry) are attacker data → constrained write primitives** → writing to `boot_id` leaks KASLR → establish arbitrary write capability → cred patch.

boot.img (v4) → Image extraction → Full kallsyms recovery (189,948 symbols)

→ Full Capstone disassembly project (frame table/call graph/anchors)

→ target.h experimental framework (symbol layer + struct layer + three-level confidence annotation [M]/[I]/[C])

→ KSuRoot kernelpack-compatible payload construction (NDK r29, 8 iterations)

→ Exhaustive carrier enumeration (copy_from_user / get_user / sigreturn / compat / mcast)

→ sigreturn FPSIMD-vregs staging (original carrier developed for this project)

→ KSuRoot App integration (Custom dynamic library channel + adb UI-based operations)

→ Physical device identification (probes / app runtime logs / wedge attribution)

All environmental prerequisites met (same as successful upstream cases): `CONFIG_FUTEX_PI=y`, do_futex not inlined (three-layer chain intact), RANDOMIZE_KSTACK_OFFSET disabled, KASLR text slide fixed in actual testing (0x20800000 as verified in the 178 archive of the same family).

## 2. Experimental Geometry (Local Image, Full Capstone Decoding)

| futex | `__arm64_sys_futex` | 0xffffffc0082d3a2c | 0xa0 |

| futex | `do_futex` | 0xffffffc0082c6f0c | 0xd0 |

| futex | `futex_wait_requeue_pi` | 0xffffffc0082cbf1c | 0x1c0 |

| pselect | `__arm64_sys_pselect6` | 0xffffffc0086101 f8 | 0xa0 |

| pselect | `core_sys_select` | 0xffffffc00860ec1c | 0x1c0 |

- **&rt_waiter = fwrpi sp+0x28** (Clear evidence of three independent call points: `add x3/x2/x1, sp, #0x28` → rt_mutex_slowlock_block / try_to_take_rt_mutex / remove_waiter)

- sp+0x80 = hrtimer_sleeper (timeout) (Source of the old mislabeled 0x80)

- fd_set stack buffer bits = core_sys_select **sp+0x50** (Supported by the zeroed-out region [0x50, 0x150))

### 2.3 Determinative Formula (K = sp at syscall entry)

W_depth (rt_waiter base address) = 0xa0+0xd0+0x1c0-0x28 = K-0x308

B_depth (fd_set base address) = 0xa0 + 0x1c0 - 0x50 = K - 0x210

Shift formula (payload convention: global = shift + o/8 + 2): shift = (B - W)/8 - 2 = -33 → invalid

Machine verification: TaskA/verify_math.txt (frames automatically extracted and independently recalculated; all results match)

### 2.4 Actual Requirements for Leak Writes (Key Point Discovered Later)

The leak write `[rb_left] = rb_parent_color` (rb_erase Case 2) operates on the **tree_entry** of a stale waiter:

| Field | o | Depth | Value Requirement |

| tree_entry.parent_color | 0x00 | K-0x308 | **LOGGERS (fixed, known address)** — the leaked value itself |

| tree_entry.rb_right | 0x08 | K-0x300 | 0 (Case 2 condition) |

| tree_entry.rb_left | 0x10 | K-0x2F8 | BOOTID (write target = boot_id buffer) |

**tree_entry must be staged**—the resulting 8-byte gap (parent_color) thus becomes a global deadlock (§3).

## 3. Carrier Exhaustive (Final scan results for all categories)

| copy_from_user destination | 1,254 reachable roots, 851 endpoints fully scanned + **span re-screening under correct conditions** (D ≥ 0x308 and D−size ≤ 0x2B0) | Zero hits |

| get_user / Inline small copy (ldtr signature) | ldtr→str [sp] Full kernel scan in this mode; method validity verified via `__arch_copy_from_user` (34×ldtr) | [0x2E0,0x320] Zero hits |

| sigreturn FPSIMD-vregs (**Original to this project**) | `__copy_from_user(fpsimd.vregs, sigframe, 0x200)` @ K-0x2F0, contents = user registers V0–V31 | Covered o≥0x18, **missing 8 bytes of parent_color** |

| sigreturn SVE variant (D=K-0x300) | Same as above, +0x10 depth | Still missing o=0x00-0x07; sve header (D=0x310) is a fixed SVE magic number |

| Compatibility | Requires a 32-bit caller; SM7550 big core A715 lacks AArch32 | Unavailable |

| RMG MCAST (S23U verification path) | ip_mcast_join_leave, etc.; prefix-aware D_true | 0x420–0x490, out of bounds |

| CVE-2025-38352 | POSIX CPU timers configuration match (TASK_WORK=n) | **5. 15.179+ Fixed**, 197 included (git.kernel.org v5.15.179-197 file signature verification) |

| CVE-2026-64560 | UAF reliably triggerable | k_itimer dedicated cache isolation, heap spray failure |

**Conclusion**: The 8 bytes required for the leak write (parent_color @ K-0x308) are out of reach in all known carrier classes. Residual value of `tree_entry` = `{0x1, NULL, NULL}` (Determined state after a single-waiter root node is live-dequeued) → No write leak occurs when `rb_erase` follows the leaf path.

## 4. Source-Code-Level Verification of the “punch” Path (Why `sched_setattr` Is Ineffective)

v5.15 line-by-line (rtmutex_api.c:431 `rt_mutex_adjust_pi` / rtmutex.c `rt_mutex_adjust_prio_chain`):

void __sched rt_mutex_adjust_pi(struct task_struct *task) {

waiter = task->pi_blocked_on; // = stale waiter

if (!waiter || rt_mutex_waiter_equal(waiter, task_to_waiter(task)))

rt_mutex_adjust_prio_chain(task, RT_MUTEX_MIN_CHAINWALK, NULL, // orig_lock = NULL!

- Chain walk iteration 1: `waiter = task->pi_blocked_on` = stale; `task_to_waiter(fair task) = pi_blocked_on` = stale itself → **always equal → out** (no rb operations)

- If the task’s RT conversion breaks the equality: [7] `rt_mutex_dequeue(fake_lock, stale)` erases the **`{0x1,NULL,NULL}`** residue in `tree_entry` → leaf erase → no leak write

- [6] Not triggered (orig_lock=NULL) — the “EDEADLK early exit” concern from the v7 era does not exist, but this does not help either

**This conclusion is validated by closed-loop testing**: r6 (punch preempted by route_done, not issued) — 12+ attempts, all graceful; r7 (punch issued) — wedge → reboot within 1 minute— —attributed to pathological accumulation of mm-leaks (r8 manual shell test showed a load of 2953) rather than the punch itself.

## 5. Payload Iteration History (8 Versions)

| Version | First 8 characters of SHA256 | Changes | Results on Physical Device |

| v1 | 3bee63e4 | Upstream build; **dead shift in top-level `slide.c`** (does not consume `PSELECT_WAITER_WORD_SHIFT`; all four `sed` variants provide SHA corroboration) | Discarded without device testing |

| r2 | ad2b95cb | Full tokay-C1 package adopted (compiled with shift=6) + Fixed kernel_rw_fd UND + FDs preserved on successful paths | Manual shell with 9 env groups: `/system/bin/true` exits immediately, wiping everything; long-running sleep shell with 8 groups all cause a wedge (LOCK_PI storm ~430 tid) |

| r3 | f5462215 | RMG SIGKILL-supervisor structure port (PDEATHSIG/poll timeout/kill cleanup) | App path still wedges (helper churn 10s → reboot) |

| r4 | f3c7c8ed | `generate_target.py` regenerated (**fallback value mistakenly treated as native BTF**: `shift=0`/`WAITER_LOCAL_OFF=0x98`) | Wedge at the same point |

| r5 | 6b546ffc | slide.c:494 hard-coded `{1}` → changed to consume target.h | **vivo picker selected the group header; import failed and was not deployed** |

| r6 | 904377f3 | **sigreturn FPSIMD staging injection** (Default route) | **12+ attempts, all graceful, zero wedge**; each attempt failed because the punch was not sent (timing bug: `route_done` preempted the 50ms `enter_delay`) |

| r7 | ba7fd7a8 | Timing fix: Wait for consumer punch to complete after stage | Punch sent → ~1 min reboot (attributed to mm-leak pathological accumulation; r8 load 2953 is of the same type) |

| r8 | (Experiment) | Manual shell execution | Stuck in mm-leak (all failures in fresh boot shell domain) + load 2953 pathological behavior; reboot to clear the system |

## 6. Pitfall Checklist (Key Points for Future Researchers)

1. **Shift key deadlock**: The top-level `slide.c` hard-codes `SLIDE_PSELECT_WORD_SHIFT 0`, which does not consume the key from `target.h`—the fact that all four variants the same SHA is the only circumstantial evidence. **Every time you change a parameter, you must perform a "consumption sanity check" (change value → recompile → SHA must change) + grep for consumption points**.

2. **slide.c:494 hard-codes `shift_candidates[] = {1}`**: The `shift` value in `target.h` is only fed to the main FOP path; the slide path always uses 1.

3. **Tool fallback masquerading as derivation**: `generate_target.py` silently falls back to hard-coded values (shift=0/WAITER_LOCAL_OFF=0x98) when BTF is unavailable; this was once mistakenly submitted as “native BTF” during a lab. **Fallbacks must be explicitly flagged, and the values must include source annotations**.

4. **kernel_rw_fd UND Loading Failure**: The `fops.c` adopted by C1 is missing the `kernel_rw_fd` definition → the dynamic linker directly rejects loading; furthermore, the C1 success path `close (fd)` has reversed semantics. **Always check the UND symbol table before pushing**.

5. **Manual shell host lifespan**: `/system/bin/true` exits immediately; the 30-second race condition window is not even completed. Host lifespan ≥ maximum kernel wait time.

6. **LOCK_PI storm**: Failed attempts hold the lock → ~430 tid stuck at the same uaddr + spinning in R state → watchdog reboot. **Must use an RMG-style SIGKILL supervisor** (process death = full kernel futex-exit reclamation); in-process unwind is not feasible.

7. **sigreturn staging timing**: `route_done` is released immediately after staging completes; the main thread’s read of `boot_id` will preempt the consumer’s 50ms `enter_delay`—**the "bad leaked pointer" actually reads the raw `boot_id` UUID string** (ASCII characters from c74ec690-…!). Look for this characteristic first when analyzing the logs.

8. **8-byte gap in tree_entry** (Final Analysis): The leak requires `o=0x00-0x10`; even after exhausting all carrier classes, there is still an 8-byte gap in `parent_color`—**this is not a parameter tuning issue**.

1. **No delta feasibility check**: Neither end of the derivation chain checks whether `(B-W)` falls within a valid window—this has been institutionalized in `check_delta.py` (7/7 self-tests).

2. **Single boot, single variable**: Each failure = wedge + reboot. Static testing without closed-loop verification is not performed on the target machine.

3. **Interpretation: Identify characteristic values first**: A bad leaked pointer = the original UUID of `boot_id`; a `sched_ret` hang ≠ a semantic issue with `sched_setattr` (may be timing-related or other issues).

4. **Evidence Integrity**: "Phantom writes" (fabricated strings not verified by tools) have appeared in 916 sessions. All conclusions are anchored to tool outputs.

1. **PowerShell → SSH → ADB Quote Hell**: Always save scripts to disk / use base64-encoded commands; never manually embed nested quotes.

2. **Three Pitfalls of Windows OpenSSH**: The Administrators group has read-only access to `administrators_authorized_keys`; ACL ownership; and LF line endings.

3. **vivo File Selector**: When importing via the app, you must select the **file body** of the .so file; selecting the “Download” group header will cause the verification to fail.

4. **KSuRoot App Logs**: Payload stdout is not logged in logcat; it appears only in the app’s “Run History” (accessible after the fact, but current entries are lost upon reboot); only manual execution in the shell domain appears in the console.

5. **mm-leak Domain Differences**: The app domain (untrusted_app) has a normal success rate on fresh boot; the shell domain fails entirely on fresh boot and shows abnormally high load (2953). Run after the device has warmed up or use the app domain.

6. **Root-free identity chain verification**: Character-by-character comparison of `/proc/version` + `zcat /proc/config.gz | sha256sum` ≡ firmware-embedded config (S18 passes both tests).

- **IKCONFIG** (S18.config sha256 `165e0b01…a94ff`, can be verified without root by comparing with `/proc/config.gz`): FUTEX_PI=y, CFI_CLANG=y (non-kCFI inline, range check table with 137 slots), SHADOW_CALL_STACK=y, PAC=y, VMAP_STACK=y, PANIC_ON_OOPS=y, USER_NS=n, VA_BITS=39, RANDOM_KMALLOC_CACHES=y, SLAB_FREELIST_HARDENED=y, MODULE_SIG_PROTECT=y (FORCE=n), DEBUG_INFO_BTF=y but strip

- ** vr.ko (vivo anti-root) analysis**: Version 5.15** lacks** the PD2520 tag-and-kill mechanism (162-slot obfuscated symbol table fully decoded, no commit_creds hook) ; Actual mechanism = avc_has_perm self-limiting kprobe + vrp SELinux domain + hfm/ftrace engine. **GhostLock’s direct modification of the cred path is not intercepted, requires no VR bypass, and VR_TAG_* must never be defined** (as it would be compiled into `patch_task_vr_tag`, which clears arbitrary bytes in the task).

- **KASLR**: Text slide leakage was not successful on this machine (leaked data is out of reach); Tested on the same 178 archive**, fixed at 0x20800000** (kaslr_early_init disassembly + multiple reboots). The physmap alias (where P0_PAGE_OFFSET=0xffffff8800000000) are unrelated to KASLR and are known throughout the process

- **symbol/offset final values** (197, corresponding to evidence/ disassembly in this repository): init_task=0x2ef0d00, TASK_TASKS=0x4d0, ATOMIC_FLAGS=0x598, CRED=0x790/0x798, PI_BLOCKED_ON=0x8b0, rt_mutex_waiter 5.15 flat layout (tree@0/pi_tree@0x18/task@0x30/lock@0x38/prio@0x44), ASHMEM_MISC_FOPS=0x2f3fc00

## 8. Cross-validation with the 5.15.178 archive

| Item | 178 Archive | This Project (197) | Consistency |

| futex chain frame | 0xa0+0xd0+0x1c0 | Same | ✓ |

| rt_waiter | K−0x260 (x29−0x90, 178) | K−0x308 (sp+0x28, three call sites, 197) | Patch-level offsets differ, **same deadlock family** |

| Difference | 0x50 | 0xA0 | Determined by compilation output; **neither can be fixed via parameters** |

| Full syscall scan | 355 full scans, no byte-level overlaps | 1,254 roots + re-sifting with correct spans, zero hits | ✓ |

| Defense Matrix | PANIC_ON_OOPS/SLAB Isolation/Seccomp/USER_NS=n | All match | ✓ |

| KASLR slide | Fixed at 0x20800000 (178 in tests) | 197 untested (text leak unreachable) | High probability of being fixed within the same family |

| CVE-2025-38352 | "Configuration matches, only one case not exhausted" | **5. 15.179+ Fixed; 197 contains ** (verified via git.kernel.org file signatures) | 178 conclusion applies to old firmware; 197 not applicable |

## 9. Final Conclusions and Future Directions

**On 5.15.197 firmware, all publicly known privilege escalation paths have been closed**: GhostLock leak-based write dead end (this archive); 38352 fixed (5.15.179+); 64560 heap spray isolated by dedicated cache; see §11 of the 178 archive for the list of excluded CVEs.

1. **Wait for new CVEs + configuration matches** (monitor disclosures related to PI/futex/SLUB in the 5.15 series)

2. **Port to 6.6 devices** (Neo11 Plus / blazer / frankel) — The original IonStack exploit is directly usable (upstream testing shows 70–90% success rate)

3. get_user-style scans have been exhausted; **if a new stack carrier covering K-0x2F8~K-0x308 is discovered in the future, the GhostLock approach can be revived** (staging, timing, and detection engineering are all ready; only the carrier function name needs to be replaced)

- `docs/gl_review_20260918.md` — Final audit report (Appendix A: Machine verification / B 0916 Pitfall Avoidance / C Carrier Reselection / D r6-r7 Detection / E Manual Shell / F Final Conclusion / G 178 Cross-Validation / H ldtr Blind Spot Scan)

- `docs/gl_state.md` — Full Measurement Ledger

- `tools/` — All analysis and deployment scripts (kallsyms pipeline, carrier_scan, check_delta, verify_sve, lockpi_depth, UI automation, deploy)

- `payload_src/` — Copy of the vivo-S18 target payload source code (slide/fops/main/util/common/target.h)

- `evidence/` — Device logs (attempt1/console1) + disassembly evidence (verify_math/carrier_scan/verify_sve/geom_out1/fwrpi_anchors/ldtr_scan)

The contents of this repository are intended solely for security research and educational purposes. All data is derived from legitimate research conducted on devices owned by the author. Please do not use this content for any illegal purposes.

Extracted Entities

Domains (1)

IP Addresses (1)