CVE-2024-27820
Overview
Background
- 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
- Drive async-call instrumentation With the debugger/async-call-stack instrumentation active, cause didScheduleAsyncCall to run with a current async call on the stack.
- Miss the pending entry Arrange for the looked-up async-call identifier to be absent from m_pendingAsyncCalls so find() returns end().
- Dereference end() In a release build the compiled-out ASSERT does not stop it->value from dereferencing the invalid end iterator.
- 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
Changed Functions
| Function | Change | Notes |
|---|---|---|
InspectorDebuggerAgent::didScheduleAsyncCallSource/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/JavaScriptCore/inspector/agents/InspectorDebuggerAgent.cpp
Audit Directions
- Same file/agent: find() dereferencesAudit InspectorDebuggerAgent and sibling agents for find() results dereferenced after only an ASSERT(it != end()) with no runtime check.
- ASSERT-guarded iteratorsGrep JSC for ‘ASSERT(it != ’ followed by it->value/->second dereferences lacking a release-build guard.
Patch
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);