
Android GKI 6.12 Kernel-Exploit für CVE-2026-43499, der einen rt_mutex-Rollback-Bug mit pselect-Stack-Overwrite verkettet, um Root auf Samsung- und Pixel-Geräten zu erlangen.
Ein Linux-Kernel-Exploit für die Android-GKI 6.12-Linie (Samsung- und Pixel-
Geräte), der auf CVE-2026-43499 abzielt: einen fehlerhaften remove_waiter()-Rollback in
rt_mutex_start_proxy_lock(), der current statt waiter::task verwendet
und dadurch pi_blocked_on eines Waiters auf seinen (später entfernten) Kernel-Stack
rt_mutex_waiter zeigen lässt. In Kombination mit einem pselect()-fd_set-Stack-Overwrite und einem
konsumierenden, durch sched_setattr angetriebenen Fake-rb_tree-Durchlauf ergibt sich
ein deterministisches Kernel-Schreibprimitiv und vollständiges Root.
Der Exploit läuft als LD_PRELOAD-Shared-Library (preload.so) und installiert
einen Root-su-Daemon sowie ein Wallpaper als Post-Root-Artefakte.
Status: aktive Entwicklung. Das m1q-Ziel (ZF1) ist der Bring-up-Fokus. Der pipei tmp_page-uname-Bootstrap ist die aktuell aktive Route: Der Walk ist auf dem Gerät als sauber nachgewiesen (Build #33: pro-Kind geseedete rt_mutex-Regionen) und der vollständige 168-Kandidaten-Sweep läuft. Die configfs-CFI-Route ist eine Sackgasse auf m1q (Rust-Ashmem — kein injizierbarer fops-Slot) und bleibt die Route für C-Ashmem-Ziele. Die Offsets pro Ziel variieren je nach Gerät — vor dem Vertrauen gegen das tatsächliche Kernel-Binary verifizieren.
Wenn FUTEX_CMP_REQUEUE_PI einen Waiter requeued und der Deadlock-Erkennungs-
Kettendurchlauf des Requeues -EDEADLK zurückgibt, rollt __rt_mutex_start_proxy_lock()
über remove_waiter() (rtmutex.c:1535) zurück. Der Rollback dequeued den Waiter korrekt
aus dem Wait-Tree, löscht aber das pi_blocked_on des Requeue-Aufrufers
statt waiter->task->pi_blocked_on. Das pi_blocked_on des WAITERS
bleibt baumelnd an seinem Stack-rt_mutex_waiter, der entfernt wird,
sobald der Futex timeoutet.
Nach dem Timeout wird die Kernel-Stack-Region des Waiters wiederverwendet: core_sys_select()
kopiert die drei fd_sets in diesen Stack-Puffer (der nfds < 344-Stack-Pfad auf
ZF1). Ein präpariertes fd_set-Wort-Array materialisiert einen Fake-rt_mutex_waiter /
Fake-Task / Fake-rt_mutex (mit einer rb_tree-Wurzel, deren Zeiger kontrolliert werden).
Ein Consumer-Thread ruft dann sched_setattr_tid(waiter) auf →
rt_mutex_adjust_pi() → rb_erase_cached, was einen beliebigen
Adress-Schreibvorgang eines kontrollierten Werts erzeugt.
Das gesamte Primitiv erfordert den PI-Ketten-Zyklus: Der Owner hält f_pi_target
und blockiert zugleich auf f_pi_chain (gehalten vom Waiter), sodass der Kettendurchlauf
auf owner → chain → waiter → target → owner trifft und mit -EDEADLK fehlschlägt. Wird der
Zyklus entfernt, gibt der Requeue Erfolg mit null Kernel-Effekt zurück.
sched_blocked_reason (m1q primär, auf dem Gerät bewiesen): Der
gespeicherte Return-PC eines blockierten kworkers (stack_trace_save_tsk) wird aus
dem Ringpuffer gelesen und gegen den einkompilierten worker_thread-
Offset verglichen. Läuft, bevor irgendwelche boot_id-Routen-Wörter verwendet werden, damit Data-Alias-
Schreibziele (data_addr() = p0-Alias + slide_p0_offset) bei einem von Null verschiedenen Slide
korrekt bleiben.SLIDE_LOGGERS_0_1 über einen Linear-Map-
Alias in die boot_id-sysctl-Daten; der geleakte Wert rekonstruiert stext. Auf m1q geparkt: Sein W1-
Ziel (SLIDE_RANDOM_BOOT_ID_DATA_OFF, niedriger physischer RAM) hat
seitenabhängige Beschreibbarkeit — der Slide verschiebt das Ziel pro Boot über Seitengrenzen,
sodass ~6/7 Läufe fehlschlagen. Nur explizit über
SLIDE_FORCE_BOOTID (Mechanismus-Test) ausgeführt, mit tree_pc/tree_left
auf die gesprayte Seite umgeleitet.mm_struct-große Objekte sprayen und eine Futex-Hash-
Kollision nutzen, um ein auf dem Heap zu lokalisieren, wodurch eine Kernel-Heap-Seiten-
Adresse geleakt wird, die als Basis für den Fake-Object-Spray dient. Waiter werden beim
Cleanup abgetrennt (nicht gejoint), um den Kernel-Stack-OOM zu vermeiden, der Builds vor
#26 zum Absturz brachte.Die pselect-fd_set-Wörter materialisieren einen Fake-rt_mutex_waiter auf dem
Kernel-Stack des Waiters; der sched_setattr_tid(waiter) eines Consumer-Threads durchläuft
ihn via rt_mutex_adjust_pi(). ZF1-Wortkarte (disasm-verifiziert):
PSELECT_WAITER_WORD_SHIFT = 0, Wort 12 = task (@+0x50), Wort 13 = lock
(@+0x58), Wort 14 = wake_state (@+0x60 = 3). Mit einem geseedeten Fake-rt_mutex
schließt der Walk sauber ab: [7]s rb_erase W1 *(tree_left)=tree_pc / W2
*(tree_pc&~3+8)=tree_left sind die einzigen Kernel-Schreibvorgänge; [11] (setprio /
dequeue_pi) und der [9]-Wake werden beide übersprungen (lokaler Fake-Waiter bei Prio 100
hält den Stack-Knoten aus dem Top-Waiter). Jede walk-entered-Vervollständigung seit
Build #19 überlebt auf dem Gerät; die deterministischen Abstürze davor waren ein
8-Byte-Payload-Platzierungsfehler (SKB_DATA_DELTA), kein Walk-Body-Fehler.
Optionale STAGE3=1-Phase, die ein Bridge-Kind forkt, dessen mm->pgd
auf eine vorbereitete Fake-Page-Table tauscht (3-Ebenen: PGD→L1→L2, RX-Loop-Leaf + RW-Stack-Leaf)
und das Kind einen reinen Register-Assembly-Blob ausführen lässt, der seine eigenen Cred
durch ein 2MB-physisches Scan-Fenster patcht — all-RAM-Phys-R/W ohne die configfs/pipe-
Primitive. Derzeit nur in das m1q-Ziel verdrahtet und nicht auf dem Gerät
ausgeführt.
41 Ziele in src/targets/<codename>-<build>/, jedes benötigt mindestens eine
target.h (Kernel-Symbol-Offsets, KIMAGE_TEXT_BASE, Direct-Map-Basis, Struct-
Offsets). Pixel-Ziele (comet, tokay, tegu, caiman, komodo,
frankel, mustang, rango, stallion, blazer) überschreiben gemeinsame Quellen;
Samsung-Ziele (m1q-*) fügen gerätespezifische Logik hinzu.
make list-projects # full list
Kernel-Struct-Layouts unterscheiden sich je Gerät — Offsets müssen gegen das tatsächliche Kernel-Binary / kallsyms für jedes Ziel verifiziert werden.
Ausgabe: build/<PROJECT>/bin/preload.so (Shared Lib, geladen via LD_PRELOAD).
# Default project
CC=clang make
# Specific device target (default: blazer-CP2A.260605.012)
CC=clang make PROJECT=m1q-BP4A.251205.006
# Without CC=clang: uses NDK if found ($NDK_ROOT / $ANDROID_NDK_HOME /
# $ANDROID_NDK_ROOT), otherwise host clang + Android sysroot
make PROJECT=m1q-BP4A.251205.006
# Show build configuration
make info
# Clean
make clean
Der Build bettet ein PIE-su_daemon-Binary (src/su_daemon.c, gebaut zu
build/embed/su_daemon_aarch64_pie) und assets/wallpaper.webp in
preload.so via src/su_blob.S / src/wallpaper_blob.S ein.
Die Build-Ausgaben nach /data/local/tmp pushen, dann den Exploit unter
LD_PRELOAD ausführen. Auf einem Live-Bring-up-Ziel OHNE tee ausführen — tee puffert sein eigenes
stdio und verliert das Ende des Logs bei einem Kernel-Panic (Exploit-stdout ist
ungepuffert, daher bewahrt eine direkte Umleitung in eine Datei jede Zeile):
adb push build/m1q-BP4A.251205.006/bin/preload.so /data/local/tmp/preload.so
adb push build/embed/su_daemon_aarch64_pie /data/local/tmp/su_daemon_aarch64_pie
adb shell "chmod 755 /data/local/tmp/preload.so /data/local/tmp/su_daemon_aarch64_pie"
adb shell "LD_PRELOAD=/data/local/tmp/preload.so \
/data/local/tmp/su_daemon_aarch64_pie \
> /data/local/tmp/output.log 2>&1"
Bei Erfolg lauscht der Daemon auf /data/local/tmp/temp_su.sock und su ist
unter /apex/com.android.virt/bin installiert.
tools/run_pipei.sh pusht das Binary, verifiziert es per sha256 auf dem Gerät (ein
veraltetes preload.so erzeugt stillschweigend unlesbare Urteile — die Leiter lief einst
einen Build < #22, dessen Log-Format abweicht), und führt dann den vollständigen 168-Kandidaten-
Sweep in 3x56-Chunks aus, sodass ein Panic höchstens einen Chunk verliert (~10 min). Der Exploit-
stdout streamt direkt durch adb (keine geräteseitige Umleitung — ein
geräteseitiges tee verliert das Ende bei Panic); eine hostseitige Kopie jedes Chunks wird
unter /tmp/m1q_pipei_chunk{1..3}_stream.log aufbewahrt:
sh tools/run_pipei.sh
PASS = irgendein Chunk zeigt tmp_page uname changed + after uname='CatOS.
PIPEI_CHILD_REGIONS muss mindestens auf die Chunk-Größe gesetzt sein, sonst bleiben die pro-Kind-
rt_mutex-Regionen ungeseedet und der Sweep panict bei Kandidat 0. Vollständige
Abdeckung erfordert außerdem TMP_UNAME_PIPEI_SPLIT_ORDER_PAGES=1 — ohne dies wird nur
eine Seite des freigegebenen 8-Seiten-mm-Blocks gesweept. run_pipei_slim.sh ist die
Einzelkandidaten-Diagnosevariante; run_m1q_ladder.sh ist die
Walk-/Isolations-Batterie (B16-2-Orakel, Slide-Mechanismus, Sweep-Schritte).
exploit/
Makefile
assets/ # wallpaper.webp (embedded post-root artifact)
src/
main.c # run_exploit() entry point (shared)
preload.c # LD_PRELOAD constructor, su/wallpaper install
util.c # addr translation, page prep, configfs helpers
pipe.c # pipe-based physical read/write
root.c # cred patching / task walk
slide.c # KASLR slide (tracefs + boot_id routes)
fops.c # pselect corruption + fops manipulation
faketables.c # stage-3 fake page-table builders (3-level)
stage3.c # pgd-swap bridge child (no-op unless configured)
stage3_loop.S, stage3_poll.S # bridge blob + enter trampoline
su_daemon.c # root daemon (compiled as separate PIE)
su_blob.S, wallpaper_blob.S # embedded binaries
offset.h # pulls per-target target.h via -DTARGET_CONFIG_H
kernelsnitch/ # mm_struct futex-hash leak library
targets/<codename>-<build>/ # per-device target.h + optional overrides
poc/poc.c # standalone POC (separate from the exploit)
Ziele überschreiben gemeinsame Quellen über den pick_src-Mechanismus des Makefiles:
Wenn src/targets/<PROJECT>/<file>.c existiert, wird es statt der gemeinsamen
src/<file>.c verwendet; andernfalls wird die gemeinsame Version verwendet.
-EDEADLK (errno 35) fehlschlägt. Ein fehlender Zyklus zeigt sich als
success=1 mit null Kernel-Effekt.PSELECT_WAITER_WORD_SHIFT muss 0 sein (ZF1-Stack-Tiefen aus ELF
verifiziert). Andere Ziele verwenden weiterhin standardmäßig 1 — pro Gerät verifizieren, bevor
man ihnen vertraut.p0_data_alias() verwendet DIRECT_MAP_BASE (die lineare Map), die sich
von P0_PAGE_OFFSET auf ZF1 unterscheidet. Daten-Aliase für dereferenzierte Kernel-Objekte
müssen die lineare Map verwenden.pr_error() ist fatal (exit(-1)), anders als die Warn-/Info-Makros.SLIDE_LOGGERS_0_1, nicht SLIDE_NFULNL_LOGGER;
das Wort ist ein nicht-randomisierter Linear-Map-Alias, daher ist die boot_id-Route
slide-unabhängig (nur bei slide=0 verwenden — zuerst den tracefs-Leak ausführen).Dies ist Exploit-Code für Android-Geräte und ist für Sicherheitsforschung, autorisiertes Schwachstellentesting und Verteidigungsarbeit auf Hardware gedacht, die Sie besitzen oder für die Sie ausdrückliche Testgenehmigung haben. Missbrauch auf Geräten, die Sie nicht besitzen, ist illegal. Der Autor ist nicht verantwortlich für gebrickte Geräte, gebrickte Kernel oder erlöschene Garantien.
mm_structwrite_iter-Slot und das ASHMEM_SET_NAME-Heap-Objekt ist
ein KVec, dessen Layout nicht mit dem private_data von configfs_bin_write_iter
übereinstimmt. m1q wechselt stattdessen zu einem kmalloc-Schreibvorgang: den geleakten mm-
Order-3-Block als pipe_inode_info-Objekte zurückgewinnen und den pselect-W1-Schreibvorgang
(*(tree_left) = tree_pc) nutzen, um den tmp_page-Slot eines Kandidaten
(+0x90) mit der UTS-Namespace-Seite (init_uts_ns) zu überschreiben. Ein Fanout-Schreibvorgang platziert
einen Marker-Namen am sysname-Offset und uname() meldet "CatOS" — ein
verifizierbarer Kernel-Schreibvorgang ohne Abhängigkeit von statischen Objekten. C-Ashmem-Ziele
behalten den configfs-Pfad.preload.c den eingebetteten su-Daemon (tmpfs-gemountet in
/apex/com.android.virt/bin, plus adbd-Namespace- und lokale Varianten) und
tauscht das Wallpaper aus.| Variable | Standard | Zweck |
|---|
PSELECT_ROUTE_SHIFT | Compile-Zeit | A/B-fd_set-Wort-Shift für die Hauptroute (m1q) |
SLIDE_SHIFT | Compile-Zeit | A/B-fd_set-Wort-Shift für die Slide-Route |
PSELECT_ROUTE_DELAY_USEC | 50000 | Consumer-Verzögerung vor sched_setattr (muss > 0 sein) |
SLIDE_CONSUME_USEC, SLIDE_ENTER_DELAY_USEC, SLIDE_CONSUMER_CORE | — | Slide-Routen-Timing/Pinning |
SKIP_SLIDE | 0 | Direct-Map-Fallback verwenden (slide = 0) |
SLIDE_ONLY | 0 | Nur den KASLR-Slide ausführen und beenden |
SLIDE_P0_OFFSET, SLIDE_BOOTID_OFF | — | Slide-p0-Alias / boot_id-Offsets überschreiben |
SLIDE_FORCE_BOOTID | 0 | Die boot_id-pselect-Route nach dem tracefs-Slide erzwingen (Mechanismus-Test) |
KSNITCH_COLLISIONS | 4 | KernelSnitch-Kollisionsanzahl |
STAGE3 | 0 | Die pgd-Swap-Bridge-Kind-Phase aktivieren (m1q) |
STAGE3_DRYRUN | 0 | Tabellen vorbereiten/verifizieren, dann vor dem Swap abbrechen |
PAGE_RECLAIM_SENDS, PSELECT_SIMPLE_LAYOUT | — | Spray-/fd_set-Layout-Anpassungen |
TMP_PAGE_UNAME | 0 | Den m1q pipei tmp_page-uname-Bootstrap ausführen |
TMP_UNAME_PIPEI_SWEEP, TMP_UNAME_PIPEI_ALIGN_ORDER, TMP_UNAME_PIPEI_SPLIT_ORDER_PAGES | — | Sweep-Modus; split_order sweept alle 8 Seiten des freigegebenen mm-Blocks |
TMP_UNAME_PIPEI_SLOT, TMP_UNAME_PIPEI_SLOT_CANDIDATES | — | Sweep-Bereich (Chunk-Start / Anzahl) |
PIPEI_CHILD_REGIONS | 0 | Pro-Kind geseedete rt_mutex-Regionen; MUSS >= Sweep-Chunk sein, sonst panicen ungeseedete Regionen bei Kandidat 0 |
PIPEI_RECLAIM_COUNT, PIPEI_DRAIN_COUNT | — | Pipe-Paare zurückgewinnen/drainen; bei einem Reclaim-Fehlschlag erhöhen |
PIPEI_SPONGE_MB | 3072 | Order-0-Flood-Schwamm, der über den Reclaim gehalten wird (Build #29) |
PSELECT_W0_PRIO_OVERRIDE | 130 | Top-Waiter-Prio; inert mit geseedeten Regionen (Legacy-Stale-Tree-Guard, nur A/B) |
PSELECT_SKB_DELTA_OVERRIDE | 0 | A/B-Payload-Intra-Page-Offset (nur falls ein Gerät von -0xe78 abweicht) |
TMP_UNAME_NAME, TMP_UNAME_HOLD_SEC | "CatOS", 5 | Marker-Name / Haltezeit nach Erfolg |
SKB_DATA_DELTA ist -0xe78 (auf dem Gerät gemessen, Build #19) — der echte
skb-frag-Head ist 8 Bytes kleiner als der disasm-abgeleitete -0xe80; dieser 8-Byte-
Fehler verursachte jeden deterministischen pselect-Absturz von Builds #6-18
(walk-entered → Garbage-Deref). GKI 6.12 gemeinsamer Wert; pro-Ziel-util.c-
Kopien tragen einen SKB_DATA_DELTA-Env-A/B-Knopf. PSELECT_SKB_DELTA_OVERRIDE
NICHT auf die eingebackene Konstante stapeln.ASHMEM_SET_NAME-KVec-Hülle passt nicht
zu configfs_bin_write_iter. Den pipei tmp_page-Bootstrap verwenden. Die
PSELECT_WRITE_RIGHT_SPRAYED / PSELECT_PI_TOP_TASK_SELF-Guards in m1q
target.h leiten die [11]-Schreibvorgänge des Walks von den RO-statischen fops weg.payload+0x6000 + idx*0x40) innerhalb der
zurückgewonnenen Seite um. Shared-fake_lock-Sweeps panicten bei Kandidat ~9-12 (recycelte
Stale-Kernel-Stack-Knoten), und die leeren Regionen von Build #32 panicten bei
Kandidat 0 ([9] owner==NULL-Wake auf dem mit 0x41 gefüllten fake_task).
PIPEI_CHILD_REGIONS muss gesetzt sein, sonst bleiben die Regionen ungeseedet.