
CVE-2026-31413: BPF वेरिफायर साउंडनेस बग - कंटेनर एस्केप
मुझे Linux BPF वेरिफायर में एक साउंडनेस बग मिला - एक push_stack() कॉल में + 1, जिसके कारण वेरिफायर फोर्क किए गए पथ पर एक ALU निर्देश को स्किप कर देता है। BPF_OR के लिए, इसका मतलब है कि वेरिफायर dst = 0 ट्रैक करता है जबकि CPU 0 | K = K की गणना करता है। मैंने एक पूर्ण कंटेनर एस्केप लिखा: BPF मैप से OOB रीड/राइट, vtable हाइजैक, modprobe_path ओवरराइट, होस्ट पर रूट। फिर मैंने दो-पैच श्रृंखला बनाई - एक-अक्षर वाला वेरिफायर फिक्स और 90 लाइनों के selftests - और इसे मुख्यलाइन (mainline) में मर्ज करवाया।
| CVE | CVE-2026-31413 |
| बग वर्ग | Verifier soundness - रजिस्टर मान विचलन |
| मूल कारण | push_stack(env, env->insn_idx + 1, ...) फोर्क किए गए पथ पर ALU निर्देश को स्किप करता है |
| प्रस्तुत | bffacdb80b93 - Linux 7.0-rc1 (Jan 14, 2026) |
| फिक्स्ड | c845894ebd6f - Linux 7.0-rc5 (Mar 22, 2026) |
| प्रभावित | 6.12.75+ (stable backport dea9989a3f) से 7.0-rc4 तक |
| प्रभाव | मनमाना kernel R/W → कंटेनर एस्केप → होस्ट रूट |
| आवश्यक | CAP_BPF + CAP_PERFMON + CAP_NET_ADMIN |
| फिक्स | एक अक्षर: insn_idx + 1 → insn_idx |
maybe_fork_scalars() जब ARSH + AND/OR को किसी constant के साथ देखता है तो वेरिफायर स्थिति (state) को फोर्क करता है। पुश किए गए पथ को dst = 0 मिलता है और वह ALU निर्देश को स्किप कर देता है। AND के लिए यह ठीक है: 0 & K = 0। OR के लिए यह गलत है: 0 | K = K, न कि 0।
वेरिफायर सोचता है कि रजिस्टर शून्य है। CPU के पास K है। मैंने इसका उपयोग BPF मैप वैल्यू से मनमाना OOB रीड/राइट बनाने, मैप का कर्नेल पता लीक करने, एक नकली bpf_map_ops vtable बनाने, map_push_elem को array_map_get_next_key के माध्यम से मनमाने राइट के लिए रीडायरेक्ट करने, और modprobe_path को ओवरराइट करने के लिए किया। अज्ञात बाइनरी फॉर्मेट ट्रिगर करें, कर्नेल मेरी स्क्रिप्ट को रूट के रूप में चलाता है। कंटेनर में, पूर्ण होस्ट एस्केप।
दो-पैच श्रृंखला: एक-अक्षर वाला वेरिफायर फिक्स और 90 लाइनों के BPF selftests जो OR बनाम AND फोर्किंग को कवर करते हैं। 22 मार्च को Alexei Starovoitov द्वारा मर्ज किया गया। CVE-2026-31413 12 अप्रैल को Greg Kroah-Hartman द्वारा निर्धारित किया गया।
eBPF आपको कर्नेल में छोटे प्रोग्राम लोड करने देता है - पैकेट फिल्टर, ट्रेसिंग हुक, सुरक्षा नीतियां - बिना कर्नेल मॉड्यूल कंपाइल किए। पकड़ यह है कि आप ring 0 में कोड इंजेक्ट कर रहे हैं। अगर उस कोड में बग है, तो वह कर्नेल बग है।
इसलिए किसी भी BPF प्रोग्राम के चलने से पहले, कर्नेल का वेरिफायर हर संभव निष्पादन पथ का अनुकरण करता है। यह ट्रैक करता है कि प्रत्येक रजिस्टर में क्या है (एक पॉइंटर? एक स्केलर? कौन सी रेंज?), हर मेमोरी एक्सेस को मैप बाउंड्स के विरुद्ध जांचता है, और किसी भी चीज़ को अस्वीकार करता है जो बाउंड्स से बाहर पढ़ या लिख सकती है। यदि वेरिफायर कहता है कि प्रोग्राम सुरक्षित है, तो JIT इसे नेटिव मशीन कोड में कंपाइल करता है और पूर्ण कर्नेल विशेषाधिकार के साथ चलाता है। उस बिंदु के बाद कोई रनटाइम बाउंड्स जांच नहीं होती। वेरिफायर ही सुरक्षा सीमा है।
इसीलिए वेरिफायर साउंडनेस बग सामान्य मेमोरी करप्शन से अलग होते हैं। हीप ओवरफ्लो या UAF के साथ, आपको एक करप्शन प्रिमिटिव मिलता है और वहाँ से काम करना होता है - हीप स्प्रे करें, ऑब्जेक्ट्स को ग्रूम करें, एक विंडो के लिए रेस करें। वेरिफायर बग के साथ, आप कर्नेल को रजिस्टर के मान के बारे में झूठ पर विश्वास करवाते हैं। हर बाउंड्स जांच जो उस रजिस्टर पर निर्भर करती है, पास हो जाती है। कर्नेल ने आपकी OOB एक्सेस को मंजूरी दे दी। वह इसे बिना प्रश्न के चलाता है। यदि आप रजिस्टर स्थिति को सही ढंग से संरेखित कर सकते हैं, तो आपको इससे एक स्वच्छ और विश्वसनीय प्रिमिटिव मिलता है।
मैं maybe_fork_scalars() का ऑडिट कर रहा था - नया कोड, जनवरी 2026 में bffacdb80b93 में जोड़ा गया। स्टेट फोर्किंग हमेशा दिलचस्प होती है क्योंकि यहीं वेरिफायर समानांतर अन्वेषण पथों में विभाजित होता है, और यदि कोई पथ गलत मान ट्रैक करता है, तो उस पथ के नीचे की हर चीज़ असाउंड (unsound) होती है।
फ़ंक्शन तब फोर्क करता है जब वह किसी constant स्रोत के साथ ARSH + AND/OR देखता है। पुश किए गए पथ को dst = 0 मिलता है, और वह ALU निर्देश को स्किप कर देता है। मैं push_stack(env, env->insn_idx + 1, ...) लाइन पढ़ रहा था और यह तुरंत समझ में आ गया - + 1 का मतलब है कि पुश किया गया पथ ALU ऑप को कभी निष्पादित नहीं करता। AND के लिए, 0 & K = 0, इसलिए स्किप करना ठीक है। OR के लिए, 0 | K = K। पुश किए गए पथ को लगता है कि परिणाम 0 है जबकि वास्तव में यह K है।
मैंने उसी शाम एक BPF प्रोग्राम लिखा। {0, -1} पाने के लिए ARSH 63, एक constant के साथ OR, वेरिफायर पथों को अलग करने के लिए सशर्त शाखा, फिर "शून्य" रजिस्टर को एक मैप पॉइंटर में जोड़ा। वेरिफायर ने map_value + 0 को मंजूरी दी। CPU ने map_value + K तक पहुँच बनाई। KASAN ने परीक्षण में आउट-ऑफ-बाउंड्स एक्सेस की पुष्टि की।
अगली सुबह तक OOB रीड/राइट। अगली रात तक कंटेनर एस्केप। मैंने पूरे समय Claude (Opus 4.5) का उपयोग किया - वेरिफायर की स्टेट फोर्किंग लॉजिक को समझने, एक्सप्लॉइटेशन प्रिमिटिव्स पर विचार-मंथन करने, और OOB को पूर्ण एस्केप चेन में बदलने के लिए। vtable हाइजैक दृष्टिकोण एक विचार-विमर्श से उभरा, जिसमें Claude ने bpf_map_ops फंक्शन पॉइंटर्स की समीक्षा की, कॉल करने योग्य गैजेट्स की तलाश में।
कमिट bffacdb80b93 ("bpf: Recognize special arithmetic shift in the verifier") 14 जनवरी, 2026 को 7.0-rc1 में आया। Alexei Starovoitov, Puranjay Mohan द्वारा सह-विकसित। इसने एक LLVM DAGCombiner पैटर्न को संभालने के लिए maybe_fork_scalars() जोड़ा:```
w2 s>>= 31 // arithmetic shift right: w2 becomes 0 or -1
w2 &= -134 // AND with constant K
LLVM `select_cc setlt X, 0, A, 0` को `sra + and` में बदलता है। अंकगणितीय
दायाँ शिफ्ट के बाद, रजिस्टर या तो `0` (गैर-ऋणात्मक इनपुट) या `-1` (सभी एक) होता है।
एक स्थिरांक के साथ AND देता है `0` या `K`।
वेरिफायर एकल `bpf_reg_state` में `{0, K}` को ट्रैक नहीं कर सकता - इसकी साइन्ड रेंज
`[0, K]` अति-अनुमानित करती है, और इसी कारण यह मान्य Cilium
प्रोग्रामों को अस्वीकार कर रहा था। समाधान: वेरिफायर स्थिति को फोर्क करें। एक पथ `dst = 0` का अन्वेषण करता है, दूसरा
`dst = -1`, प्रत्येक सटीक मान का ट्रैक रखता है।
कार्यान्वयन:```c
static int maybe_fork_scalars(struct bpf_verifier_env *env,
struct bpf_insn *insn,
struct bpf_reg_state *dst_reg)
{
// ... condition check: dst range is [-1, 0], src is constant ...
branch = push_stack(env, env->insn_idx + 1, env->insn_idx, false);
// ^^^^^^^^^^^^
// pushed path resumes AFTER the ALU insn
if (IS_ERR(branch))
return PTR_ERR(branch);
regs = branch->frame[branch->curframe]->regs;
__mark_reg_known(®s[insn->dst_reg], 0); // pushed: dst = 0
__mark_reg_known(dst_reg, -1ull); // current: dst = -1
return 0;
}
पुश किए गए पथ पर दो चीज़ें होती हैं:
0 पर सेट हो जाता हैinsn_idx + 1 पर फिर से शुरू होता है - ALU op के बाद वाला निर्देशBPF_AND के लिए: dst = 0, AND को छोड़ें। रनटाइम: 0 & K = 0। मेल खाता है। सुदृढ़।
BPF_OR के लिए: dst = 0, OR को छोड़ें। रनटाइम: 0 | K = K। बेमेल।
वेरिफ़ायर 0 देखता है। CPU के पास K है। असुदृढ़।
फ़ंक्शन opcode की जाँच नहीं करता। इसे AND के लिए लिखा गया था - जहाँ निर्देश
को छोड़ना उसे dst = 0 के साथ निष्पादित करने के समान है - और इसे
OR पर भी लागू कर दिया गया। OR के लिए, यह समतुल्यता मान्य नहीं है।
ट्रिगर पैटर्न पाँच निर्देशों का है:``` r6 = (u64)(map_value + 0) // load a positive value (guaranteed by map init) r6 s>>= 63 // arithmetic shift: r6 = 0 (positive input) r6 |= K // BUG: verifier forks, pushed path gets r6=0 if r6 s< 0 goto exit // steers verifier paths r9 += r6 // verifier: r9 += 0 (in-bounds) // runtime: r9 += K (OOB)
वेरिफायर दो पथों का अन्वेषण करता है: