CVE-2024-27834
Overview
Background
- Pointer Authentication (PAC) / ARM64E
- An ARM64E mitigation that signs pointers (e.g. return addresses) with a cryptographic MAC keyed by a context/modifier, so forged pointers fail authentication.
- Signing gadget
- A code sequence that signs a pointer using an attacker-controllable modifier register; abused to forge valid PACs for arbitrary pointers.
- pacib vs pacizb / diversifier
- pacib signs with a register-provided modifier; pacizb signs with a zero modifier. Using zero or the stack pointer removes the attacker-controllable input.
Root Cause Analysis
This hardens JavaScriptCore’s ARM64E pointer-authentication (PAC) usage against signing-gadget abuse, closing a PAC bypass usable by an attacker who already has arbitrary read/write. When the JIT reconstructs inlined call frames on OSR exit (reifyInlinedCallFrames in DFGOSRExitCompilerCommon) and fixes up arity (arityFixupGenerator in ThunkGenerators), it must re-sign the return-PC pointers with PAC.
Pre-patch, this re-signing passed an attacker-influenceable TAG/modifier held in a general-purpose register to the signing instruction (pacib): arityFixupGenerator generated a RANDOM tag (tempReturnPCTag = random()) into a register and signed with it, and reifyInlinedCallFrames used general-purpose temp registers (regT2/regT3/regT4) as the signing diversifier. An attacker with arbitrary read/write can hijack the register holding that modifier to turn the JIT’s signing sequence into a PAC signing gadget — signing an arbitrary pointer under a known/controlled context to forge a validly-signed return PC and defeat PAC-protected control flow.
The fix removes the controllable modifier: arityFixupGenerator now signs with a ZERO modifier via pacizb (jit.tagPtr(NoPtrTag, …), with the comment that passing a tag register to pacib could be hijacked into a PAC-bypass gadget), and reifyInlinedCallFrames, when Options::allowNonSPTagging() is false, uses the STACK POINTER as the signing diversifier (signingTagReg = sp) and LR as the return-PC register, carefully saving/restoring sp around the sequence.
The restored invariant is that JIT pointer re-signing uses a diversifier the attacker cannot control (zero, or the stack pointer), so the signing sequences cannot be repurposed to forge PACs. The regression tests exercise arity-fixup and OSR-exit return-PC signing paths.
Attack Path
- Obtain arbitrary read/write Start from a separate primitive giving arbitrary read/write in the WebContent process (the advisory’s stated precondition).
- Reach a JIT signing sequence Drive OSR exit / arity fixup so the JIT executes a return-PC PAC signing sequence.
- Hijack the modifier register Corrupt the general-purpose register holding the PAC tag/modifier (the random tag, or a temp register) that the signing instruction consumes.
- Forge a signed pointer Use the signing sequence as a gadget to sign an attacker-chosen pointer under a known context — a validly-signed return PC.
- Bypass PAC Redirect control flow to the forged pointer, defeating pointer authentication and continuing exploitation.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
reifyInlinedCallFramesSource/JavaScriptCore/dfg/DFGOSRExitCompilerCommon.cpp |
modified | On ARM64E, when allowNonSPTagging() is false, re-signs the return PC using the stack pointer as the diversifier (signingTagReg = sp) and LR as the PC register, saving/restoring sp, instead of using hijackable general-purpose temp registers as the modifier. |
arityFixupGeneratorSource/JavaScriptCore/jit/ThunkGenerators.cpp |
modified | Signs the return PC with a zero modifier (pacizb via tagPtr(NoPtrTag, ...)) instead of a random tag held in a register, so the signing step cannot be hijacked into a PAC-bypass gadget; adds an allowNonSPTagging() SP-based path. |
Files Changed
Source/JavaScriptCore/assembler/MacroAssemblerARM64E.hSource/JavaScriptCore/dfg/DFGOSRExitCompilerCommon.cppSource/JavaScriptCore/jit/ThunkGenerators.cppSource/JavaScriptCore/llint/LLIntThunks.cppSource/JavaScriptCore/runtime/Options.cppSource/JavaScriptCore/runtime/OptionsList.hSource/WTF/wtf/PtrTag.hSource/WebKit/WebProcess/WebProcess.cppTools/Scripts/run-jsc-stress-tests
Audit Directions
- Register-modifier signingGrep JSC ARM64E codegen for tagPtr/pacib sequences that take the modifier from a general-purpose register an attacker could corrupt; prefer NoPtrTag (pacizb) or SP.
- Return-PC re-signing pathsAudit all OSR-exit / thunk / arity-fixup return-PC untag/validate/tag sequences for use of non-SP tagging under allowNonSPTagging().
Patch
diff --git a/Source/JavaScriptCore/assembler/MacroAssemblerARM64E.h b/Source/JavaScriptCore/assembler/MacroAssemblerARM64E.h
index 6925f04bc3d6..3f9233b2de58 100644
--- a/Source/JavaScriptCore/assembler/MacroAssemblerARM64E.h
+++ b/Source/JavaScriptCore/assembler/MacroAssemblerARM64E.h
@@ -65,6 +65,12 @@ class MacroAssemblerARM64E : public MacroAssemblerARM64 {
ALWAYS_INLINE void tagPtr(PtrTag tag, RegisterID target)
{
+ if (!tag) {
+ m_assembler.pacizb(target);
+ return;
+ }
+
+ RELEASE_ASSERT(Options::allowNonSPTagging());
auto tagGPR = getCachedDataTempRegisterIDAndInvalidate();
move(TrustedImm64(tag), tagGPR);
m_assembler.pacib(target, tagGPR);
@@ -76,11 +82,17 @@ class MacroAssemblerARM64E : public MacroAssemblerARM64 {
m_assembler.pacibsp();
return;
}
+ RELEASE_ASSERT(Options::allowNonSPTagging());
m_assembler.pacib(target, tag);
}
ALWAYS_INLINE void untagPtr(PtrTag tag, RegisterID target)
{
+ if (!tag) {
+ m_assembler.autizb(target);
+ return;
+ }
+
auto tagGPR = getCachedDataTempRegisterIDAndInvalidate();
move(TrustedImm64(tag), tagGPR);
m_assembler.autib(target, tagGPR);
diff --git a/Source/JavaScriptCore/dfg/DFGOSRExitCompilerCommon.cpp b/Source/JavaScriptCore/dfg/DFGOSRExitCompilerCommon.cpp
index 4e0d005cc08a..a597735bfaf7 100644
--- a/Source/JavaScriptCore/dfg/DFGOSRExitCompilerCommon.cpp
+++ b/Source/JavaScriptCore/dfg/DFGOSRExitCompilerCommon.cpp
@@ -254,6 +254,16 @@ void reifyInlinedCallFrames(CCallHelpers& jit, const OSRExitBase& exit)
ASSERT(JITCode::isBaselineCode(jit.baselineCodeBlock()->jitType()));
jit.storePtr(AssemblyHelpers::TrustedImmPtr(jit.baselineCodeBlock()), AssemblyHelpers::addressFor(CallFrameSlot::codeBlock));
+ GPRReg returnPCReg = GPRInfo::regT5;
+#if CPU(ARM64E)
+ GPRReg signingTagReg = GPRInfo::regT2;
+ if (!Options::allowNonSPTagging()) {
+ returnPCReg = ARM64Registers::lr;
+ signingTagReg = MacroAssembler::stackPointerRegister;
+ // We could save/restore lr here but we don't need to because the LLInt/Baseline will load it from the stack before returning anyway.
+ }
+#endif
+
const CodeOrigin* codeOrigin;
for (codeOrigin = &exit.m_codeOrigin; codeOrigin && codeOrigin->inlineCallFrame(); codeOrigin = codeOrigin->inlineCallFrame()->getCallerSkippingTailCalls()) {
InlineCallFrame* inlineCallFrame = codeOrigin->inlineCallFrame();
@@ -266,15 +276,25 @@ void reifyInlinedCallFrames(CCallHelpers& jit, const OSRExitBase& exit)
if (!trueCaller) {
ASSERT(inlineCallFrame->isTail());
- jit.loadPtr(AssemblyHelpers::Address(GPRInfo::callFrameRegister, CallFrame::returnPCOffset()), GPRInfo::regT3);
+ jit.loadPtr(AssemblyHelpers::Address(GPRInfo::callFrameRegister, CallFrame::returnPCOffset()), returnPCReg);
#if CPU(ARM64E)
+ if (!Options::allowNonSPTagging()) {
+ JIT_COMMENT(jit, "lldb dynamic execution / posix signals could trash your stack"); // We don't have to worry about signals because they shouldn't fire in WebContent process in this window.
+ jit.move(MacroAssembler::stackPointerRegister, GPRInfo::regT4);
+ }
+
jit.addPtr(AssemblyHelpers::TrustedImm32(sizeof(CallerFrameAndPC)), GPRInfo::callFrameRegister, GPRInfo::regT2);
- jit.untagPtr(GPRInfo::regT2, GPRInfo::regT3);
- jit.addPtr(AssemblyHelpers::TrustedImm32(inlineCallFrame->returnPCOffset() + sizeof(void*)), GPRInfo::callFrameRegister, GPRInfo::regT2);
- jit.validateUntaggedPtr(GPRInfo::regT3, GPRInfo::regT4);
- jit.tagPtr(GPRInfo::regT2, GPRInfo::regT3);
+ jit.untagPtr(GPRInfo::regT2, returnPCReg);
+ jit.validateUntaggedPtr(returnPCReg, GPRInfo::regT2);
+ jit.addPtr(AssemblyHelpers::TrustedImm32(inlineCallFrame->returnPCOffset() + sizeof(CPURegister)), GPRInfo::callFrameRegister, signingTagReg);
+ jit.tagPtr(signingTagReg, returnPCReg);
+
+ if (!Options::allowNonSPTagging()) {
+ JIT_COMMENT(jit, "lldb dynamic execution / posix signals are ok again");
+ jit.move(GPRInfo::regT4, MacroAssembler::stackPointerRegister);
+ }
#endif
- jit.storePtr(GPRInfo::regT3, AssemblyHelpers::addressForByteOffset(inlineCallFrame->returnPCOffset()));
+ jit.storePtr(returnPCReg, AssemblyHelpers::addressForByteOffset(inlineCallFrame->returnPCOffset()));
jit.loadPtr(AssemblyHelpers::Address(GPRInfo::callFrameRegister, CallFrame::callerFrameOffset()), GPRInfo::regT3);
callerFrameGPR = GPRInfo::regT3;
} else {
@@ -291,10 +311,20 @@ void reifyInlinedCallFrames(CCallHelpers& jit, const OSRExitBase& exit)
}
#if CPU(ARM64E)
- jit.addPtr(AssemblyHelpers::TrustedImm32(inlineCallFrame->returnPCOffset() + sizeof(void*)), GPRInfo::callFrameRegister, GPRInfo::regT2);
- jit.move(AssemblyHelpers::TrustedImmPtr(jumpTarget.untaggedPtr()), GPRInfo::regT4);
- jit.tagPtr(GPRInfo::regT2, GPRInfo::regT4);
- jit.storePtr(GPRInfo::regT4, AssemblyHelpers::addressForByteOffset(inlineCallFrame->returnPCOffset()));
+ if (!Options::allowNonSPTagging()) {
+ JIT_COMMENT(jit, "lldb dynamic execution / posix signals could trash your stack"); // We don't have to worry about signals because they shouldn't fire in WebContent process in this window.
+ jit.move(MacroAssembler::stackPointerRegister, GPRInfo::regT4);
+ }