
CVE-2025-43529 के लिए तकनीकी एक्सप्लॉइट, WebKit DFG JIT कंपाइलर भेद्यता जो समवर्ती GC में लापता स्टोर बैरियर के माध्यम से उपयोग-के-बाद-मुक्त सक्षम करती है, iOS और macOS के लिए पूर्ण शोषण प्राइमिटिव के साथ।
Apple ने हाल ही में iOS 26.2 और iPadOS 26.2 जारी किया है, साथ ही एक सुरक्षा सलाहकार भी जिसमें WebKit कमजोरियों के लिए सुधार शामिल हैं। DFG JIT कंपाइलर में एक बग (CVE-2025-43529) सबसे अलग था, इसलिए मैंने इसे खंगालने का फैसला किया।
JIT कंपाइलर ने सही ढंग से पहचाना कि एक Phi नोड (जहाँ कई नियंत्रण-प्रवाह पथ मिलते हैं) बच गया था, लेकिन वह यह नोटिस करने में विफल रहा कि Phi के Upsilon नोड भी बच गए थे। इस वजह से, DFG संकलन StoreBarrierInsertionPhase ने Store Barrier सम्मिलित करना छोड़ दिया, जो एक महत्वपूर्ण मेमोरी-सुरक्षा तंत्र है। परिणामस्वरूप, समवर्ती GC उन वस्तुओं को मिस कर सकता है जिन्हें उसे स्कैन करना चाहिए था, जो उपयोग-के-बाद-मुक्त (use-after-free) का कारण बन सकता है।
आप पैच कमिट यहाँ पा सकते हैं।
मैंने पुष्टि की है कि मेरा शोषण iOS 26.1, iPadOS 26.1 और macOS Tahoe 26.0.1 पर काम करता है।
JSC हीप को कुशलता से प्रबंधित करने के लिए जनरेशनल GC मॉडल का उपयोग करता है। इस मॉडल में, मेमोरी को वस्तु की आयु के आधार पर Eden (नया स्थान) और पुराने स्थान में विभाजित किया जाता है। सभी नव-आवंटित वस्तुएँ Eden में शुरू होती हैं। जब Eden भर जाता है, तो एक Eden GC ट्रिगर होता है और कोई भी जीवित वस्तु पुराने स्थान पर प्रवर्तित हो जाती है। पुराने स्थान की वस्तुओं को साफ करने के लिए पूर्ण GC की आवश्यकता होती है।
जनरेशनल GC को कार्य करने के लिए, GC को वस्तुओं को "पहले से स्कैन किया गया", "स्कैन करने की आवश्यकता है" या "पुनः स्कैन करने की आवश्यकता है" के रूप में वर्गीकृत करने की आवश्यकता है। JSC में, इसे किसी वस्तु के cellState का उपयोग करके ट्रैक किया जाता है। (सभी GC-प्रबंधित वस्तुएँ JSCell से प्राप्त होती हैं।)
StructureID m_structureID;
union {
uint32_t m_blob;
struct {
IndexingType m_indexingTypeAndMisc;
JSType m_type;
TypeInfo::InlineTypeFlags m_flags;
CellState m_cellState;
};
};
cellState 1 बाइट है और तीन रंगों में से एक हो सकता है: Black (0), White (1), और Grey (2)।
White का अर्थ है एक ऐसी वस्तु जिसे अभी-अभी Eden में आवंटित किया गया है। वर्तमान GC चक्र में, इसे अभी तक चिह्नित नहीं किया गया है। यदि यह GC चक्र के अंत तक इसी अवस्था में रहता है, तो वस्तु एकत्रित हो जाएगी।
Black का अर्थ है कि GC ने पहले ही वस्तु को चिह्नित करना समाप्त कर दिया है या चिह्नित करने की प्रक्रिया में है। मूलतः इसे जीवित माना जाता है, हालाँकि isMarked बिट अभी भी बंद हो सकता है।
Grey का अर्थ है एक ऐसी वस्तु जिसे अभी भी स्कैन करने की आवश्यकता है। अधिक सटीक रूप से, यह मूल रूप से Black थी, लेकिन यह write barrier द्वारा पकड़ी गई और याद किए गए सेट में जोड़ दी गई। दूसरे शब्दों में, इसके संदर्भ बदल गए, इसलिए GC को इसे फिर से स्कैन करने की आवश्यकता है।
JSC में समवर्ती GC भी है, जो मेमोरी को पुनः प्राप्त किए जाने के दौरान एप्लिकेशन को चलने देता है। यदि GC पृष्ठभूमि में किसी वस्तु को चिह्नित कर रहा है और एप्लिकेशन उसी समय उस वस्तु की स्थिति बदल देता है, तो आप रेस की स्थिति में पहुँच सकते हैं।
इसे रोकने के लिए, आपको ऑर्डरिंग गारंटी की आवश्यकता है। जैसे "लिखना (स्टोर) A, फिर पढ़ना (लोड) B" उसी सटीक क्रम में होना। लेकिन ARM64 पर, प्रदर्शन कारणों से, CPU मेमोरी ऑपरेशन को पुनर्क्रमित कर सकता है। इसका मतलब है कि GC गलत मान पढ़ सकता है।
इसलिए JSC ऑर्डरिंग लागू करने के लिए CPU के डेटा डिपेंडेंसी पर निर्भर डिपेंडेंसी क्लास या विशेष ARM64 निर्देश जैसे STLR और LDAR का उपयोग करता है। STLR यह सुनिश्चित करता है कि पहले के रीड/राइट स्टोर से पहले दिखाई दें (एक रिलीज़ स्टोर)। LDAR यह सुनिश्चित करता है कि बाद के रीड/राइट लोड से आगे न बढ़ सकें (एक अधिग्रहण लोड)। जब कोई अन्य थ्रेड किसी वस्तु को पढ़ता है, तो LDAR को STLR के साथ जोड़ा जाता है ताकि वह सुरक्षित रूप से सबसे हाल के डेटा का निरीक्षण कर सके।
DMB एक एकल विशेष निर्देश नहीं है। यह एक बैरियर है जो इसके आस-पास सभी मेमोरी एक्सेस में ऑर्डरिंग लागू करता है। यह सुनिश्चित करता है कि DMB से पहले के मेमोरी ऑपरेशन इसके बाद के ऑपरेशन से पहले दिखाई दें।
JSC में कुल तीन JIT टियर हैं। निष्पादन गति और संकलन लागत (मेमोरी/समय) को संतुलित करने के लिए, यह अनुकूलन लागू करता है और कोड को इसके चलने की आवृत्ति के आधार पर अगले टियर पर ले जाता है।
Baseline JIT पहला JIT कंपाइलर है। यह कम संकलन ओवरहेड के साथ तेज़ी से नेटिव कोड प्राप्त करने पर ध्यान केंद्रित करता है। DFG JIT Baseline JIT के बाद का अगला चरण है, जहाँ गंभीर अनुकूलन शुरू होता है।
DFG टियर पर, जावास्क्रिप्ट निर्देशों को DFG IR नोड्स से बने ग्राफ में परिवर्तित किया जाता है। इसके द्वारा एकत्रित प्रकार की जानकारी का उपयोग करके, कंपाइलर अनावश्यक संचालन को हटाने के लिए अनुमान (speculation) लगाता है।
JSC के DFG अनुकूलन पाइपलाइन में, StoreBarrierInsertionPhase मेमोरी पर लिखने वाले नोड्स के बाद, जैसे PutByOffset, एक StoreBarrier सम्मिलित करता है।
CVE-2025-43529 एक कमजोरी है जो StoreBarrierInsertionPhase के दौरान StoreBarrier को सम्मिलित करने में विफलता के कारण होती है, जब इसे सम्मिलित किया जाना चाहिए था।
StoreBarrier एक नोड है जो write barrier की तरह कार्य करता है। इसका उपयोग मार्किंग थ्रेड के साथ रेस में शुद्धता बनाए रखने के लिए किया जाता है।
पैच कमिट में वर्णित कमजोर DFG नोड परिदृश्य इस प्रकार दिखता है:
BB#1
a: NewObject
b: NewObject
...
c: Upsilon(@b, ^f)
Branch(BB#2, BB#3)
BB#2
...
d: Something
e: Upsilon(@d, ^f)
Jump(BB#3)
BB#3
f: Phi(@c, @e)
...
g: PutByOffset(@a, @f)
...
h: PutByOffset(@b, ...)
...
BB#1 में, दो नई वस्तुएँ बनाई जाती हैं, और निष्पादन या तो BB#2 या BB#3 पर शाखा करता है। फिर BB#2 BB#3 में गिर जाता है। BB#3 में दिलचस्प हिस्सा Phi नोड है। इसका मतलब है कि f या तो c या e है, और यह विकल्प अपस्ट्रीम Upsilon नोड्स द्वारा निर्धारित किया जाता है। BB#3 में, PutByOffset का अर्थ है किसी वस्तु के गुण में मान जोड़ना।
let A = { p0: 0x41414141 };
function jitme(flag) {
// BB#1
let a = { p0: 13.37 };
let b = { p0: 0x42424242 };
let f;
if (flag) {
// BB#2
f = b;
} else {
// BB#3
f = 1.1; // d
}
// BB#4
A.p0 = f;
b.p0 = a;
}
यदि आप --dumpFTLDisassembly=true विकल्प का उपयोग करते हैं, तो आप FTL संकलन के बाद असेंबली का निरीक्षण कर सकते हैं।
// Starting BB#3
0 3 60: D@46:< 1:-> Phi(JS|PureInt, Final|NonIntAsDouble, W:SideState, bc#50, ExitInvalid)
1 3 60: D@53:<!0:-> ZombieHint(Check:Untyped:D@79, MustGen, loc5, W:SideState, ClobbersExit, bc#50, ExitInvalid)
2 3 60: D@57:<!0:-> ExitOK(MustGen, W:SideState, bc#50, ExitValid)
3 3 60: D@63:<!0:-> KillStack(MustGen, loc8, W:Stack(loc8), ClobbersExit, bc#50, ExitValid)
4 3 60: D@60:<!0:-> ZombieHint(Check:Untyped:D@79, MustGen, loc8, W:SideState, ClobbersExit, bc#50, ExitInvalid)
5 3 60: D@67:<!0:-> KillStack(MustGen, loc9, W:Stack(loc9), ClobbersExit, bc#57, ExitValid)
6 3 60: D@66:<!0:-> ZombieHint(Check:Untyped:Kill:D@79, MustGen, loc9, W:SideState, ClobbersExit, bc#57, ExitInvalid)
7 3 60: D@69:<!0:-> FilterPutByStatus(Check:Untyped:D@65, MustGen, (Simple, <id='uid:(p0)'Replace: [0x301006550:[0x1006550/16803152, Object, (1/2, 0/0){p0:0}, NonArray, PropertyAddition, Proto:0x108008488, Leaf (Watched)]], offset = 0, viaGlobalProxy = false>), W:SideState, bc#66, ExitValid)
// D@65 : A
// D@46 : f
// A.p0 = f
8 3 60: D@70:<!0:-> PutByOffset(KnownCell:D@65, KnownCell:D@65, Check:Untyped:Kill:D@46, MustGen, id0{p0}, 0, W:NamedProperties(0), ClobbersExit, bc#66, ExitValid)
// StoreBarrier for D@65(A)
9 3 60: D@78:<!0:-> FencedStoreBarrier(Check:KnownCell:Kill:D@65, MustGen, R:Heap, W:JSCell_cellState, bc#66, ExitInvalid)
10 3 60: D@73:<!0:-> FilterPutByStatus(Check:Untyped:D@36, MustGen, (Simple, <id='uid:(p0)'Replace: [0x301006550:[0x1006550/16803152, Object, (1/2, 0/0){p0:0}, NonArray, PropertyAddition, Proto:0x108008488, Leaf (Watched)]], offset = 0, viaGlobalProxy = false>), W:SideState, bc#72, ExitValid)