Fil-C का सरलीकृत मॉडल
(corsix.org)- C/C++ pointers के साथ AllocationRecord metadata जोड़कर उन्हें ट्रैक करने और dereference के समय memory boundary checks करने वाली संरचना
- pointer assignment, arithmetic, function argument passing, return, और
malloc·freecalls तक मूल pointer value के साथ संबंधित metadata को भी साथ ले जाना, या उन्हें Fil-C-विशिष्ट calls में बदलने का तरीका - heap memory के भीतर pointers का metadata invisible_bytes में अलग से स्टोर किया जाता है, और pointer load·store के समय value और metadata दोनों को साथ पढ़ा-लिखा जाता है, साथ ही alignment checks भी लागू होते हैं
filc_freeकेवलvisible_bytesऔरinvisible_bytesको free करता है, AllocationRecord स्वयं बना रहता है, और बाद की सफाई garbage collector संभालता है; जिन local variables के address के escape होने की संभावना हो उन्हें heap promotion किया जाता है- threads, function pointers, memory·performance optimizations जैसी वास्तविक implementation complexities अभी भी बची हैं, लेकिन बड़े C/C++ codebases की memory safety verification या pointer provenance के ठोस system example के रूप में इसका उपयोग संभव है
Fil-C का सरलीकृत मॉडल
- Fil-C C/C++ code को memory-safe तरीके से संभालने के लिए pointers के साथ AllocationRecord* metadata ट्रैक करने वाली संरचना का उपयोग करता है
- वास्तविक implementation LLVM IR rewriting पर आधारित है, लेकिन यह simplified model C/C++ source code को automatic transformation के रूप में दिखाता है
- हर function के pointer-type local variable के लिए एक corresponding
AllocationRecord*local variable जोड़ा जाता है - उदाहरण के लिए
T1* p1के लिएAllocationRecord* p1ar = NULLजोड़ा जाता है
- pointer local variables पर simple assignment और calculation में मूल pointer value के साथ AllocationRecord* भी साथ चलता है
p1 = p2कोp1 = p2, p1ar = p2arमें बदला जाता हैp1 = p2 + 10में भीp1ar = p2arसाथ आता है- integer से pointer में cast करने पर metadata को
NULLसेट किया जाता है - pointer को integer में बदलने वाला cast ज्यों का त्यों रहता है
- function argument passing और return में भी pointer के साथ अतिरिक्त AllocationRecord* पास किया जाता है, और कुछ standard library calls को Fil-C-विशिष्ट functions से बदला जाता है
mallocऔरfreecalls को क्रमशःfilc_malloc,filc_freeमें बदला जाता है- उदाहरण के लिए
p1 = malloc(x); free(p1);को{p1, p1ar} = filc_malloc(x); filc_free(p1, p1ar);में बदला जाता है
filc_mallocकेवल requested memory allocate नहीं करता, बल्कि तीन allocations करता हैAllocationRecordobject allocation- वास्तविक data के लिए
visible_bytesallocation - अदृश्य metadata storage के लिए
invisible_bytesकोcallocसे allocate करना AllocationRecordमेंvisible_bytes,invisible_bytes,lengthfields होते हैं
Dereference और boundary checks
- pointer dereference के समय साथ मौजूद AllocationRecord* का उपयोग करके boundary checks किए जाते हैं
- जाँचा जाता है कि pointer metadata
NULLन हो - वर्तमान pointer position और
visible_bytesके starting address का अंतर निकाला जाता है - देखा जाता है कि offset कुल length से छोटा है या नहीं
- बची हुई length dereference target के size के लिए पर्याप्त है या नहीं
- जाँचा जाता है कि pointer metadata
- read और write दोनों पर यही checking process लागू होती है
x = *p1से पहले भी check किया जाता है*p2 = xसे पहले भी यही check लागू होता है
- इस संरचना से pointer जिस target को दिखा रहा है, उस पर allocation range के बाहर access रोकी जाती है
Heap के अंदर pointers और invisible_bytes
- heap memory में stored pointers को compiler local variables की तरह सीधे अलग variables से manage नहीं कर सकता, इसलिए invisible_bytes का उपयोग होता है
- अगर
visible_bytes + ilocation पर pointer है, तो उसका correspondingAllocationRecord*invisible_bytes + ilocation पर store किया जाता है - यानी
invisible_bytesऐसा व्यवहार करता है मानो वहAllocationRecord*element type वाला array हो
- अगर
- memory से pointer value पढ़ते या लिखते समय सामान्य boundary check के अलावा alignment check भी जोड़ा जाता है
- offset
iकाsizeof(AllocationRecord*)का multiple होना जाँचा जाता है - तभी
invisible_bytesकोAllocationRecord**array की तरह सुरक्षित रूप से access किया जा सकता है
- offset
- pointer load के समय data pointer के साथ metadata भी साथ load होता है
p2 = *p1मेंp2 = *p1के बादp2ar = *(AllocationRecord**)(p1ar->invisible_bytes + i)जोड़ा जाता है
- pointer store के समय pointer value के साथ उसका corresponding metadata भी store किया जाता है
*p1 = p2में वास्तविक data store करने के बाद*(AllocationRecord**)(p1ar->invisible_bytes + i) = p2arकिया जाता है
filc_free और garbage collector
filc_freepointer केNULLन होने पर AllocationRecord के साथ consistency check के बाद केवल दो memory regions free करता हैpar != NULLकी जाँचp == par->visible_bytesकी जाँचvisible_bytesऔरinvisible_bytesको free करना- इसके बाद
visible_bytes,invisible_bytesकोNULLऔरlengthको 0 में बदलना
filc_mallocतीन allocations करता है, लेकिनfilc_freeAllocationRecord object को स्वयं free नहीं करता- इस अंतर को garbage collector संभालता है
- इस simplified model में stop-the-world GC पर्याप्त है, जबकि वास्तविक Fil-C parallel concurrent incremental collector का उपयोग करता है
- GC
AllocationRecordobjects को follow करते हुए tracing करता है - unreachable
AllocationRecordको free के लिए target बनाता है
- GC
- GC अतिरिक्त रूप से दो और काम करता है
- unreachable
AllocationRecordको free करते समयfilc_freecall करना length0 वालेAllocationRecordको point करने वाले सभी pointers को length 0 वाले एक single canonicalAllocationRecordमें बदलना
- unreachable
- इस व्यवहार से
freeन बुलाने पर भी memory leak नहीं होती- GC automatic free करता है
- हालांकि
freecall करने से GC से पहले memory को जल्दी release करना संभव होता है
freeके बाद संबंधितAllocationRecordअंततः unreachable हो जाता है और बाद में साफ किया जा सकता है
Local variable address escape और heap promotion
- GC मौजूद होने पर local variables के addresses को सुरक्षित रूप से संभालने की सीमा बढ़ जाती है
- यदि किसी local variable का address लिया गया है और compiler यह साबित नहीं कर सकता कि वह address variable की lifetime के बाहर escape नहीं करेगा, तो उसे heap allocation में promote किया जाता है
- ऐसे local variables stack के बजाय
mallocके जरिए allocate होते हैं- उनके लिए अलग से
freeinsert करने की जरूरत नहीं होती - GC उनकी सफाई संभालता है
- उनके लिए अलग से
Fil-C version का memmove
- C standard library का
memmovearbitrary memory को संभालता है, इसलिए compiler को उसके भीतर pointers हैं या नहीं, यह पता नहीं होता - इसके लिए heuristic लागू की जाती है
- arbitrary memory के भीतर pointer पूरी तरह उसी memory range के अंदर पूर्ण रूप से शामिल होना चाहिए
- pointer सही तरह aligned होना चाहिए
- इसी नियम की वजह से समान 8-byte move में भी व्यवहार अलग हो सकता है
- aligned 8 bytes को एक बार में
memmoveकरने पर संबंधित range केinvisible_bytesभी साथ move होते हैं - अगर 1 byte करके 8 बार
memmoveकिया जाए, तोinvisible_bytesmove नहीं होते
- aligned 8 bytes को एक बार में
वास्तविक implementation में जुड़ने वाली अतिरिक्त जटिलताएँ
-
Threads
- concurrency, GC complexity बढ़ाने वाला तत्व है
filc_freeतुरंत memory free नहीं कर सकता- क्योंकि free करने वाला thread और उसी memory को access करने वाला दूसरा thread race condition में हो सकते हैं
- pointers पर atomic operations के लिए भी अतिरिक्त handling चाहिए
- मूल rewriting pointer load/store को दो load/store में बदल देती है, जिससे atomicity टूट जाती है
-
Function pointers
AllocationRecordके अतिरिक्त metadata से यह चिह्नित किया जाता है किvisible_bytesसामान्य data नहीं बल्कि executable code pointer है- function pointer
p1के जरिए call करते समयp1 == p1ar->visible_bytesकी जाँच और साथ ही यह verify किया जाता है किp1arfunction pointer को represent करता है - function pointer पर type confusion attacks को रोकने के लिए calling ABI में भी type signature verification चाहिए
- एक तरीका यह है कि सभी functions की type signature समान बना दी जाए
- जैसे सभी arguments को struct में रखकर memory के जरिए pass किया जाए
- ABI boundary पर हर function उस struct से संबंधित केवल एक
AllocationRecordप्राप्त करे
-
Memory usage optimization
- यह विचार किया जा सकता है कि
filc_mallocinvisible_bytesको तुरंत allocate न करे, बल्कि जरूरत पड़ने पर lazy allocation करे AllocationRecordऔरvisible_bytesको एक ही allocation में साथ रखने का तरीका भी संभव है- यदि underlying
mallocहर allocation के सामने metadata जोड़ता है, तो उस metadata कोAllocationRecordमें रखने पर भी विचार किया जा सकता है
- यह विचार किया जा सकता है कि
-
Performance optimization
- Fil-C की memory safety के साथ performance cost जुड़ा हुआ है
- खोई हुई performance को आंशिक रूप से वापस पाने के लिए कई techniques लागू की जा सकती हैं
Fil-C के उपयोग के अवसर
- बड़े C/C++ codebases जो ऊपर से चलते हुए दिखते हैं, लेकिन जिनमें memory safety verification नहीं है, और memory safety के लिए GC तथा बड़े performance overhead को स्वीकार किया जा सकता है, वहाँ इसका उपयोग संभव है
- Java, Go, Rust में rewrite करने से पहले इसे एक अस्थायी उपाय के रूप में देखा जा सकता है
- ASan की तरह memory bug detection के उद्देश्य से भी Fil-C चलाया जा सकता है
- C/C++ code को Fil-C के नीचे चलाकर memory bugs देखे जा सकते हैं
- जिन भाषाओं में compile-time language और runtime language समान हों और compile-time safety मजबूत हो, वहाँ safe compile-time evaluation के लिए इसका उपयोग संभव है
- उदाहरण के रूप में Zig का उल्लेख है
- भले runtime evaluation safe न हो, compile-time evaluation में Fil-C जैसी संरचना उपयोगी हो सकती है
- pointer provenance को संभालने वाले एक ठोस system case के रूप में भी इसका महत्व है
- जब
p1औरp2का type समान हो, तोif (p1 == p2) { f(p1); }कोif (p1 == p2) { f(p2); }में बदलने जैसी optimization संभव है या नहीं, यह प्रश्न उठता है - Fil-C में
fको pass किया जाने वालाAllocationRecord*अलग हो सकता है, इसलिए उत्तर स्पष्ट रूप से नहीं है - इस अर्थ में Fil-C pointer provenance वाले एक ठोस system example की भूमिका निभाता है
- जब
1 टिप्पणियां
Hacker News टिप्पणियाँ
reference counting या invisible capability system के किसी variant को भी आज़माने की गुंजाइश है, और थोड़ा indirect reference cost लेकर memory बचाई जा सकती है, ऐसा लगता है
उम्मीद है कि जो लोग इन दोनों को साथ इस्तेमाल करके hermetic builds बनाना चाहते हैं, उनके लिए यह मददगार होगा
Upon freeing an unreachable AllocationRecord, call filc_free on it.मेरी समझ से इसका आशय शायद यह है कि किसी unreachable AR को free करने से पहले,
visible_bytesऔरinvisible_bytesfields जिन memory blocks की ओर इशारा करती हैं, उन्हें पहले free किया जाएसुरक्षा के लिए “rewrite it in Rust” कहने से ज़्यादा दिलचस्प यह लगता है कि मौजूदा C programs को पूरी तरह memory-safe तरीके से compile किया जा सकता है
पहली, Fil-C ज़्यादा धीमा है और आकार में बड़ा है. अगर यह स्वीकार्य होता, तो पिछले 10 सालों में Rust की जगह पहले Java या C# की ओर जाना चाहिए था, यह भी कहा जा सकता है
दूसरी, आप अब भी C ही इस्तेमाल कर रहे हैं. मौजूदा codebase को maintain करने के लिए यह ठीक हो सकता है, लेकिन अगर नया code बहुत लिखना हो तो मुझे Rust काफ़ी ज़्यादा सुखद लगता है
तीसरी, Fil-C runtime safety देता है, जबकि Rust कुछ चीज़ों को compile time पर व्यक्त कर सकता है. इससे भी आगे WUFFS जैसी भाषाएँ runtime checks के बिना compile चरण में ही safety prove करने की कोशिश करती हैं, इसलिए code तार्किक रूप से गलत हो सकता है, लेकिन crash या arbitrary code execution को रोकने की दिशा अलग है
Fil-Qt: A Qt Base build with Fil-C experience, Linux Sandboxes and Fil-C, Ported freetype, fontconfig, harfbuzz, and graphite to Fil-C, A Note on Fil-C, Notes by djb on using Fil-C, Fil-C: A memory-safe C implementation, Fil's Unbelievable Garbage Collector जैसे threads रहे हैं
आप अब भी memory-unsafe code लिख सकते हैं, बस अब उसका नतीजा vulnerability की जगह निश्चित crash बन जाता है
अगर आप untrusted input लेने वाला web API जैसी कोई चीज़ बना रहे हैं, तो ऐसी समस्या आख़िरकार denial-of-service तक जा सकती है, इसलिए यह बेहतर तो है, लेकिन इसे काफ़ी अच्छा कहना मुश्किल है
मैं Fil-C के काम को कमतर नहीं बता रहा, बस runtime approach की साफ़ सीमाएँ हैं
लेकिन निष्पक्ष होकर कहें तो Fil-C Rust से काफ़ी धीमा है, और memory भी ज़्यादा इस्तेमाल करता है
दूसरी ओर Fil-C safe dynamic linking को support करता है, और कुछ मायनों में यह कहा जा सकता है कि यह Rust से भी ज़्यादा सख़्ती से safe है
आख़िरकार यह trade-off है, इसलिए हर कोई अपनी परिस्थिति के हिसाब से चुन सकता है
इसलिए यह विचार तकनीकी रूप से दिलचस्प होते हुए भी भावनात्मक स्तर पर आसानी से नहीं जंचता
capability और pointer values assignment के दौरान torn हो सकती हैं, इसलिए अगर thread interleaving खराब हुआ तो गलत pointer से object access होकर arbitrary misbehavior हो सकता है
इस सीमा को स्वीकार किया जा सकता है, लेकिन जो लोग इस समस्या की ओर इशारा करते हैं, उनके ख़िलाफ़ समर्थकों का भी काफ़ी आक्रामक हो जाना थोड़ा निराशाजनक लगता है
दुर्भाग्य से वही overhead के बड़े कारणों में से एक भी है
इस तरह के तरीके कई बार लागू किए गए और फिर इसलिए छोड़ दिए गए कि या तो security guarantees काफ़ी नहीं थीं, या non-fat ABI boundaries को पार करना मुश्किल था, या overhead बहुत ज़्यादा था
और फिर भी, मुझे नहीं लगता कि filc को सिर्फ़ साधारण fat pointer से पूरी तरह समझाया जा सकता है