
CVE-2026-98260 (posix-cpu-timers exec UAF) Exploit-Forschung: S9280 Android 14 GKI 6.1 — UAF-Trigger + Zero-Write-Primitive + SELinux Permissive. Übergabedokument in HANDOFF.md
exec: Cleanup POSIX timers right after de_thread() (Autor Hyunwoo Kim, 2026-09-11 upstream,
Fixes: 55e8c8eb2c7b = ab 5.7).
Am Nicht-Leader-Thread E hängt ein CLOCK_THREAD_CPUTIME_ID-Timer; in execve()
schlägt nach dem tid-Austausch durch de_thread() die Rückwärtssuche des Timers nach dem Task per PID fehl, und der Knoten bleibt in den
posix_cputimers von E zurück. Wenn mmap_read_lock_killable() in exec_mmap() -EINTR
zurückgibt (exec schlägt fehl und der Bereinigungscode steht weiter hinten), gibt do_exit() → exit_itimers() den k_itimer
frei, aber der Knoten wird nicht entfernt; wenn der Elternprozess per wait4() aufräumt, führt cleanup_timerqueue() → timerqueue_del()
eine Operation auf einem bereits freigegebenen Objekt aus → UAF.
| Branch | posixtimer_exec | Status |
|---|---|---|
| linux-6.1.y | nicht vorhanden | nicht behoben |
| linux-5.15.y | nicht vorhanden | nicht behoben |
| linux-6.6.y | vorhanden | behoben (6.6.158+) |
| linux-6.12.y | vorhanden | behoben (6.12.112+) |
→ Die Ziele dieses Projekts (5.15-/6.1-Linie sowie 6.6 unterhalb von 6.6.158) liegen alle im betroffenen Bereich.
fragment_vmas() zerlegt den Adressraum mit memfd + MAP_FIXED in
--vmas (Standard 50000) nicht zusammenführbare VMAs (jede hat vm_pgoff = 0,
benachbarte Adressen haben kein fortlaufendes pgoff → vma_merge schlägt fehl). Der Locker-Thread ruft wiederholt mprotect
über den gesamten Bereich auf; das Durchlaufen von Zehntausenden VMAs hält den mmap-Write-Lock millisekundenlang.ESRCH von tgkill(tgid, alte tid, 0),
dass „de_thread() den tid-Austausch abgeschlossen hat", und sendet erst dann SIGKILL.exec_mmap() bei mmap_read_lock_killable()
(der Read-Lock wird vom Write-Lock blockiert), und SIGKILL trifft Mikrosekunden später ein → dieser Aufruf gibt -EINTR
zurück → exec schlägt fehl. Das Fenster (ms) ≫ Reaktionszeit (µs), daher ist kein Glück nötig.⚠️ Zu unterscheiden: Der Trigger ist deterministisch; vom UAF zu beliebigem Schreiben/Ändern der cred hängt es von der
Slab-Wiederverwendungs-Orchestrierung des posix_timers_cache ab (Order, Objects/Slab, CPU-Partial,
memcg-Zugehörigkeit) — das ist messbares, iterierbares Heap-Engineering, kein Race.
export ANDROID_NDK_HOME=/home/nt/项目/samsung_root_research/06_工具链/android-ndk-r29
make # 产物 trigger(aarch64 / Android 21+ / 仅依赖 libc、libdl)
adb push trigger /data/local/tmp/
adb shell "chmod 755 /data/local/tmp/trigger"
adb shell "/data/local/tmp/trigger --check" # 先看环境
adb shell "/data/local/tmp/trigger; echo exit=\$?"
Erfolgsindikatoren (einer von drei reicht):
[*] 开始触发 startet das Gerät neu / Bildschirm wird schwarz (am direktesten bei panic_on_oops=1)exit=137/139), und das System lebt danach nochcleanup_timerqueue / timerqueue_del /
rb_erase / rb_nextBei Fehlschlag (keine Auslösung) versucht das Programm selbst --attempts Mal erneut und gibt für jede Runde das wait4-Ergebnis aus,
zuletzt folgt ein Hinweis zum Parameter-Tuning: --vmas 120000 --lockers 4 --cpu 0.
panic_on_oops ist möglicherweise 0: Dann wird die Aufgabe beendet, und der Kernel läuft mit einem beschädigten rbtree weiter;
es wird empfohlen, 2–3 Mal hintereinander zu laufen und zu beobachten, ob ein nachfolgender Panic auftritt.Testumgebung: S9280ZHU6EZIF, Kernel 6.1.162-android14-11-34343818-abS9280ZHU6EZIF
(Build 2026-09-20), nicht gerootet, shell-Domäne (uid 2000), SELinux Enforcing.
| Runde | Beobachtung | Bedeutung |
|---|---|---|
| Jede Runde | Elternprozess tgkill(child_tgid, etid, 0) gibt ESRCH zurück | de_thread() hat den tid-Austausch abgeschlossen |
| Jede Runde | Danach kill(child_tgid, SIGKILL), Kindprozess rc=-9 | exec wurde abgebrochen (nicht rc=0 eines erfolgreichen exec) |
--no-killer-Kontrolle | exec von armed bis Ende dauert 277 ms (mit killer 287 ms) | Der mmap-Schreib-Lock blockiert tatsächlich mmap_read_lock_killable() von exec_mmap() |
5/5 Runden trafen alle "ESRCH → SIGKILL → exec fehlgeschlagen", kein einziges Mal war exec erfolgreich.
Das beweist, dass das Fenster zum Abbruch von exec nach de_thread kontrollierbar und stabil ist; das Fenster stammt vom mprotect des lockers
(60.000 VMAs, einzeln 200–700 ms), die Reaktion des Elternprozesses liegt im µs-Bereich.
de_thread() ruft zap_other_threads() auf und tötet alle
anderen Threads desselben Prozesses; ein im Kindprozess platzierter killer stirbt im Moment von de_thread selbst
und sieht ESRCH nie (äußert sich darin, dass exec jedes Mal erfolgreich ist).SIGKILL zu sprayen ist wirkungslos – zu diesem Zeitpunkt ist der Kindprozess noch nicht tot,
und k_itimer wurde noch nicht per RCU freigegeben. Die richtige Reihenfolge ist
waitid(WNOWAIT) auf den Zombie warten → auf RCU-Callback-Freigabe warten → Seiten schnappen → zuletzt wait4 zum Auslösen des Reclaim-Pfads.Derzeit ist der Schritt "nach der Freigabe ist der Knoten noch in der Queue" stabil erreicht, aber das rb_erase
zum Zeitpunkt von wait4 erzeugt kein oops – das bedeutet, die betroffene Slab-Seite wurde noch nicht von unserem Spray geschnappt (der partial slab
von posix_timers_cache ist noch gecacht, die Seite ist nicht zum Buddy zurückgekehrt). Das gehört zur Reclaim-Orchestrierung des Exploits,
kein Race-Problem; man muss nach der Methode aus den vivo-Notizen vorgehen: zuerst mit k_itimer selbst den Slab füllen/leeren,
um die ganze Seite zurückzugeben, dann mit einem order-0-Träger (AIO ring usw.) die gesplittete halbe Seite präzise abfangen.
Nächster Schritt: Diese Maschine als Basis nehmen und gemäß docs/08-reclaim-choreography.md (vivo-Notizen)
gezieltes Reclaim durchführen; oder mit einem selbstgebauten Kernel mit KASAN verifizieren, ob der UAF tatsächlich auftritt.
Die öffentliche Kette (Honor uid0.c) nutzt kmem-Events von tracefs für zwei entscheidende Dinge: