Released May 13, 2024

CVE-2024-27820

CVSS 8.8 severity high CWE-119 vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

Overview

Impact:

Description: Processing web content may lead to arbitrary code execution

Researchers: Jeff Johnson of underpassapp.com

Key insight. A HashMap lookup was only ASSERTed non-end and then dereferenced, so in release builds a missing async-call entry dereferenced the end() iterator; a runtime it != end() guard fixes it.

Patch Details

diff --git a/Source/JavaScriptCore/inspector/agents/InspectorDebuggerAgent.cpp b/Source/JavaScriptCore/inspector/agents/InspectorDebuggerAgent.cpp
index 24352a97dc76..e46f788274a8 100644
--- a/Source/JavaScriptCore/inspector/agents/InspectorDebuggerAgent.cpp
+++ b/Source/JavaScriptCore/inspector/agents/InspectorDebuggerAgent.cpp
@@ -444,7 +444,8 @@ void InspectorDebuggerAgent::didScheduleAsyncCall(JSC::JSGlobalObject* globalObj
     if (!m_currentAsyncCallIdentifierStack.isEmpty()) {
         auto it = m_pendingAsyncCalls.find(m_currentAsyncCallIdentifierStack.last());
         ASSERT(it != m_pendingAsyncCalls.end());
-        parentStackTrace = it->value;
+        if (LIKELY(it != m_pendingAsyncCalls.end()))
+            parentStackTrace = it->value;
     }
 
     auto identifier = asyncCallIdentifier(asyncCallType, callbackId);

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2024-27820
Bugzilla 270139
Component JSC
Bug Class OOB
Severity critical
Commit 6a341af34a111bdf…
Advisory Apple Advisory

Terminology

  • Web Inspector debugger agent — JSC instrumentation that tracks async call stacks for debugging; didScheduleAsyncCall records parent traces.
  • HashMap end() iterator — find() returns end() when a key is absent; dereferencing end() reads invalid memory.
  • ASSERT vs release — ASSERTs are compiled out of release builds, so an assertion is not a substitute for a runtime iterator check.

Root Cause Analysis

This fixes an unchecked hash-map iterator dereference in JavaScriptCore’s Web Inspector debugger agent. InspectorDebuggerAgent::didScheduleAsyncCall records async call stack traces; when there is a current async call on the stack it looks up the parent: auto it = m_pendingAsyncCalls.find(m_currentAsyncCallIdentifierStack.last()); ASSERT(it != m_pendingAsyncCalls.end()); parentStackTrace = it->value;. The ASSERT documents the expectation that the entry exists, but assertions are compiled out in release builds, so if the identifier is not actually present, find() returns the end() iterator and it->value dereferences that invalid end iterator — an out-of-bounds read of a HashMap bucket (reading and using garbage as the parent stack trace). The fix guards the dereference with if (LIKELY(it != m_pendingAsyncCalls.end())) parentStackTrace = it->value;, so a missing entry is handled instead of dereferenced. The restored invariant is that the iterator is validated before use in release builds, not only asserted. NOTE: this code lives in the inspector/automation debugger agent, so reachability from ordinary web content depends on that instrumentation being active; the diff establishes the defensive iterator fix, while the advisory’s arbitrary-code-execution rating implies a stronger path that this one-file change does not fully show (marked as inference).

Attack Path

1. Drive async-call instrumentation

With the debugger/async-call-stack instrumentation active, cause didScheduleAsyncCall to run with a current async call on the stack.

2. Miss the pending entry

Arrange for the looked-up async-call identifier to be absent from m_pendingAsyncCalls so find() returns end().

3. Dereference end()

In a release build the compiled-out ASSERT does not stop it->value from dereferencing the invalid end iterator.

4. Read out of bounds

Garbage read as the parent stack trace crashes or corrupts state (advisory: code execution; escalation not shown in this diff).

Impact Assessment

An out-of-bounds read from an unchecked end() iterator in the WebContent process’s inspector agent; the advisory rates it arbitrary code execution. The shown change is a defensive iterator guard, so the demonstrated primitive is an OOB read; a fuller exploit path is not evident in this one-file diff and reachability depends on async-call instrumentation being active.

Audit Directions

Same file/agent: find() dereferences

Audit InspectorDebuggerAgent and sibling agents for find() results dereferenced after only an ASSERT(it != end()) with no runtime check.

ASSERT-guarded iterators

Grep JSC for ‘ASSERT(it != ’ followed by it->value/->second dereferences lacking a release-build guard.

Changed Functions

Function File Change Note
InspectorDebuggerAgent::didScheduleAsyncCall Source/JavaScriptCore/inspector/agents/InspectorDebuggerAgent.cpp modified Guards the parent-stack-trace lookup with if (LIKELY(it != m_pendingAsyncCalls.end())) before dereferencing, so a missing entry is not read as an end() iterator in release builds.

Files Changed

Source Files

  • Source/JavaScriptCore/inspector/agents/InspectorDebuggerAgent.cpp