CVE-2024-27808
Overview
Background
- AccessCase / inline cache
- AccessCase is per-case IC feedback for a property access; polymorphic ICs are compiled into a JITStubRoutine.
- CallLinkInfo
- Compiled-code call-site metadata owned by the JITStubRoutine; its lifetime ends when that routine is destroyed.
- visitWeak / delayed CodeBlock destruction
- GC weak-visiting walks IC structures; when CodeBlock destruction is delayed, a raw CallLinkInfo* cached in AccessCase can outlive the freed routine.
Root Cause Analysis
This fixes a use-after-free in JavaScriptCore’s inline caches by decoupling CallLinkInfo from AccessCase (per the commit, ‘AccessCase should not hold CallLinkInfo*’). An AccessCase is inline-cache (IC) feedback data describing one property-access case; for cases that call out (Getter/Setter and the ProxyObject cases) it previously held a raw CallLinkInfo* — a data structure that actually belongs to the compiled JITStubRoutine. CallLinkInfo’s lifetime is tied to that stub routine, so when the JITStubRoutine is destroyed the AccessCase’s CallLinkInfo* becomes dangling. This was previously masked by a strict destruction ordering (CodeBlock was always destroyed synchronously first, then the JITStubRoutine cleaned up), but CodeBlock destruction can now be DELAYED, breaking the ordering.
As a result, code that walked AccessCase’s dependent cells and weak references during garbage collection — AccessCase::forEachDependentCell, doesCalls (which marked callLinkInfo dependent cells), and visitWeak (which called accessor.callLinkInfo()->visitWeak) — could dereference a freed CallLinkInfo, a use-after-free during GC/marking that corrupts engine state.
The fix removes CallLinkInfo* from AccessCase entirely (deleting the callLinkInfo handling from forEachDependentCell/doesCalls/visitWeak) and moves CallLinkInfo lifetime management to where the pointer actually lives: MarkingGCAwareJITStubRoutine now performs the visitWeak iteration over its CallLinkInfos (visitWeakImpl / callLinkInfoAtImpl), and the InlineCacheHandler / StructureStubInfo expose callLinkInfoAt so callers reach the CallLinkInfo through the live stub routine rather than a cached raw pointer.
The restored invariant is that CallLinkInfo is owned and weak-visited by the compiled stub routine, never held as a raw pointer by the IC-feedback AccessCase whose lifetime can outlast it. The analysis is grounded in the commit message and the shown deletions; the corresponding additions in MarkingGCAwareJITStubRoutine are described by the commit though not fully shown in the diff hunks.
Attack Path
- Build polymorphic inline caches Run JS that creates Getter/Setter or Proxy-object inline caches, so AccessCases hold CallLinkInfo tied to a JITStubRoutine.
- Delay CodeBlock destruction Arrange conditions where CodeBlock destruction is delayed relative to the JITStubRoutine cleanup, so the CallLinkInfo is freed while an AccessCase still references it.
- Trigger GC weak visiting Cause garbage collection so forEachDependentCell/doesCalls/visitWeak walk the AccessCase’s now-dangling CallLinkInfo*.
- Use-after-free The freed CallLinkInfo is dereferenced during marking/weak-visiting, a UAF the attacker grooms toward arbitrary read/write and code execution in WebContent.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
AccessCase::forEachDependentCell / doesCalls / visitWeakSource/JavaScriptCore/bytecode/AccessCase.cpp |
modified | Removes all handling of a held CallLinkInfo* (dependent-cell marking and visitWeak), and drops the cellsToMarkIfDoesCalls path, so AccessCase no longer dereferences a CallLinkInfo whose lifetime it does not own. |
MarkingGCAwareJITStubRoutine::visitWeakImpl / callLinkInfoAtImplSource/JavaScriptCore/jit/GCAwareJITStubRoutine.cpp |
modified | Takes over weak-visiting of the CallLinkInfos it owns, so their lifetime is managed by the compiled stub routine (per the commit message). |
InlineCacheHandler::callLinkInfoAt / StructureStubInfo::callLinkInfoAtSource/JavaScriptCore/bytecode/StructureStubInfo.cpp |
modified | Expose the CallLinkInfo via the live handler/stub routine so callers no longer rely on a raw pointer cached in AccessCase. |
Files Changed
JSTests/stress/decouple-calllinkinfo-from-access-case.jsSource/JavaScriptCore/bytecode/AccessCase.cppSource/JavaScriptCore/bytecode/AccessCase.hSource/JavaScriptCore/bytecode/GetByStatus.cppSource/JavaScriptCore/bytecode/GetterSetterAccessCase.cppSource/JavaScriptCore/bytecode/GetterSetterAccessCase.hSource/JavaScriptCore/bytecode/InlineCacheCompiler.cppSource/JavaScriptCore/bytecode/InlineCacheCompiler.hSource/JavaScriptCore/bytecode/ProxyObjectAccessCase.cppSource/JavaScriptCore/bytecode/ProxyObjectAccessCase.hSource/JavaScriptCore/bytecode/PutByStatus.cppSource/JavaScriptCore/bytecode/StructureStubInfo.cppSource/JavaScriptCore/bytecode/StructureStubInfo.hSource/JavaScriptCore/jit/GCAwareJITStubRoutine.cppSource/JavaScriptCore/jit/GCAwareJITStubRoutine.hSource/JavaScriptCore/jit/JITStubRoutine.cppSource/JavaScriptCore/jit/JITStubRoutine.h
Audit Directions
- Raw compiled-code pointers in feedback dataAudit AccessCase and other IC-feedback structures for raw pointers into compiled code (CallLinkInfo, stub routines) whose lifetime they do not own.
- Destruction-ordering assumptionsGrep JSC for lifetime assumptions that CodeBlock is destroyed before its JITStubRoutines; delayed destruction can violate them.
Patch
diff --git a/JSTests/stress/decouple-calllinkinfo-from-access-case.js b/JSTests/stress/decouple-calllinkinfo-from-access-case.js
new file mode 100644
index 000000000000..a7fd1f75b1ab
--- /dev/null
+++ b/JSTests/stress/decouple-calllinkinfo-from-access-case.js
@@ -0,0 +1,96 @@
+// runDefault("--validateOptions=true", "--thresholdForJITSoon=10", "--thresholdForJITAfterWarmUp=10", "--thresholdForOptimizeAfterWarmUp=100", "--thresholdForOptimizeAfterLongWarmUp=100", "--thresholdForOptimizeSoon=100", "--thresholdForFTLOptimizeAfterWarmUp=1000", "--thresholdForFTLOptimizeSoon=1000", "--validateBCE=true")
+
+const ProxyConstructor = Proxy;
+const getPrototypeOf = Object.getPrototypeOf;
+const ReflectGet = Reflect.get;
+const ReflectSet = Reflect.set;
+const ReflectHas = Reflect.has;
+const setPrototypeOf = Object.setPrototypeOf;
+
+function probe(id, value) {
+ let originalPrototype, newPrototype;
+ let handler = {
+ get(target, key, receiver) {
+ if (key === '__proto__' && receiver === value) return originalPrototype;
+ if (receiver === newPrototype) return ReflectGet(target, key);
+ return ReflectGet(target, key, receiver);
+ },
+ set(target, key, value, receiver) {
+ if (receiver === newPrototype) return ReflectSet(target, key, value);
+ return ReflectSet(target, key, value, receiver);
+ },
+ has(target, key) {
+ return ReflectHas(target, key);
+ },
+ };
+
+ try {
+ originalPrototype = getPrototypeOf(value);
+ newPrototype = new ProxyConstructor(originalPrototype, handler);
+ setPrototypeOf(value, newPrototype);
+ } catch (e) {}
+}
+
+probe("v1", "2003629588");
+let v4 = 9150;
+v4--;
+probe("v6", 51828);
+function F7(a9, a10, a11) {
+ if (!new.target) { throw 'must be called with new'; }
+ const v12 = this?.constructor;
+ try { new v12(this, "object", 447824390); } catch (e) {}
+ a11 % a11;
+ this.b = a9;
+ this.g = a10;
+}
+const v15 = new F7("2003629588", "object", 447824390);
+const v16 = new F7(v15, v4, v4);
+const v17 = new F7("2003629588", 51828, 51828);
+probe("v17", v17);
+const v18 = v17?.constructor;
+probe("v18", v18);
+let v19;
+try { v19 = new v18("r", v17, "r"); } catch (e) {}
+probe("v19", v19);
+const v20 = [v17,v17];
+probe("v20", v20);
+const v21 = [F7,v15,v20,v15,v4];
+const v22 = [v4,"object",51828];
+probe("v22", v22);
+let v23;
+try { v23 = v22.reduce(v15); } catch (e) {}
+const v24 = [2,-354747782,-16,10251,-1485280459,5,6,536870888,-47153,-193790246];
+probe("v24", v24);
+function f25(a26, a27) {
+ const o28 = {
+ [a27]: a26,
+ "d": v21,
+ };
+ return o28;
+}
+f25(v16, v22);
+f25(v23, v16);
+f25(v15, v22);
+v24[4];
+function f33(a34, a35, a36, a37) {
+ probe("v36", a36);
+ ~a35;
+ v22.length = 1;
+ a36?.[v21];
+}
+v24.flatMap(f33);
+gc();
+class C20 {
+ valueOf(a22, a23) {
+ return ("n")[1204] - this;
+ }
+}
+const v26 = new C20();
+function f27(a28, a29) {
+ new BigInt64Array(3603);
+ return v26 * v26;
+}
+try {
+v26[Symbol.toPrimitive] = f27;