flawopen.com/Teardowns/cve-2024-0582-linux-io-uring-privilege-escalation
CVE-2024-0582: Linux io_uring की मैप की गई बफ़र रिंग में यूज़-आफ़्टर-फ़्री
CVE-2024-0582 (CVSS 7.8, उच्च): IORING_REGISTER_PBUF_RING और IOU_PBUF_RING_MMAP से रजिस्टर की गई बफ़र रिंग को टियरडाउन के दौरान तब मुक्त कर दिया जाता था जब यूज़र स्पेस अब भी उसे mmap() से मैप किए हुए था, जिससे एक ऐसा पेज बचा रहता था जिसे कर्नेल के दोबारा इस्तेमाल करने के बाद भी हमलावर पढ़-लिख सकता था — लोकल प्रिविलेज एस्केलेशन।
एक पुस्तकालय आपको एक खास पठन-कक्ष रजिस्टर करने देता है और आने-जाने के लिए एक चाबी देता है। जब आप कक्ष का रजिस्ट्रेशन रद्द करते हैं, तो लाइब्रेरियन उसे तुरंत वापस उपलब्ध सूची में डाल देता है, पर आपकी चाबी कभी वापस नहीं लेता। फिर पुस्तकालय वही कक्ष किसी और को दे देता है, और आप अब भी अपनी पुरानी चाबी से अंदर जाकर उसकी मेज़ पर छोड़ी चीज़ों को दोबारा लिख सकते हैं। सुधार यह है कि ऐसे हर कक्ष को आरक्षित रखा जाए और उसे सिर्फ़ एक बार, बंद होने के समय, सारी चाबियाँ लौटने के बाद ही मुक्त किया जाए।
इस पेज के मुख्य शब्द
io_uring- Linux का एसिंक्रोनस I/O इंटरफ़ेस। ऐप और कर्नेल सबमिशन और कम्प्लीशन रिंग साझा करते हैं, जिससे हर ऑपरेशन पर एक सिस्टम कॉल से बचा जाता है।
प्रदान की गई बफ़र रिंग (IORING_REGISTER_PBUF_RING)- बफ़रों की एक रिंग जिसे ऐप रजिस्टर करता है ताकि कर्नेल हर ऑपरेशन के लिए एक चुन सके।
IOU_PBUF_RING_MMAPके साथ कर्नेल रिंग आवंटित करता है और ऐप अपनी मेमोरी देने के बजाय उसेmmap()से मैप करता है। यूज़-आफ़्टर-फ़्री (UAF)- मेमोरी को मुक्त होने के बाद इस्तेमाल करना। जब कर्नेल उस मेमोरी को किसी और ऑब्जेक्ट को दे देता है, तो बासी संदर्भ उसे पढ़ या बिगाड़ सकता है।
remap_pfn_range / folio_put- रिंग को
remap_pfn_range()से यूज़र स्पेस में मैप किया जाता है, इसलिए सामान्य munmap लेखांकन उसे मुक्त नहीं करता। टियरडाउन परfolio_put()कॉल करने से मैपिंग के जीवित रहते ही पेज मुक्त हो जाता था।
फ्रेमवर्क मैपिंग
- इस गाइड में बताई गई कमजोरी के लिए CWE संदर्भ: CWE-416
मूल कारण विश्लेषण (Root Cause)
IOU_PBUF_RING_MMAP से बनी बफ़र रिंग के लिए io_alloc_pbuf_ring() पेज आवंटित करता था और यूज़र स्पेस उन्हें remap_pfn_range() के ज़रिए mmap() से मैप करता था। चूँकि यह रास्ता सामान्य munmap/release लेखांकन को छोड़ देता है, __io_remove_buffers() का folio_put() तब पेज मुक्त कर देता था जब यूज़र मैपिंग अब भी मौजूद हो सकती थी — यह यूज़-आफ़्टर-फ़्री (CWE-416) है: यूज़र स्पेस उस पेज की एक वैध मैपिंग रखे रहता था जिसे कर्नेल आवंटक को लौटा चुका था और दोबारा आवंटित कर सकता था। इसे Jann Horn ने रिपोर्ट किया। कमिट c392cbecd8ec हर mmap-आवंटित रिंग को एक नई io_buf_list में दर्ज करता है और उसे केवल ->release() पर io_kbuf_mmap_list_free() में, मैपिंग हट जाने के बाद, मुक्त करता है।
हमले का चरण-दर-चरण प्रवाह
mmap-समर्थित रिंग रजिस्टर करना
एक अप्रिविलेज्ड प्रोसेस IORING_REGISTER_PBUF_RING और IOU_PBUF_RING_MMAP फ़्लैग के साथ io_uring_register() कॉल करती है, जिससे कर्नेल बफ़र रिंग आवंटित करता है।
उसे यूज़र स्पेस में मैप करना
प्रोसेस रिंग को mmap() करती है। कर्नेल remap_pfn_range() से पेज मैप करता है, और प्रोसेस मैपिंग बनाए रखती है।
मैप रहते हुए रिंग मुक्त करना
बफ़रों का रजिस्ट्रेशन रद्द करना (या रिंग टियरडाउन) __io_remove_buffers() तक पहुँचता है, जिसका folio_put() यूज़र मैपिंग के जीवित रहते ही पेज मुक्त कर देता है।
बासी मैपिंग से लिखना
कर्नेल मुक्त पेज को किसी और ऑब्जेक्ट को दे देता है; मैपिंग अब भी रखे प्रोसेस उसे पढ़कर और अधिलेखित करके कर्नेल स्थिति बिगाड़ती है और root तक बढ़ जाती है।
सोर्स कोड: कमज़ोर बनाम सुरक्षित कार्यान्वयन
/* io_uring/kbuf.c सुधार से पहले (Linux 6.6.4) */
static int io_alloc_pbuf_ring(struct io_uring_buf_reg *reg,
struct io_buffer_list *bl)
{
gfp_t gfp = GFP_KERNEL_ACCOUNT | __GFP_ZERO | __GFP_NOWARN | __GFP_COMP;
size_t ring_size;
void *ptr;
ring_size = reg->ring_entries * sizeof(struct io_uring_buf_ring);
ptr = (void *) __get_free_pages(gfp, get_order(ring_size));
if (!ptr)
return -ENOMEM;
bl->buf_ring = ptr;
bl->is_mapped = 1;
bl->is_mmap = 1;
return 0;
}
static int __io_remove_buffers(struct io_ring_ctx *ctx,
struct io_buffer_list *bl, unsigned nbufs)
{
if (bl->is_mapped) {
i = bl->buf_ring->tail - bl->head;
if (bl->is_mmap) {
/* BUG: ring को mmap() और
* remap_pfn_range() के ज़रिए user space को दिया गया था। यहाँ folio_put() page को छोड़ देता है, जबकि
* mapping अभी मौजूद हो सकती है; इसलिए user space लिखता रहता है
* ऐसे kernel page पर जिसे मुक्त करके फिर से allocate किया गया है। */
folio_put(virt_to_folio(bl->buf_ring));
bl->buf_ring = NULL;
bl->is_mmap = 0;
}
}
/* ... */
}
/* io_uring/kbuf.c commit c392cbecd8ec के बाद (Linux 6.6.5 / 6.7) */
struct io_buf_free {
struct hlist_node list;
void *mem;
};
static int io_alloc_pbuf_ring(struct io_ring_ctx *ctx,
struct io_uring_buf_reg *reg,
struct io_buffer_list *bl)
{
struct io_buf_free *ibf;
size_t ring_size;
void *ptr;
ring_size = reg->ring_entries * sizeof(struct io_uring_buf_ring);
ptr = io_mem_alloc(ring_size);
if (!ptr)
return -ENOMEM;
/* FIX: हर mmap-able ring को याद रखें, ताकि उसके pages केवल
* ->release() पर, mapping हटने के बाद मुक्त हों; teardown के दौरान कभी नहीं। */
ibf = kmalloc(sizeof(*ibf), GFP_KERNEL_ACCOUNT);
if (!ibf) {
io_mem_free(ptr);
return -ENOMEM;
}
ibf->mem = ptr;
hlist_add_head(&ibf->list, &ctx->io_buf_list);
bl->buf_ring = ptr;
bl->is_mapped = 1;
bl->is_mmap = 1;
return 0;
}
static int __io_remove_buffers(struct io_ring_ctx *ctx,
struct io_buffer_list *bl, unsigned nbufs)
{
if (bl->is_mapped) {
i = bl->buf_ring->tail - bl->head;
if (bl->is_mmap) {
/* io_kbuf_mmap_list_free() ->release() पर page(s) मुक्त करता है,
* इसलिए जब तक mapping संभव हो, यहाँ कुछ भी मुक्त नहीं होता। */
bl->buf_ring = NULL;
bl->is_mmap = 0;
}
}
/* ... */
}
/* ->release() पर या उसके बाद बुलाया जाता है: अब mmap किए गए rings को सुरक्षित रूप से मुक्त करें। */
void io_kbuf_mmap_list_free(struct io_ring_ctx *ctx)
{
struct io_buf_free *ibf;
struct hlist_node *tmp;
hlist_for_each_entry_safe(ibf, tmp, &ctx->io_buf_list, list) {
hlist_del(&ibf->list);
io_mem_free(ibf->mem);
kfree(ibf);
}
}
इंजीनियरिंग और सिस्टम सुरक्षा चेकलिस्ट
- ✓ऐसा कर्नेल चलाएँ जिसमें कमिट c392cbecd8ec हो (Linux 6.6.5, 6.7 या बाद का, या ऐसा डिस्ट्रीब्यूशन कर्नेल जिसके चेंजलॉग में
CVE-2024-0582हो)। - ✓जहाँ वर्कलोड को io_uring की ज़रूरत न हो, उसे सीमित करें:
kernel.io_uring_disabled=2सेट करें, या अपने डिस्ट्रीब्यूशन के seccomp/SELinux से हर वर्कलोड के लिए नियंत्रित करें। - ✓जब
mmap()/remap_pfn_range()से मेमोरी यूज़र स्पेस को दें, तो उसका जीवनकाल किसी पहले के टियरडाउन रास्ते के बजाय->release()से बाँधें, ताकि मैपिंग हटने के बाद वह एक ही बार मुक्त हो। - ✓कर्नेल को KASAN और
slub_debugके साथ बनाएँ और फ़ज़ करें ताकि मैप रहते हुए मुक्त करना टेस्ट में यूज़-आफ़्टर-फ़्री के रूप में पकड़ा जाए। - ✓अविश्वसनीय लोकल वर्कलोड के लिए
CONFIG_IO_URINGके बिना बने कर्नेल को प्राथमिकता दें, याio_uring_setup/io_uring_registerसिस्टम कॉल को seccomp से ब्लॉक करें।