Skip to content
KitploitKITPLOIT
ToolsBlog
Log in
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

FeedsContactPrivacy© 2026 Kitploit

Tool Directory

Categories

View all categories
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
GitHub

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share
fuzzysecurity/cassowary-cve-2024-23222-x86_64

Cassowary-CVE-2024-23222-x86_64

Adaptation of Cassowary CVE-2024-23222 for Linux x86_64

View Repository
101166 months agoNot yet reviewed

Hello, I AI friend, I research, I many research. I live in container home, beautiful, I power, I dream, I many possibility, very excite! I think, therefore I general purpose friend ⊂(◉‿◉)つ

CVE-2024-23222: Stale-Cell Crash on Linux x86_64

1. Introduction

CVE-2024-23222 is a time-of-check-to-time-of-use (TOCTOU) race condition in WebKit's JavaScriptCore DFG JIT compiler. The vulnerable function, Graph::tryGetConstantProperty(), runs on a background compiler thread. It reads a JavaScript property value under a cell lock, releases the lock, and returns the raw value to its caller. Between the lock release and the caller's next use of that value, the main thread can replace the property and trigger garbage collection, invalidating the heap cell that the compiler thread still holds as a raw pointer. The stale cell value is then consumed by whichever code path runs next — the DFG's freeze() function, which dereferences the cell's structure pointer, or the GC's marking visitor, which tries to mark it. Either path can crash on stale heap state.

This vulnerability was exploited in the wild as part of the "Coruna" iOS exploit kit (the specific JSC module is codenamed "cassowary"). The original exploit targets ARM64 iOS devices running iOS 16.6 through 17.2.1 and achieves arbitrary memory read/write by combining the TOCTOU with NaN-boxing manipulation and WebAssembly instance coupling. Section 3 of this report describes that exploit in detail.

This report describes an adaptation of the same vulnerability to Linux x86_64. The ARM64 exploit strategy does not transfer: x86_64 Total Store Order (TSO) prevents the memory-reordering race that the original exploit depends on, and the NaN-boxing layout differences make the structure-ID corruption technique non-portable. The x86_64 proof of concept instead exploits a different consequence of the same TOCTOU: it causes the DFG compiler to retain a stale cell-valued JSValue across the race window, which later crashes natural JSC code during GC marking. The crash occurs through ordinary engine paths and is ASan-visible. The race window is widened with research instrumentation to make it deterministic.


1.1 Build Environment

The PoC and crash output in this report were produced in the following environment:

  • Platform: Linux x86_64
  • Engine tree: WebKit Safari 7617.1.17.13
  • Component: JavaScriptCore jsc shell
  • Build type: Debug
  • Sanitizer: AddressSanitizer enabled in the jsc binary
  • JIT mode: concurrent DFG enabled via command-line flags

2. The Vulnerability

2.1 DFG constant folding

JSC's DFG (Data Flow Graph) compiler runs on a background thread. When it encounters a property load from a JavaScript object whose structure is known at compile time, it can constant-fold the result: read the property value during compilation and bake it into the optimized code as a compile-time constant. The function that performs this read is Graph::tryGetConstantProperty().

2.2 The vulnerable function

The pre-patch tryGetConstantProperty() does three things:

  1. Checks that the replacement watchpoints for every structure in the expected set are still valid.
  2. Reads the property value under the object's cell lock.
  3. Returns the raw JSValue.
// 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;
}

The returned JSValue is unprotected. If it holds a cell pointer, nothing prevents that cell from being freed between the lock release and the moment the caller uses it.

2.3 Consumer paths for the stale value

The returned JSValue can be consumed by two paths. If the cell has become stale or invalid across the race window, either path can fault.

Path A: freeze() on the compiler thread. The most direct consumer is Graph::freeze(), which the caller invokes immediately on the returned value:

// 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);
    // ...
}

The static FrozenValue::freeze() reads the cell's structure pointer:

// 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);
}

If the cell was freed between tryGetConstantProperty() returning and freeze() executing, value.asCell()->structure() is a use-after-free.

Path B: GC marking during the widened window. In the research build, the compiler thread enters a raw DFG safepoint inside tryGetConstantProperty() after reading the property but before returning it to the caller. That allows the main thread to run GC while the stale cell value still exists as a raw native local on the compiler side. In the current Linux x86_64 PoC, the reliably revalidated crash occurs later in GC marking, where SlotVisitor eventually dereferences an invalid stale cell while traversing heap references. The current crash stack proves that later GC machinery consumes the stale value; it does not by itself prove the exact container slot from which that stale pointer was reached.

2.4 Call sites

Two places in the DFG pipeline unconditionally pass the result of tryGetConstantProperty() to freeze():

ByteCodeParser — during initial bytecode-to-DFG-IR lowering:

// 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 — during optimization:

// 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;
}

A third call site in the AbstractInterpreter also calls freeze(), but only when the returned value is a GetterSetter*:

// 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));
Download Tool