12. آشنایی با DataFlowSanitizer (معروف به dfsan)
DataFlowSanitizer يک سامانه تحليل پوياي گردش داده (dynamic data flow analysis) به صورت عمومي است. بر خلاف ساير ابزارهاي سنیتایزر (Sanitizer)، اين ابزار براي شناسايي يک رده مشخص از خطاها به صورت مستقل طراحي نشده است. در عوض، يک چارچوب عمومي تحليل پوياي گردش داده فراهم ميکند تا توسط برنامه نويسان به عنوان client مورد استفاده قرار گيرد و به آنان امکان دهد بر اساس نيازهاي خاص برنامه خود، اشکالات و ناهنجاريهاي ويژه را شناسايي کنند.
12.1 روش ساخت ++libc با DFSan
DFSan نياز دارد که يا تمام کد شما ایزارگذاری (instrument) شود، يا اينکه توابع بدون ایزارگذاری در فهرست ABI به عنوان ابزارگذاری نشده (uninstrumented) مشخص شوند. اگر هدف این باشد که توابع مربوط به ++libc نیز ابزارگذاری شوند، لازم است این کتابخانه به صورت جداگانه از سورس و با پشتیبانی DFSan کامپایل شود. در ادامه، نمونهای از این فرایند ارائه میشود که نشان میدهد چگونه میتوان ++libc و libc ABI را همراه با ابزارگذاری مربوط به DataFlowSanitizer (DFSan) تولید و استفاده کرد.
mkdir libcxx-build
cd libcxx-build
An example using ninja
cmake -GNinja -S /runtimes \
-DCMAKE_C_COMPILER=clang \
-DCMAKE_CXX_COMPILER=clang++ \
-DLLVM_USE_SANITIZER="DataFlow" \
-DLLVM_ENABLE_RUNTIMES="libcxx;libcxxabi"
ninja cxx cxxabi
توجه: اطمينان حاصل کنيد که فرآيند ساخت (build) را با نسخه کافي جديد از Clang انجام ميدهيد.
12.2 نحوه استفاده (Usage)
بدون هيچ گونه تغيير در برنامه، به کارگيري DataFlowSanitizer رفتار برنامه را تغيير نخواهد داد. براي استفاده از DataFlowSanitizer، برنامه از توابع API براي اعمال tag به دادهها جهت رهگيري (tracking) آنها، و براي بررسي tag يک داده خاص استفاده ميکند. DataFlowSanitizer مديريت انتشار (propagation) tagها را مطابق گردش داده در برنامه انجام ميدهد.
APIها در فايل سرآيند (header) با نام sanitizer/dfsan_interface.h تعريف شدهاند. براي دريافت اطلاعات بيشتر درباره هر تابع، به اين فايل رجوع کنيد.
12.3 فهرست ABI (ABI List)
DataFlowSanitizer از فهرستي از توابع که با عنوان ABI list شناخته ميشود استفاده ميکند تا تعيين کند که آيا فراخواني يک تابع بايد از ABI بومي سيستم عامل استفاده کند يا بايد از گونه اي اصلاح شده از اين ABI استفاده کند که labelها را از طريق پارامترهاي تابع و مقادير برگشتي جريان ميدهد (propagate). فايل ABI list همچنين تعيين ميکند که در حالت نخست labelها چگونه منتشر ميشوند. DataFlowSanitizer داراي يک ABI list پيش فرض است که هدف آن در نهايت پوشش کتابخانه glibc در Linux است، ولي ممکن است کاربران ناچار شوند اين فهرست را گسترش دهند، در مواردي که يک کتابخانه يا تابع خاص قابل ابزارگذاری (instrument) نباشد (براي مثال زماني که به زبان اسمبلی (assembly) يا زبان ديگري نوشته شده باشد که DataFlowSanitizer از آن پشتيباني نميکند)، يا زماني که يک تابع از درون يک کتابخانه يا تابع غير قابل ابزارگذاری فراخواني شود.
فايل ABI list در DataFlowSanitizer يک نمونه خاص از لیست موارد خاص سنیتایزر (Sanitizer special case list) است. گذر (pass) مربوطه هر تابع در دسته ابزارگذاری نشده (uninstrumented) در فايل ABI list را مطابق با ABI بومي سيستم عامل در نظر ميگيرد. مگر آنکه ABI list دسته بندي هاي اضافي براي اين توابع داشته باشد، فراخواني به چنين توابعي يک هشدار توليد ميکند، زيرا رفتار label گذاري (labelling behavior) تابع ناشناخته است. دستههاي پشتيباني شده ديگر عبارتند از: discard، functional و custom.
- discard هر ميزاني که اين تابع در حافظه قابل دسترس کاربر (user accessible memory) بنويسد، labelهاي متناظر را نيز در shadow memory به روز رساني ميکند (اين شرط براي توابعي که در حافظه قابل دسترس کاربر نمي نويسند به صورت بديهي برقرار است). مقدار برگشتي تابع بدون label است.
- functional مشابه discard، با اين تفاوت که label مقدار برگشتي، اجتماع (union) label پارامترهاي ورودي است.
- custom به جاي فراخواني تابع اصلي، يک wrapper اختصاصي با نام __dfsw_F فراخواني ميشود که F نام تابع اصلي است. اين wrapper ميتواند تابع اصلي را فراخواني کند يا يک پيادهسازي مستقل ارائه دهد. اين دسته معمولاً براي توابع غير قابل ابزارگذاری که در حافظه قابل دسترس کاربر مينويسند يا رفتار انتشار label پيچيده تري دارند استفاده ميشود. امضاي تابع __dfsw_F بر اساس امضاي F است، با اين تفاوت که براي هر آرگومان، يک label از نوع dfsan_label به فهرست آرگومان ها افزوده ميشود. اگر F مقدار برگشتي غير void داشته باشد، يک آرگومان نهايي از نوع dfsan_label * اضافه ميشود که تابع custom ميتواند label مقدار برگشتي را در آن قرار دهد. براي مثال:
void f(int x);
void __dfsw_f(int x, dfsan_label x_label);
void *memcpy(void *dest, const void *src, size_t n);
void *__dfsw_memcpy(void *dest, const void *src, size_t n,
dfsan_label dest_label, dfsan_label src_label,
dfsan_label n_label, dfsan_label *ret_label);
اگر يک تابع که در translation unit در حال کامپايل تعريف شده است در دسته ابزارگذاری نشده (uninstrumented) قرار بگيرد، آن تابع به گونهاي کامپايل ميشود که با native ABI سازگار باشد. فرض ميشود که آرگومانهاي آن بدون label هستند، اما تابع، labelها را در shadow memory منتشر ميکند (propagate).
براي مثال:
# main is called by the C runtime using the native ABI.
fun:main=uninstrumented
fun:main=discard
# malloc only writes to its internal data structures, not user-accessible memory.
fun:malloc=uninstrumented
fun:malloc=discard
# tolower is a pure function.
fun:tolower=uninstrumented
fun:tolower=functional
# memcpy needs to copy the shadow from the source to the destination region.
# This is done in a custom function.
fun:memcpy=uninstrumented
fun:memcpy=custom
براي توابع ابزارگذاری (instrument) شده، ABI list يک دسته به نام force_zero_labels را پشتيباني ميکند که موجب ميشود تمام عمليات store و مقادير بازگشتي با برچسب صفر (zero label) تنظيم شوند. هرگز نبايد يک تابع را به طور هم زمان در دو دسته force_zero_labels و ابزارگذاری نشده (uninstrumented) يا هر يک از انواع wrapperهاي مربوط به ابزارگذاری نشده قرار داد.
براي مثال:
# e.g. void writes_data(char* out_buf, int out_buf_len) {...}
# Applying force_zero_labels will force out_buf shadow to zero.
fun:writes_data=force_zero_labels
12.4 فلگهاي کامپايل (Compilation Flags)
- -dfsan-abilist فايل هاي اضافي ABI list را که نحوه انتقال shadow parameterها را کنترل ميکنند مشخص مينمابد. نام فايلها با comma از هم جدا ميشوند.
- -dfsan-combine-pointer-labels-on-load کنترل ميکند که آيا labelهاي pointer در load instructionها در نظر گرفته شوند يا ناديده گرفته شوند. مقدار پيش فرض آن true است. براي مثال:
v = *p;
- اگر flag برابر true باشد، label متغير v اجتماع label مربوط به p و label مربوط به *p خواهد بود.
- اگر flag برابر false باشد، label متغير v فقط برابر label مربوط به *p خواهد بود.
- -dfsan-combine-pointer-labels-on-store کنترل ميکند که آيا labelهاي pointer در store instructionها در نظر گرفته شوند يا ناديده گرفته شوند. مقدار پيش فرض آن false است. براي مثال:
*p = v;
- اگر flag برابر true باشد، label مقدار *p اجتماع label مربوط به p و label مربوط به v خواهد بود.
اگر flag برابر false باشد، label مقدار *p فقط برابر label مربوط به v خواهد بود. - -dfsan-combine-offset-labels-on-gep کنترل ميکند که آيا labelهاي مربوط به offset در دستورهاي GEP منتشر شوند يا خير. مقدار پيش فرض آن true است. براي مثال:
p += i;
- اگر flag برابر true باشد، label مربوط به p اجتماع label p و label i خواهد بود.
اگر flag برابر false باشد، label مربوط به p بدون تغيير باقي ميماند. - -dfsan-track-select-control-flow کنترل ميکند که آيا جريان کنترل (control flow) مربوط به select instructionها رهگيري شود يا خير. مقدار پيش فرض آن true است. براي مثال:
v = b ? v1 : v2;
- اگر flag برابر true باشد، label متغير v اجتماع label هاي b، v1 و v2 خواهد بود.
- اگر flag برابر false باشد، label متغير v فقط اجتماع labelهاي v1 و v2 خواهد بود.
- -dfsan-event-callbacks يک قابليت آزمايشي (experimental) است که callback براي رويدادهاي مشخص مربوط به داده ها درج ميکند. در حال حاضر callback فقط براي load، store، انتقال حافظه (مانند memcpy و memmove)، و مقايسه ها درج ميشود. مقدار پيش فرض آن false است.
- اگر اين flag برابر با true تنظيم شود، کاربر بايد پياده سازي اين توابع callback را فراهم کند:
void __dfsan_load_callback(dfsan_label Label, void* Addr);
void __dfsan_store_callback(dfsan_label Label, void* Addr);
void __dfsan_mem_transfer_callback(dfsan_label *Start, size_t Len);
void __dfsan_cmp_callback(dfsan_label CombinedLabel);
- -dfsan-conditional-callbacks يک قابليت آزمايشي است که callback براي عبارتهاي شرطي مربوط به جريان کنترل (control flow conditional expressions) درج ميکند. اين قابليت ميتواند براي يافتن مکانهايي استفاده شود که در آنها مقادير tainted ميتوانند جريان اجرا را کنترل کنند.
علاوه بر اين فلگ، بايد يک callback handler با استفاده از دستور زير ثبت شود:
dfsan_set_conditional_callback(my_callback);
که در آن my_callback تابعـي با امضا (signature) زير است:
void my_callback(dfsan_label l dfsan_origin o);
اين امضا زماني که origin tracking غير فعال باشد نيز همينگونه است؛ در اين حالت مقدار dfsan_origin هميشه برابر 0 خواهد بود. callback فقط زماني فراخواني ميشود که يک مقدار tainted به يک عبارت شرطي مربوط به جريان کنترل برسد (مانند شرط موجود در if). callback براي عبارتهاي شرطي داخل signal handler اجرا نميشود، زيرا اين کار مستعد ايجاد بن بست است. به جاي آن، مقادير tainted که در عبارتهاي شرطي داخل signal handler استفاده ميشوند، به صورت تجميعي از طريق bitwise or جمع آوري ميشوند و ميتوان با تابع زير به آنها دسترسي داشت:
dfsan_label dfsan_get_labels_in_signal_conditional();
- -dfsan-reaches-function-callbacks قابليتي آزمايشي است که callback براي دادههايي درج ميکند که وارد يک تابع ميشوند.
علاوه بر اين فلگ، بايد يک callback handler با استفاده از دستور زير ثبت شود:
dfsan_set_reaches_function_callback(my_callback);
که در آن my_callback تابعـي با امضاي زير است:
void my_callback(dfsan_label label, dfsan_origin origin, const char *file, unsigned int line, const char *function);
این امضا حتی در حالتی که Origin Tracking غیرفعال باشد نیز تغییری نمیکند و در این حالت مقدار dfsan_origin همواره برابر با 0 خواهد بود. این callback زمانی فراخوانی میشود که یک مقدار آلوده (tainted) به رجیسترها یا پشته (stack) یک تابع وارد شود. مقادیر آلوده (tainted values) میتوانند از منابع مختلفی وارد شوند، از جمله:
- آرگومانهای تابع
- مقدار بازگشتی (return value) از یک فراخوانی درون تابع
- دادهای که از طریق عملیات load در داخل تابع خوانده شده باشد
لازم به ذکر است که این فراخوانی (callback) برای عبارتهای شرطی داخل signal handler اجرا نمیشود، زیرا این حالت میتواند منجر به بنبست (deadlock) شود. در این شرایط، مقادیر آلوده (tainted) که به تابع میرسند از طریق عملیات bitwise OR با یکدیگر ترکیب (aggregate) میشوند. برای بازیابی این مقادیر نیز میتوان از تابع زیر استفاده کرد:
dfsan_label dfsan_get_labels_in_signal_reaches_function();
- -dfsan-track-origins نحوه پيگيري originها را کنترل ميکند.
مقدار 0: runtime هيچ originي را پيگيري نميکند.
مقدار 1: runtime origin را در عمليات store پيگيري ميکند.
مقدار 2: runtime origin را در عمليات load و store پيگيري ميکند.
مقدار پيش فرض: 0.
- -dfsan-instrument-with-call-threshold اگر يک تابع instrument شده نياز داشته باشد بيش از اين تعداد عمليات origin store انجام دهد، به جاي inline check از callback استفاده ميشود (مقدار -1 يعني هرگز از callback استفاده نشود). مقدار پيش فرض: 3500.
12.5 متغيرهاي محيطي (Environmental variables)
- warn_unimplemented مشخص ميکند آيا براي توابع پيادهسازي نشده هشدار داده شود يا خير. مقدار پيش فرض: false.
- strict_data_dependencies مشخص ميکند که آيا labelها فقط در صورت وجود وابستگي دادهاي آشکار منتشر شوند يا خير.
(مثلاً در مقايسه رشتهها، ناديده گرفتن اينکه نتيجه مقايسه ممكن است به صورت ضمني وابسته به محتواي رشته ها باشد.) اين تنظيم فقط براي توابع موجود در دسته custom در ABI list اعمال ميشود. مقدار پيش فرض: true.
- origin_history_size حداکثر طول زنجيره origin. مقادير غير مثبت به معناي نامحدود بودن است. مقدار پيش فرض: 16.
- origin_history_per_stack_limit حداکثر تعداد ارجاع هاي يک origin node. مقادير غير مثبت به معناي نامحدود بودن است. مقدار پيش فرض: 20000.
- store_context_size حداکثر عمق stack trace براي origin tracking. مقدار پيش فرض: 20.
- zero_in_malloc مشخص ميکند که آيا shadow space حافظه جديد اختصاص داده شده صفر شود يا خير. مقدار پيش فرض: true.
- zero_in_free مشخص ميکند که آيا shadow space حافظه آزاد شده صفر شود يا خير. مقدار پيش فرض: true.
12.6 مثال (Example)
DataFlowSanitizer تا 8 label را پشتيباني ميکند تا سربار CPU و اندازه کد کم باشد. labelهاي پايه (base labels) اعداد 8 بيت unsigned هستند که توانهاي 2 ميباشند (مانند 1، 2، 4، 8، …، 128). labelهاي ترکيبي (union labels) با انجام عمل OR روي labelهاي پايه ساخته ميشوند. برنامه زير انتشار labelها را با بررسي اينکه آيا labelهاي صحيح منتشر ميشوند نشان ميدهد.
#include
#include
int main(void) {
int i = 100;
int j = 200;
int k = 300;
dfsan_label i_label = 1;
dfsan_label j_label = 2;
dfsan_label k_label = 4;
dfsan_set_label(i_label, &i, sizeof(i));
dfsan_set_label(j_label, &j, sizeof(j));
dfsan_set_label(k_label, &k, sizeof(k));
dfsan_label ij_label = dfsan_get_label(i + j);
assert(ij_label & i_label); // ij_label has i_label
assert(ij_label & j_label); // ij_label has j_label
assert(!(ij_label & k_label)); // ij_label doesn't have k_label
assert(ij_label == 3); // Verifies all of the above
// Or, equivalently:
assert(dfsan_has_label(ij_label, i_label));
assert(dfsan_has_label(ij_label, j_label));
assert(!dfsan_has_label(ij_label, k_label));
dfsan_label ijk_label = dfsan_get_label(i + j + k);
assert(ijk_label & i_label); // ijk_label has i_label
assert(ijk_label & j_label); // ijk_label has j_label
assert(ijk_label & k_label); // ijk_label has k_label
assert(ijk_label == 7); // Verifies all of the above
// Or, equivalently:
assert(dfsan_has_label(ijk_label, i_label));
assert(dfsan_has_label(ijk_label, j_label));
assert(dfsan_has_label(ijk_label, k_label));
return 0;
}
-mllvm -dfsan-track-origins=1
براي مثال:
cat test.cc
#include
#include
int main(int argc, char** argv) {
int i = 0;
dfsan_set_label(i_label, &i, sizeof(i));
int j = i + 1;
dfsan_print_origin_trace(&j, "A flow from i to j");
return 0;
}
clang++ -fsanitize=dataflow -mllvm -dfsan-track-origins=1 -fno-omit-frame-pointer -g -O2 test.cc
./a.out
Taint value 0x1 (at 0x7ffd42bf415c) origin tracking (A flow from i to j)
Origin value: 0x13900001, Taint value was stored to memory at
0 0x55676db85a62 in main test.cc:7:7
1 0x7f0083611bbc in __libc_start_main libc-start.c:285
Origin value: 0x9e00001, Taint value was created at
0 0x55676db85a08 in main test.cc:6:3
1 0x7f0083611bbc in __libc_start_main libc-start.c:285
با استفاده از -mllvm -dfsan-track-origins=1، DataFlowSanitizer تنها storeهاي مياني را كه يك مقدار داراي label از آنها عبور كرده است جمع آوري ميكند. فعال بودن رهگیری مبدا باعث كاهش سرعت اجراي برنامه تا 2 برابر نسبت به كاهش سرعت عادي DataFlowSanitizer ميشود و ميزان مصرف حافظه را حدود 1 برابر افزايش ميدهد. با استفاده از -mllvm -dfsan-track-origins=2، DataFlowSanitizer علاوه بر اين، لودهای مياني را نيز كه يك مقدار داراي label از آنها عبور كرده است جمع آوري ميكند. اين حالت سرعت اجراي برنامه را تا 4 برابر كاهش ميدهد.
12.8 وضعيت كنوني (Current status)
DataFlowSanitizer يك پروژه در حال توسعه است و در حال حاضر براي x86_64 Linux در حال تكميل ميباشد.
12.9 طراحي (Design)
لطفاً به سند طراحي (design document[1]) مراجعه كنيد.
13. آشنایی با LeakSanitizer (معروف به leak)
LeakSanitizer يك شناساگر memory leak در زمان اجرا (run-time) است. اين ابزار ميتواند همراه با AddressSanitizer مورد استفاده قرار گيرد تا هم خطاهاي حافظه و هم memory leakها شناسايي شوند، يا ميتوان آن را به صورت stand-alone به كار برد. LSan تا زمان پاياني اجرا تقريباً هيچ سربار كارايي ايجاد نميكند و تنها در مرحله پاياني فرآيند، يك فاز اضافي براي شناسايي leak اجرا ميشود.
13.1 نحوه استفاده (How to use)
AddressSanitizer: ابزار LeakSanitizer را در خود ادغام ميكند و آن را به طور پيش فرض در پلتفرم هاي پشتيباني شده فعال ميسازد.
cat memory-leak.c
#include
void *p;
int main() {
p = malloc(7);
p = 0; // The memory is leaked here.
return 0;
}
clang -fsanitize=address -g memory-leak.c ; ASAN_OPTIONS=detect_leaks=1 ./a.out
==23646==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 7 byte(s) in 1 object(s) allocated from:
0 0x4af01b in __interceptor_malloc /projects/compiler-rt/lib/asan/asan_malloc_linux.cc:52:3
1 0x4da26a in main memory-leak.c:4:7
2 0x7f076fd9cec4 in __libc_start_main libc-start.c:287
SUMMARY: AddressSanitizer: 7 byte(s) leaked in 1 allocation(s).
براي استفاده از LeakSanitizer در حالت stand-alone، برنامه خود را با فلگ زير لينك كنيد:
-fsanitize=leak
اطمينان حاصل كنيد كه براي مرحله link از clang استفاده كنيد (نه ld)، تا كتابخانه run-time مناسب مربوط به LeakSanitizer در فايل اجرايي نهايي لينك شود.
13.2 ملاحظات امنيتي Security Considerations
LeakSanitizer يك ابزار شناسايي خطا (bug detection tool) است و runtime آن براي لينك شدن به فایلهای اجرایی مورد استفاده در محيط تولید (production) طراحي نشده است. هرچند ممكن است براي آزمون و آزمايش سودمند باشد، اما runtime مربوط به LeakSanitizer با در نظر گرفتن محدوديتهاي امنيتي حساس توسعه داده نشده و ممكن است امنيت فايل اجرايي نهايي را به خطر بيندازد.
13.3 پلتفرمهاي پشتيباني شده (Supported platforms)
- Android
Fuchsia
Linux
macOS
NetBSD
13.4 اطلاعات بيشتر (More information)
https://github.com/google/sanitizers/wiki/AddressSanitizerLeakSanitizer
13.5 مروری بر leaksanitizer (Leaksanitizer Review)
LeakSanitizer يك ابزار run-time براي شناسايي memory leak است. اين ابزار ميتواند همراه با AddressSanitizer استفاده شود تا هم خطاهاي حافظه و هم memory leakها شناسايي شوند، يا ميتوان آن را به صورت stand-alone به كار برد. براي استفاده از LeakSanitizer در حالت stand-alone، برنامه خود را با پرچم زير لينك كنيد:
-fsanitize=leak
اطمينان حاصل كنيد كه براي مرحله link از clang استفاده كنيد (نه ld)، تا كتابخانه run-time مناسب مربوط به LeakSanitizer در فايل اجرايي نهايي لينك شود. براي مثال هايي كه با اين ابزار ارائه ميشوند، از محيط محلي توسعه يافته PrgEnv-llvm استفاده ميكنيم.
$ cat memory-leak.c
#include
void *p;
int main() {
p = malloc(7);
p = 0; // The memory is leaked here.
return 0;
}
$ clang -fsanitize=leak -g -O0 memory-leak.c
$ ./a.out
=================================================================
==2335900==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 7 byte(s) in 1 object(s) allocated from:
#0 0x55966653a842 in malloc /.../nersc/nersc-user-env/prgenv/llvm_src_17.0.6/compiler-rt/lib/lsan/lsan_interceptors.cpp:75:3
#1 0x559666565898 in main /pscratch/sd/e/elvis/addresssanitizer/memory-leak.c:4:7
#2 0x7efe8f83e24c in __libc_start_main (/lib64/libc.so.6+0x3524c) (BuildId: ddc393ac74ed8f90d4fdfff796432fbafd281e1b)
SUMMARY: LeakSanitizer: 7 byte(s) leaked in 1 allocation(s)
AddressSanitizer، ابزار LeakSanitizer را در خود ادغام كرده و آن را به صورت پيش فرض فعال ميسازد. مثال بعدي اين موضوع را نشان ميدهد:
$ clang -fsanitize=address -g memory-leak.c
$ ASAN_OPTIONS=detect_leaks=1 ./a.out
=================================================================
==2339511==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 7 byte(s) in 1 object(s) allocated from:
#0 0x56040740afde in malloc /.../nersc/nersc-user-env/prgenv/llvm_src_17.0.6/compiler-rt/lib/asan/asan_malloc_linux.cpp:69:3
#1 0x560407447a68 in main /pscratch/sd/e/elvis/addresssanitizer/memory-leak.c:4:7
#2 0x7fdab443e24c in __libc_start_main (/lib64/libc.so.6+0x3524c) (BuildId: ddc393ac74ed8f90d4fdfff796432fbafd281e1b)
SUMMARY: AddressSanitizer: 7 byte(s) leaked in 1 allocation(s)
14. آشنایی با TypeSanitizer (معروف به type)
TypeSanitizer يك شناساگر (detector) براي نقضهاي strict type aliasing است. اين ابزار از يك ماژول ابزارگذاری (instrumentation) در مرحله كامپايل و يك كتابخانه run-time تشكيل شده است. در زبان هاي C++/C قوانين type-based aliasing وجود دارند و LLVM ميتواند با استفاده از TBAA metadata كه توسط Clang توليد ميشود، اين قوانين را براي انجام بهينه سازيها مورد استفاده قرار دهد. به طور كلي، اشاره گر يك نوع مشخص نبايد به شيء متعلق به نوع ديگر دسترسي پيدا كند، جز در چند استثناي محدود.
اين قوانين هميشه براي كاربران روشن نيستند و در نتيجه باعث پديد آمدن كدي ميشوند كه اين قواعد را نقض ميكند (مثلاً براي type punning). چنين وضعيتي ممكن است موجب شود گذرهاي بهينه سازي (optimization passes) به شكل ناخواسته باعث ايجاد باگ شوند، مگر اينكه كد با -fno-strict-aliasing كامپايل شود كه اين خود باعث از دست دادن كارايي خواهد شد.
TypeSanitizer براي شناسايي مواردي ساخته شده است كه در آنها قوانين strict aliasing نقض شده اند، و به كاربران كمك ميكند خاستگاه (origin) چنين باگهايي را در كد خود بيابند، حتي اگر كد در نگاه اول معتبر به نظر برسد. از آنجا كه TypeSanitizer هنوز آزمايشي است، در حال حاضر ميتواند تاثير قابل توجهي بر سرعت اجرا، مصرف حافظه، و اندازه كد داشته باشد. همچنين سربار زمان كامپايل آن زياد است. كارهايي در حال انجام است تا اين سربارها كاهش يابد.
14.1 الگوريتم TypeSanitizer (TypeSanitizer algorithm)
برای هر type-access descriptor (توصيفگر دسترسي نوع) در سیستم TBAA (Type-Based Alias Analysis) که از طریق متادیتای TBAA در LLVM IR رمزگذاری شده است، گذر ابزارگذاری شده (Instrumentation Pass) اقدام به تولید جداول توصيفگر (descriptor tables) میکند.
در نتیجه، هر نوع داده و توصيفگر (descriptor) مربوط به آن دارای یک اشارهگر (Pointer) یکتا خواهد بود. این جداول بهصورت COMDAT تعریف میشوند (بهجز انواعی که در anonymous namespace قرار دارند)، بنابراین مقدار اشارهگرها در کل برنامه یکتا باقی میماند.
توصيفگرها به یکدیگر متصل میشوند و ساختاری شبیه یک درخت aliasing نوعی (Type Aliasing Tree) تشکیل میدهند؛ مشابه مکانیزمی که در خود TBAA در LLVM برای تحلیل همپوشانی انواع داده استفاده میشود.
در زمان اجرا (Runtime)، برای هر بایت از دادهای که در برنامه مورد دسترسی قرار میگیرد، از shadow memory با اندازه ۸ بایت استفاده میشود. این مقدار در واقع شامل اشارهگری به type descriptor مربوطه است.
در نتیجه، اولین بایت از هر نوع داده، در shadow memory بهصورت یک اشارهگر (pointer) به توصیفگر (descriptor) آن نوع ذخیره میشود. علاوه بر این، ممکن است مقادیر دیگری نیز در shadow memory ذخیره شوند که برای تحلیل دقیقتر aliasing و بررسی رفتار حافظه مورد استفاده قرار میگیرند.
- مقدار 0 نشان دهنده يك نوع ناشناخته (unknown type) است.
- مقادير منفي نشان دهنده interior byte هستند: بايتي داخل يك نوع كه اولين بايت نيست.
براي مثال مقدار -2 يعني آن بايت، سومين بايت در آن نوع است.
ابزارگذاری (Instrumentation) ابتدا بررسي ميكند كه آيا نوع دسترسي جاري (current access) به طور دقيق با نوع ذخيره شده در shadow memory مطابقت دارد يا خير. اين كار سريعاً با بررسي مقادير اشارهگر (pointer) انجام ميشود. اگر تطابق برقرار باشد، shadow memory مربوط به بقيه آن نوع نيز بررسي ميگردد تا مطمئن شود مقادير منفي درست هستند. اگر اين مرحله شكست بخورد، مسير آهسته (slow path) فراخواني ميشود.
اگر مطابقت دقيق برقرار نباشد، بررسي ميكنيم كه آيا مقدار، و بقيه shadow byte ها، برابر 0 هستند يا خير. اگر اينطور باشد، shadow memory با pointer صحيح به type descriptor و مقادير منفي مناسب براي ساير بايتهاي آن نوع مقدارگذاري ميشود.
اگر نوع موجود در shadow نه مطابقت دقيق باشد و نه 0، run-time check كُندتر فراخواني ميشود. اين بخش از run-time از الگوريتم كامل TBAA مشابه كامپايلر استفاده ميكند تا مشخص كند آيا دو نوع مجاز به alias هستند يا خير.
Instrumentation pass همچنين براي عمليات memset، memcpy، memmove، و allocas/byval (و نيز lifetime.start/end) فراخوانيهايي درج ميكند تا shadow memory را بازنشاني كند (reset) و نشان دهد كه نوع ناشناخته است. run-time نيز فراخوانيهاي كتابخانهاي memset و memcpy و غيره را رهگیری (interception) ميكند تا همين كار را انجام دهد.
14.2 روش ساخت (Build Method)
LLVM/Clang را با استفاده از CMake بسازيد و compiler-rt را فعال كنيد. يك نمونه تنظيم CMake براي استفاده/آزمون TypeSanitizer:
cmake -DCMAKE_BUILD_TYPE=Release \
-DLLVM_ENABLE_PROJECTS="clang" \
-DLLVM_ENABLE_RUNTIMES="compiler-rt" \
/llvm
14.3 نحوه استفاده (Usage)
برنامه خود را با فلگ زير كامپايل و لينك كنيد:
-fsanitize=type
کتابخانه runtime مربوط به TypeSanitizer باید در فایل اجرایی نهایی لینک شود؛ بنابراین در مرحله لینک نهایی حتماً باید از clang استفاده شود (نه ابزارهایی مانند ld بهصورت مستقیم). برای دستیابی به عملکرد مناسب نیز توصیه میشود از سطح بهینهسازی -O1یا بالاتر استفاده شود. بهطور پیشفرض، TypeSanitizer هنگام گزارش خطاها stack trace کامل را نمایش نمیدهد. در صورتی که نیاز به مشاهده کامل stack trace وجود داشته باشد، میتوان از تنظیم زیر استفاده کرد:
TYSAN_OPTIONS=print_stacktrace=1
براي دريافت stack trace خواناتر، از -fno-omit-frame-pointer و -g استفاده كنيد. براي داشتن stack trace كاملاً دقيق، ممكن است لازم باشد inlining را غير فعال كنيد (با -O1) و tail call elimination را خاموش كنيد:
-fno-optimize-sibling-calls
cat example_AliasViolation.c
int main(int argc, char **argv) {
int x = 100;
float *y = (float*)&x;
*y += 2.0f; // Strict aliasing violation
return 0;
}
Compile and link
clang++ -g -fsanitize=type example_AliasViolation.cc
برنامه هر بار كه يك نقض strict aliasing تشخيص داده شود، يك پيام خطا در stderr چاپ ميكند. برنامه متوقف نخواهد شد، و اين موضوع به شما امكان ميدهد كه در يك اجراي واحد، تعداد زيادي از نقضهاي strict aliasing را شناسايي كنيد.
./a.out
==1375532==ERROR: TypeSanitizer: type-aliasing-violation on address 0x7ffeebf1a72c (pc 0x5b3b1145ff41 bp 0x7ffeebf1a660 sp 0x7ffeebf19e08 tid 1375532)
READ of size 4 at 0x7ffeebf1a72c with type float accesses an existing object of type int
0 0x5b3b1145ff40 in main example_AliasViolation.c:4:10
==1375532==ERROR: TypeSanitizer: type-aliasing-violation on address 0x7ffeebf1a72c (pc 0x5b3b1146008a bp 0x7ffeebf1a660 sp 0x7ffeebf19e08 tid 1375532)
WRITE of size 4 at 0x7ffeebf1a72c with type float accesses an existing object of type int
0 0x5b3b11460089 in main example_AliasViolation.c:4:10
14.4 واژگان خطا (Error vocabulary)
برخي اصطلاحات ممكن است در خطاهاي TypeSanitizer ظاهر شوند كه از TBAA Metadata گرفته شدهاند. اين بخش يك فرهنگ لغت مختصر از اين اصطلاحات ارائه ميكند.
- omnipotent char: اين يك نوع خاص است كه ميتواند با هر چيزي alias شود. نام آن از نوع char در ++C/C گرفته شده است.
- type p[x]: اين نماد نشان دهنده pointer هايي به يك نوع است. x تعداد غیرمستقیمها (indirection) براي رسيدن به مقدار نهايي است.
براي مثال، يك pointer به pointer به يك integer به صورت type p2 int نمايش داده ميشود.
TypeSanitizer همچنان آزمايشي است. پيام هاي خطاي كاربرپسند در آينده بايد بهبود يابند تا ارجاع به اصطلاحات خاص LLVM IR حذف شود.
14.5 قابليتهاي سنیتایزر (Sanitizer capabilities)
14.5.1 __has_feature(type_sanitizer)
در برخي موارد ممكن است لازم باشد كد متفاوتي بسته به فعال بودن يا نبودن TypeSanitizer اجرا شود.
براي اين منظور ميتوان از [2]__has_feature استفاده كرد.
#if defined(__has_feature)
# if __has_feature(type_sanitizer)
// code that builds only under TypeSanitizer
# endif
#endif
14.5.2 attribute ((no_sanitize(“type”)))
گاهی ممکن است بخشی از کد نباید توسط TypeSanitizer ابزارگذاری (Instrument) شود. برای این منظور میتوان از ویژگی تابع no_sanitize("type") استفاده کرد تا ابزارگذاری مربوط به type aliasing برای آن بخش غیرفعال شود.
با این حال، بسته به رفتار کدهای غیرابزارگذاریشده، ممکن است در تعامل با کدهای instrumented نتایجی مانند false positive یا false negative ایجاد شود.
همچنین باید توجه داشت که این attribute ممکن است توسط همه کامپایلرها پشتیبانی نشود؛ بنابراین توصیه میشود همراه با بررسی ویژگی __has_feature(type_sanitizer) استفاده شود تا از پشتیبانی صحیح آن در زمان کامپایل اطمینان حاصل گردد.
14.5.3 attribute ((disable_sanitizer_instrumentation))
ویژگی disable_sanitizer_instrumentation را میتوان روی توابع اعمال کرد تا تمامی انواع ابزارگذاری (instrumentation) برای آن تابع غیرفعال شود. استفاده از این ویژگی ممکن است منجر به بروز مشکلاتی مانند false positive یا تولید stack trace نادرست شود.
بنابراین این attribute باید با احتیاط و تنها در موارد کاملاً ضروری استفاده شود؛ برای مثال در کدی که به هیچ عنوان قادر به تحمل ابزارگذاری یا اثرات جانبی ناشی از آن نیست. همچنین این attribute نسبت به no_sanitize("type") اولویت بالاتری دارد و در صورت استفاده، آن را override میکند.
14.6 Ignorelist
TypeSanitizer از موجودیت (entity)های src و fun در Sanitizer Special Case List پشتیبانی میکند. این قابلیت برای حذف گزارشهای مربوط به نقض aliasing در فایلهای منبع مشخص (source files) یا توابع مشخص (functions) مورد استفاده قرار میگیرد. مشابه سایر روشهای غیرفعالسازی ابزارگذاری (instrumentation exclusion)، استفاده از این ویژگی میتواند در برخی موارد منجر به بروز مثبت کاذب یا منفی کاذب شود.
14.7 محدوديتها (Limitations)
- TypeSanitizer نسبت به يك اجراي معمولي (native run) حافظه واقعي بيشتري استفاده ميكند. براي هر بايت از حافظه كاربر، 8 بايت shadow memory مصرف ميكند.
- برخي گذرهاي بهينه سازي پيش از TypeSanitizer اجرا ميشوند. اگر اين گذرها يك نقض aliasing را حذف كنند، TypeSanitizer ديگر قادر به كشف آن نخواهد بود.
- در حال حاضر تمام instrumentation ها به صورت inline است و اين موضوع ميتواند منجر به تا 15 برابر افزايش اندازه فايل توليدي و 3 تا 7 برابر افزايش در زمان كامپايل شود. در برخي موارد مستند، موجب hang كردن كامپايلر نيز شده است. برنامه هايي براي بهبود اين وضعيت در آينده در دست انجام است.
- كدهايي كه از union يا متغيرهاي struct-initialized استفاده ميكنند ممكن است نتايج نادرست ببينند، زيرا TypeSanitizer هنوز اين موارد را به شكل قابل اعتماد instrument نميكند.
- از آنجا كه سيستم TBAA در Clang و LLVM براي توليد چك هاي instrumentation استفاده ميشود، TypeSanitizer نيز از قوانين type aliasing در Clang و LLVM پيروي ميكند. ممكن است مواردي وجود داشته باشد كه اين قوانين با استاندارد سازگار نباشند؛ با اين حال اين موضوع تضمين ميكند كه TypeSanitizer هر نقض aliasingي را كه ممكن است هنگام كامپايل با Clang/LLVM منجر به باگ شود، شناسايي خواهد كرد.
- TypeSanitizer در حال حاضر نميتواند همراه با ساير sanitizerها مانند AddressSanitizer، ThreadSanitizer يا UndefinedBehaviourSanitizer اجرا شود.
14.8 وضعيت كنوني (Current status)
TypeSanitizer ابزاري كاملاً جديد و هنوز در حال توسعه است. برخي مشكلات شناخته شده وجود دارند، به ويژه در بخش هايي كه داده هاي TBAA توليدشده توسط Clang براي نيازهاي runtime اين ابزار كافي نيستند. ما به شكل فعال در حال ارتقاي اين ابزار هستيم — با ما همراه باشيد. هرگونه كمك، گزارش مشكل، pull request يا ايده خوشآمد گفته ميشود. ميتوانيد issue tracker [4] را در اينجا پيدا كنيد.
15. آشنایی با RealtimeSanitizer (معروف به realtime)
RealtimeSanitizer يا RTSan، يك ابزار تست امنيت زمان واقعي (real-time safety) براي پروژه هاي C و ++C است. RTSan ميتواند براي شناسايي نقض هاي زمان واقعي استفاده شود، يعني فراخواني متدهايي كه براي استفاده در توابع با زمان اجراي تعيين شده (deterministic run time) ايمن نيستند. RTSan هر تابعي كه با attribute [[clang::nonblocking]] علامتگذاري شده باشد را به عنوان تابع زمان واقعي (real-time function) در نظر ميگيرد. در زمان اجرا، اگر RTSan فراخواني به malloc، free، pthread_mutex_lock يا هر تابع ديگري كه زمان اجراي غيرتعيين شده (non-deterministic execution time) دارد را در يك تابع علامتگذاري شده با [[clang::nonblocking]] تشخيص دهد، خطا صادر ميكند. RTSan تحليل خود را در زمان اجرا انجام ميدهد اما attribute [[clang::nonblocking]] را با سيستم Function Effect Analysis كه در زمان كامپايل براي شناسايي احتمالي نقض هاي امنيت زمان واقعي عمل ميكند، به اشتراك ميگذارد. براي شناسايي جامع مسائل امنيت زمان واقعي، توصيه ميشود از هر دو سيستم به طور همزمان استفاده شود. سربار زمان اجراي ايجاد شده توسط RealtimeSanitizer ناچيز است.
15.1 روش ساخت (How to build)
LLVM/Clang را با استفاده از CMake بسازيد و compiler-rt زمان اجرا را فعال كنيد. يك نمونه تنظيم CMake براي استفاده/آزمون RealtimeSanitizer:
cmake -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_PROJECTS="clang" -DLLVM_ENABLE_RUNTIMES="compiler-rt" /llvm
15.2 نحوه استفاده (Usage)
دو پيشنياز وجود دارد:
- كد بايد با فلگ sanitize=realtime- كامپايل شود.
- توابعي كه مشمول محدوديت هاي زمان واقعي هستند بايد با attribute [[clang::nonblocking]] علامت گذاري شوند.
به طور معمول، اين attributeها بايد روي توابعي كه نقطه ورود (entry points) threadها با اولويت زمان واقعي هستند، اضافه شوند. اين threadها مشمول زمان callback ثابت هستند، مانند audio callback threadها يا rendering loopها در كد بازيهاي ويديويي.
cat example_realtime_violation.cpp
#include
void violation() [[clang::nonblocking]]{
std::vector v;
v.resize(100);
}
int main() {
violation();
return 0;
}
Compile and link
clang++ -fsanitize=realtime example_realtime_violation.cpp
اگر يك نقض امنيت زمان واقعي (real-time safety violation) در يك [[clang::nonblocking]] context يا در هر تابعي كه توسط آن تابع فراخواني شود تشخيص داده شود، برنامه با يك کد خروج غير صفر (non-zero exit code) خاتمه خواهد يافت.
clang++ -fsanitize=realtime example_realtime_violation.cpp
./a.out
==76290==ERROR: RealtimeSanitizer: unsafe-library-call
Intercepted call to real-time unsafe function `malloc` in real-time context!
0 0x000102a7b884 in malloc rtsan_interceptors.cpp:426
1 0x00019c326bd0 in operator new(unsigned long)+0x1c (libc++abi.dylib:arm64+0x16bd0)
2 0xa30d0001024f79a8 ()
3 0x0001024f794c in std::__1::__libcpp_allocate[abi:ne200000](unsigned long, unsigned long)+0x44
4 0x0001024f78c4 in std::__1::allocator::allocate[abi:ne200000](unsigned long)+0x44
... snip ...
9 0x0001024f6868 in std::__1::vector<float, std::__1::allocator>::resize(unsigned long)+0x48
10 0x0001024f67b4 in violation()+0x24
11 0x0001024f68f0 in main+0x18 (a.out:arm64+0x1000028f0)
12 0x00019bfe3150 ()
13 0xed5efffffffffffc ()
15.3 توابع Blocking (Blocking functions)
فراخواني توابع كتابخانه سيستم مانند malloc به طور خودكار توسط RealtimeSanitizer شناسايي ميشود. برنامهنويسان زمان واقعي (real-time) ممكن است توابع blocking (ناايمن براي زمان واقعي) خود را نيز تعريف كنند كه بخواهند RealtimeSanitizer آنها را شناسايي كند. در زمان اجرا، اگر هر تابعي كه با [[clang::blocking]] علامتگذاري شده باشد در يك [[clang::nonblocking]] context فراخواني شود، RealtimeSanitizer يك خطا صادر خواهد كرد.
cat example_blocking_violation.cpp
#include
#include
std::atomic has_permission{false};
int wait_for_permission() [[clang::blocking]] {
while (has_permission.load() == false)
std::this_thread::yield();
return 0;
}
int real_time_function() [[clang::nonblocking]] {
return wait_for_permission();
}
int main() {
return real_time_function();
}
clang++ -fsanitize=realtime example_blocking_violation.cpp && ./a.out
==76131==ERROR: RealtimeSanitizer: blocking-call
Call to blocking function `wait_for_permission()` in real-time context!
0 0x0001000c3db0 in wait_for_permission()+0x10 (a.out:arm64+0x100003db0)
1 0x0001000c3e3c in real_time_function()+0x10 (a.out:arm64+0x100003e3c)
2 0x0001000c3e68 in main+0x10 (a.out:arm64+0x100003e68)
3 0x00019bfe3150 ()
4 0x5a27fffffffffffc ()
RTSAN_OPTIONS=option_1=true:path_option_2="/some/file.txt" ./a.out
همچنين ميتوان اين فلگها را در زمان كامپايل با ارائه نماد __rtsan_default_options مشخص كرد:
__attribute__((__visibility__("default")))
extern "C" const char *__rtsan_default_options() {
return "symbolize=false:abort_on_error=0:log_to_syslog=0";
}
ميتوانيد تمام گزينههاي sanitizer (برخي از آنها ممكن است پشتيباني نشوند) را با استفاده از فلگ help مشاهده كنيد:
RTSAN_OPTIONS=help=true ./a.out
ليست جزئي از فلگهايي كه RealtimeSanitizer از آنها پيروي ميكند:
برخي از مسائل مربوط به فلگها را ميتوان با استفاده از verbosity=$NUM اشكالزدايي (debug) كرد:
RTSAN_OPTIONS=verbosity=1:misspelled_flag=true ./a.out
WARNING: found 1 unrecognized flag(s):
misspelled_flag
...
15.5 شخصيسازي بیشتر (Additional customization)
علاوه بر rtsan_default_options كه پيشتر توضيح داده شد، ميتوان تعريفهايي از توابع ديگر ارائه كرد كه نحوه عملكرد RTSan را تحت تأثير قرار ميدهند. براي دريافت اطلاع از هر خطايي كه توسط RTSan گزارش ميشود، تعريف تابع sanitizer_report_error_summary را ارائه دهيد.
extern "C" void __sanitizer_report_error_summary(const char *error_summary) {
fprintf(stderr, "%s %s\n", "In custom handler! ", error_summary);
/* do other custom things */
}
خلاصه خطا به شكل زير خواهد بود:
SUMMARY: RealtimeSanitizer: unsafe-library-call main.cpp:8 in process(std::__1::vector<int, std::__1::allocator>&)
براي ثبت يك callback كه قبل از خاتمه فرآيند توسط RTSan فراخواني شود:
extern "C" void __sanitizer_set_death_callback(void (*callback)(void));
void custom_on_die_callback() {
fprintf(stderr, "In custom handler!")
/* do other custom things */
}
int main()
{
__sanitizer_set_death_callback(custom_on_die_callback);
...
}
#include
void process(const std::vector& buffer) [[clang::nonblocking]] {
{
__rtsan::ScopedDisabler d;
...
}
}
اگر RealtimeSanitizer در زمان كامپايل فعال نباشد (يعني كد با fsanitize=realtime- كامپايل نشده باشد)، ScopedDisabler به عنوان يك no-op كامپايل ميشود. در زبان C، ميتوان از توابع ()rtsan_disable__ و ()rtsan_enable__ براي غيرفعال كردن و دوباره فعال كردن بررسیهاي RealtimeSanitizer به صورت دستي استفاده كرد.
#include
int process(const float* buffer) [[clang::nonblocking]]
{
{
__rtsan_disable();
...
__rtsan_enable();
}
}
هر فراخواني از ()rtsan_disable__ بايد با فراخواني متعاقب ()rtsan_enable__ همراه باشد تا عملكرد عادي سنیتایزر (sanitizer) بازگردانده شود. اگر فراخواني متناظر ()rtsan_enable انجام نشود، رفتار برنامه تعريف نشده (undefined) خواهد بود.
15.8 فايل سرکوب (Suppression)
در زمان اجرا، ميتوان سرکوبها را با استفاده از يك فايل suppressions كه از طريق RTSAN_OPTIONS مشخص شده است، تعريف كرد. سرکوب در زمان اجرا ممكن است مفيد باشد اگر منبع كد قابل تغيير نباشد.
cat suppressions.supp
call-stack-contains:MallocViolation
call-stack-contains:std::*vector
function-name-matches:free
function-name-matches:CustomMarkedBlocking*
RTSAN_OPTIONS="suppressions=suppressions.supp" ./a.out
سرکوبهاي مشخص شده در اين فايل، يكي از دو نوع زير هستند:
- function-name-matches
گزارش هر فراخواني intercepted library يا توابع علامتگذاري شده با [[clang::blocking]] بر اساس نام آنها سرکوب ميشود.
براي مثال، اگر ميدانيد كه malloc در سيستم شما امنيت زمان واقعي دارد، ميتوانيد با استفاده از function-name-matches:malloc چك مربوطه را غيرفعال كنيد. - call-stack-contains
گزارش خطاها در هر stack كه حاوي رشتهاي مطابق الگو (pattern) مشخص شده باشد، سرکوب ميشود.
براي مثال، براي سرکوب گزارش خطاهاي مربوط به رفتار غير امن زمان واقعي در std::vector ميتوان از *call-stack-contains:std::vector استفاده كرد.
براي اين روش، لازم است نمادها (symbols) در build گنجانده شوند، زيرا stack trace هاي بدون symbol قابل تطابق نيستند.
call-stack-contains بالاترين هزينه زمان اجرا را در بين روشهاي سرکوب دارد.
الگوها ميتوانند مطابقت دقيق (exact match) باشند يا الگوهاي regex-light شامل كاراكترهاي خاص مانند ^, $, * باشند.
تعداد احتمالي خطاهاي سرکوب شده توسط اين روش را ميتوان هنگام خروج با استفاده از فلگ print_stats_on_exit مشاهده كرد.
15.9 شناسايي سنیتایزر در زمان كامپايل (Compile-time sanitizer detection)
Clang ماكرو has_feature__ را ارائه ميدهد كه ميتوان از آن براي تشخيص فعال بودن RealtimeSanitizer در زمان كامپايل استفاده كرد:
#if defined(__has_feature) && __has_feature(realtime_sanitizer)
...
#endif
16. آشنایی با SanitizerCoverage (Introduction to SanitizerCover)
LLVM یک ابزار ساده برای پوشش کد (Code Coverage) بهصورت داخلی ارائه میدهد که با نام SanitizerCoverage شناخته میشود. این ابزار فراخوانیهایی به توابعی که توسط کاربر تعریف شدهاند را در سطوح مختلف شامل function، basic block و edge به کد برنامه اضافه میکند.
برای این callbackها پیادهسازیهای پیشفرض نیز ارائه شده است که امکان تولید گزارش ساده و نمایش اولیه پوشش (Coverage) را فراهم میکنند. با این حال، در صورتی که هدف صرفاً مشاهده و گزارشگیری پوشش (Coverage) باشد، ممکن است استفاده از ابزار SourceBasedCodeCoverage گزینه مناسبتری باشد.
16.1 رديابي PCها با guards (Tracking PCs with guards)
كامپايلر با استفاده از فلگ fsanitize-coverage=trace-pc-guard–، كد زير را در هر edge درج ميكند:
__sanitizer_cov_trace_pc_guard(&guard_variable);
هر edge يك guard_variable مخصوص به خود (نوع uint32_t) خواهد داشت. همچنين كامپايلر فراخوانيهايي به ماژول constructor درج ميكند:
// The guards are [start, stop).
// This function will be called at least once per DSO and may be called
// more than once with the same values of start/stop.
__sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop);
با استفاده از فلگ ...=trace-pc,indirect-calls، فراخوانی تابع __sanitizer_cov_trace_pc_indirect(void *callee) در هر فراخوانی غیرمستقیم (Indirect Call) توسط کامپایلر درج میشود. این توابع با پیشوند __sanitizer_cov_trace_pc* در Runtime مربوط به Sanitizerها پیادهسازی نشدهاند و باید بهصورت دستی توسط کاربر تعریف شوند تا رفتار موردنظر برای ثبت یا پردازش اطلاعات Coverage فراهم گردد.
برای مثال:
// trace-pc-guard-cb.cc
#include
#include
#include
// This callback is inserted by the compiler as a module constructor
// into every DSO. 'start' and 'stop' correspond to the
// beginning and end of the section with the guards for the entire
// binary (executable or DSO). The callback will be called at least
// once per DSO and may be called multiple times with the same parameters.
extern "C" void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
uint32_t *stop) {
static uint64_t N; // Counter for the guards.
if (start == stop || *start) return; // Initialize only once.
printf("INIT: %p %p\n", start, stop);
for (uint32_t *x = start; x 1) foo();
}
clang++ -g -fsanitize-coverage=trace-pc-guard trace-pc-guard-example.cc -c
clang++ trace-pc-guard-cb.cc trace-pc-guard-example.o -fsanitize=address
ASAN_OPTIONS=strip_path_prefix=`pwd`/ ./a.out
INIT: 0x71bcd0 0x71bce0
guard: 0x71bcd4 2 PC 0x4ecd5b in main trace-pc-guard-example.cc:2
guard: 0x71bcd8 3 PC 0x4ecd9e in main trace-pc-guard-example.cc:3:7
ASAN_OPTIONS=strip_path_prefix=`pwd`/ ./a.out with-foo
INIT: 0x71bcd0 0x71bce0
guard: 0x71bcd4 2 PC 0x4ecd5b in main trace-pc-guard-example.cc:3
guard: 0x71bcdc 4 PC 0x4ecdc7 in main trace-pc-guard-example.cc:4:17
guard: 0x71bcd0 1 PC 0x4ecd20 in foo() trace-pc-guard-example.cc:2:14
16.2 شمارندههاي 8 بيت inline (یا Inline 8bit-counters)
این قابلیت تجربی (Experimental) محسوب میشود و ممکن است در آینده تغییر کرده یا حذف شود. با استفاده از گزینه -fsanitize-coverage=inline-8bit-counters، کامپایلر در هر Edge از گراف جریان کنترل، افزایش یک شمارنده درونخطی (Inline Counter) را درج میکند.
این روش از نظر کارکرد مشابه -fsanitize-coverage=trace-pc-guard است، با این تفاوت که بهجای فراخوانی Callback، تنها مقدار یک شمارنده بهصورت مستقیم توسط Instrumentation افزایش مییابد. در نتیجه، سربار ناشی از فراخوانی تابع حذف شده و جمعآوری دادهها سبکتر انجام میشود. کاربران باید یک تابع واحد برای ثبت (Capture) و استخراج این شمارندهها در زمان راهاندازی (Startup) برنامه پیادهسازی کنند تا دادههای Coverage بهدرستی جمعآوری و پردازش شوند.
extern "C"
void __sanitizer_cov_8bit_counters_init(char *start, char *end) {
// [start,end) is the array of 8-bit counters created for the current DSO.
// Capture this array in order to read/modify the counters.
}
16.3 فلگ بولين درون خطی (یا Inline bool-flag)
این قابلیت تجربی (Experimental) محسوب میشود و ممکن است در آینده تغییر کرده یا حذف شود. با استفاده از گزینه -fsanitize-coverage=inline-bool-flag، کامپایلر در هر Edge یک مقدار بولی (Boolean) درونخطی (Inline) را به مقدار true تنظیم میکند.
این مکانیزم از نظر عملکردی مشابه coverage=inline-8bit-counter است، با این تفاوت که به جای افزایش یک شمارنده، تنها یک مقدار بولی به true تغییر وضعیت میدهد. کاربران موظفاند یک تابع واحد برای جمعآوری و ثبت این فلگها در زمان راهاندازی (Startup) برنامه پیادهسازی کنند تا دادههای Coverage بهدرستی پردازش و ذخیره شوند.
extern "C"
void __sanitizer_cov_bool_flag_init(bool *start, bool *end) {
// [start,end) is the array of boolean flags created for the current DSO.
// Capture this array in order to read/modify the flags.
}
16.4 جدول PC
این قابلیت تجربی (Experimental) محسوب میشود و ممکن است در آینده تغییر کرده یا حذف شود. همچنین توجه داشته باشید که این نوع ابزارگذاری (Instrumentation) میتواند با قابلیت حذف کدهای بلااستفاده (Dead Code Stripping) از طریق گزینه -Wl,-gc-sections در لینکِرهایی غیر از LLD ناسازگار باشد و در نتیجه باعث افزایش قابلتوجه اندازه فایل باینری شود. برای اطلاعات بیشتر میتوان به Bug 34636 مراجعه کرد.
با استفاده از گزینه -fsanitize-coverage=pc-table، کامپایلر جدولی شامل آدرسهای Program Counter (PC) مربوط به نقاط ابزارگذاری شده در برنامه تولید میکند. این گزینه تنها در صورتی قابل استفاده است که یکی از حالتهای زیر نیز فعال باشد:
• -fsanitize-coverage=inline-8bit-counters
• -fsanitize-coverage=inline-bool-flag
• -fsanitize-coverage=trace-pc-guard
كاربران بايد يك تابع واحد براي ثبت جدول PC در هنگام راهاندازی (startup) پيادهسازي كنند.
extern "C"
void __sanitizer_cov_pcs_init(const uintptr_t *pcs_beg,
const uintptr_t *pcs_end) {
// [pcs_beg,pcs_end) is the array of ptr-sized integers representing
// pairs [PC,PCFlags] for every instrumented block in the current DSO.
// Capture this array in order to read the PCs and their Flags.
// The number of PCs and PCFlags for a given DSO is the same as the number
// of 8-bit counters (-fsanitize-coverage=inline-8bit-counters), or
// boolean flags (-fsanitize-coverage=inline=bool-flags), or trace_pc_guard
// callbacks (-fsanitize-coverage=trace-pc-guard).
// A PCFlags describes the basic block:
// * bit0: 1 if the block is the function entry block, 0 otherwise.
}
16.5 رهگیری PCها
با فعالسازی گزینه -fsanitize-coverage=trace-pc، کامپایلر در هر Edge از گراف جریان کنترل (Control Flow Graph)، فراخوانی تابع __sanitizer_cov_trace_pc() را به کد برنامه اضافه میکند. همچنین، با استفاده از گزینه -fsanitize-coverage=trace-pc,indirect-calls، فراخوانی تابع __sanitizer_cov_trace_pc_indirect(void *callee) نیز در هر فراخوانی غیرمستقیم (Indirect Call) درج میشود.
این Callbackها در Runtime مربوط به Sanitizerها پیادهسازی نشدهاند و باید توسط کاربر تعریف شوند. این طراحی به توسعهدهنده اجازه میدهد تا رفتار دلخواه خود را برای جمعآوری یا پردازش اطلاعات Coverage پیادهسازی کند. این سازوکار بهطور خاص در Fuzzing هسته Linux مورد استفاده قرار میگیرد، از جمله در پروژه Syzkaller (https://github.com/google/syzkaller).
16.6 نقاط ابزارگذاری (Instrumentation points)
پوشش سنیتایزر (anitizer Coverage) سطوح مختلفی از Instrumentation را ارائه میدهد:
- edge (پیشفرض): در این حالت، Edgeهای گراف جریان کنترل (Control Flow Graph) تحت Instrumentation قرار میگیرند (جزئیات بیشتر در ادامه آمده است).
- bb: در این سطح، Basic Blockها بهصورت مستقیم Instrument میشوند.
- func: تنها بلوک ورودی (Entry Block) هر تابع ابزارگذاری میشود.
این فلگها را میتوان بهصورت ترکیبی با حالتهای trace-pc-guard یا trace-pc استفاده کرد. برای مثال:
-fsanitize-coverage=func,trace-pc-guard
هنگامي كه edge يا bb استفاده شود، برخي از edgeها/بلوكها ممكن است هنوز instrument نشوند (pruned) اگر اين instrumentation زائد (redundant) تشخيص داده شود. براي غيرفعال كردن pruning، از no-prune استفاده كنيد، به عنوان مثال:
-fsanitize-coverage=bb,no-prune,trace-pc-guard
void foo(int *a) {
if (a)
*a = 0;
}
اين كد شامل ۳ بلوك اساسي (basic blocks) است كه ميتوان آنها را A ،B و C نامگذاري كرد:
A
|\
| \
| B
| /
|/
C
اگر بلوکهای A، B و C همگی تحت Coverage قرار گرفته باشند، میتوان با اطمینان نتیجه گرفت که یالهای A→B و B→C حداقل یکبار اجرا شدهاند. بااینحال، همچنان مشخص نیست که آیا یال A→C نیز اجرا شده است یا خیر.
چنین یالهایی در گراف جریان کنترل (Control Flow Graph یا CFG) با عنوان یالهای بحرانی (Critical Edges) شناخته میشوند. Edge Coverage برای رفع این ابهام، تمام Critical Edgeها را با درج بلوکهای پایهٔ ساختگی (Dummy Basic Blocks) به دو یال مجزا تقسیم میکند. سپس این بلوکهای جدید Instrument میشوند تا اجرای هر Critical Edge بهطور مستقل قابل ردیابی و ثبت باشد. به این ترتیب، با ثبت Coverage این بلوکهای ساختگی، میتوان با دقت مشخص کرد که آیا هر Critical Edge اجرا شده است یا خیر.
A
|\
| \
D B
| /
|/
C
16.8 رديابي جريان دادهها (Tracing data flow)
پشتيباني براي fuzzing مبتني بر جريان داده (data-flow-guided fuzzing) فراهم شده است.
- با استفاده از -fsanitize-coverage=trace-cmp، كامپايلر instrumentation اضافي در اطراف دستورات مقايسه (comparison instructions) و دستورات switch درج ميكند.
- با استفاده از -fsanitize-coverage=trace-div، كامپايلر دستورات تقسيم صحيح (integer division) را instrument ميكند تا آرگومان سمت راست تقسيم ثبت شود.
- با استفاده از -fsanitize-coverage=trace-gep، دستورالعملهاي LLVM GEP instrument ميشوند تا شاخصهاي آرایه ثبت شوند.
- به طور مشابه، با استفاده از -fsanitize-coverage=trace-loads و -fsanitize-coverage=trace-stores، كامپايلر به ترتيب loadها و storeها را instrument ميكند.
در حال حاضر، اين فلگها به تنهايي كار نميكنند و براي فعال شدن نياز به يكي از فلگهاي -fsanitize-coverage={trace-pc, inline-8bit-counters, inline-bool} دارند.
مگر اينكه فلگ no-prune ارائه شود، برخي از دستورات مقايسه (comparison instructions) instrument نخواهند شد.
// Called before a comparison instruction.
// Arg1 and Arg2 are arguments of the comparison.
void __sanitizer_cov_trace_cmp1(uint8_t Arg1, uint8_t Arg2);
void __sanitizer_cov_trace_cmp2(uint16_t Arg1, uint16_t Arg2);
void __sanitizer_cov_trace_cmp4(uint32_t Arg1, uint32_t Arg2);
void __sanitizer_cov_trace_cmp8(uint64_t Arg1, uint64_t Arg2);
// Called before a comparison instruction if exactly one of the arguments is constant.
// Arg1 and Arg2 are arguments of the comparison, Arg1 is a compile-time constant.
// These callbacks are emitted by -fsanitize-coverage=trace-cmp since 2017-08-11
void __sanitizer_cov_trace_const_cmp1(uint8_t Arg1, uint8_t Arg2);
void __sanitizer_cov_trace_const_cmp2(uint16_t Arg1, uint16_t Arg2);
void __sanitizer_cov_trace_const_cmp4(uint32_t Arg1, uint32_t Arg2);
void __sanitizer_cov_trace_const_cmp8(uint64_t Arg1, uint64_t Arg2);
// Called before a switch statement.
// Val is the switch operand.
// Cases[0] is the number of case constants.
// Cases[1] is the size of Val in bits.
// Cases[2:] are the case constants.
void __sanitizer_cov_trace_switch(uint64_t Val, uint64_t *Cases);
// Called before a division statement.
// Val is the second argument of division.
void __sanitizer_cov_trace_div4(uint32_t Val);
void __sanitizer_cov_trace_div8(uint64_t Val);
// Called before a GetElementPtr (GEP) instruction
// for every non-constant array index.
void __sanitizer_cov_trace_gep(uintptr_t Idx);
// Called before a load of appropriate size. Addr is the address of the load.
void __sanitizer_cov_load1(uint8_t *addr);
void __sanitizer_cov_load2(uint16_t *addr);
void __sanitizer_cov_load4(uint32_t *addr);
void __sanitizer_cov_load8(uint64_t *addr);
void __sanitizer_cov_load16(__int128 *addr);
// Called before a store of appropriate size. Addr is the address of the store.
void __sanitizer_cov_store1(uint8_t *addr);
void __sanitizer_cov_store2(uint16_t *addr);
void __sanitizer_cov_store4(uint32_t *addr);
void __sanitizer_cov_store8(uint64_t *addr);
void __sanitizer_cov_store16(__int128 *addr);
16.9 رهگیری جريان كنترل (Tracing control flow)
با فعالسازی گزینه -fsanitize-coverage=control-flow، کامپایلر برای هر تابع، جدولی جهت ثبت اطلاعات Control Flow ایجاد میکند. بهطور مشخص، برای هر بلوک پایه (Basic Block) در تابع، دو فهرست تولید میشود:
- فهرستی از بلوکهای جانشین (Successors) آن بلوک.
- فهرستی از توابع غیرIntrinsic که در آن بلوک فراخوانی شدهاند (Non-Intrinsic Called Functions).
نکته: در پیادهسازی فعلی، فراخوانیهای غیرمستقیم (Indirect Calls) ردیابی نمیشوند و تنها با یک مقدار ویژه (1-) در این فهرست علامتگذاری میشوند.
هر سطر از این جدول شامل آدرس بلوک پایه بههمراه دو فهرست خاتمهیافته با مقدار NULL (Null-Terminated Lists) از Successorها و Calleeها است. این جدول در بخشی اختصاصی از فایل باینری با نام sancov_cfs ذخیره و کدگذاری میشود.
برای مثال:
int foo (int x) {
if (x > 0)
bar(x);
else
x = 0;
return x;
}
کد فوق شامل ۴ بلوک پایه (Basic Block) است که میتوان آنها را بهترتیب با حروف A، B، C و D نامگذاری کرد:
A
|\
| \
B C
| /
|/
D
جدول جریان کنترل (control flow) جمعآوري شده به صورت زير است:
A, B, C, null, null, B, D, null, @bar, null, C, D, null, null, D, null, null
كاربران بايد يك تابع واحد براي ثبت جدول CF در هنگام startup پيادهسازي كنند:
extern "C"
void __sanitizer_cov_cfs_init(const uintptr_t *cfs_beg,
const uintptr_t *cfs_end) {
// [cfs_beg,cfs_end) is the array of ptr-sized integers representing
// the collected control flow.
}
16.10 رهگیری عمق پشته (Tracing Stack Depth)
با فعالسازی گزینه -fsanitize-coverage=stack-depth، کامپایلر میزان مصرف پشته (Stack) در زنجیره فراخوانی توابع را ردیابی میکند. در این حالت، تنها توابع غیرLeaf تحت ابزارگذاری (Instrumentation) قرار میگیرند و توابع Leaf در این فرایند در نظر گرفته نمیشوند.
حداکثر عمق پشته فراخوانی (Call Stack) در یک متغیر محلی نخ (Thread-Local) با نام __sancov_lowest_stack ذخیره میشود. برای این منظور، در ابتدای هر تابع غیرLeaf کدی به برنامه افزوده میشود که مقدار Frame Pointer جاری را با مقدار ذخیرهشده در __sancov_lowest_stack مقایسه میکند. اگر مقدار Frame Pointer فعلی کوچکتر باشد (که نشاندهنده مصرف بیشتر Stack است)، مقدار جدید در این متغیر ذخیره میشود.
در عمل، ابزارگذاری (Instrumentation) کدی مشابه نمونه زیر را به هر تابع غیرLeaf اضافه میکند:
extern thread_local uintptr_t __sancov_lowest_stack;
uintptr_t stack = (uintptr_t)__builtin_frame_address(0);
if (stack < __sancov_lowest_stack)
__sancov_lowest_stack = stack;
در صورتی که گزینه -fsanitize-coverage-stack-depth-callback-min=N (که در آن N > 0 است) نیز فعال شود، ثبت عمق Stack از طریق Callback با نام __sanitizer_cov_stack_depth انجام میشود؛ در این حالت، بهجای آنکه ابزارگذاری (Instrumentation) برای بهروزرسانی متغیر __sancov_lowest_stack به کد برنامه افزوده شود، مسئولیت این کار به تابع فراخوانی (Callback) واگذار میشود.
مقدار N تعیین میکند که کدام توابع تحت ابزارگذاری (Instrumentation) قرار گیرند. تنها توابعی که کامپایلر تخمین میزند حداقل N بایت از Stack را مصرف میکنند، بهگونهای ابزارگذاری (Instrumentation) میشوند که Tracing Callback را فراخوانی کنند. بااینحال، در مورد توابعی که اندازه Stack آنها بهصورت پویا (Dynamic) تعیین میشود، فراخوانی (Callback) بدون هیچ محدودیتی به کد افزوده خواهد شد.
این فراخوانی (Callback) هیچ آرگومانی دریافت نمیکند و مسئول اندازهگیری میزان مصرف Stack، انجام مقایسههای لازم و ذخیرهسازی نتایج است. به بیان دیگر، منطق بهروزرسانی متغیر __sancov_lowest_stack از ابزارگذاری (Instrumentation) به پیادهسازی Runtime منتقل میشود. یک پیادهسازی تقریبی از رفتار __sancov_lowest_stack با استفاده از این فراخوانی (Callback) به صورت زیر خواهد بود:
void __sanitizer_cov_stack_depth(void) {
uintptr_t stack = (uintptr_t)__builtin_frame_address(0);
if (stack < __sancov_lowest_stack)
__sancov_lowest_stack = stack;
}
16.11 فراخواني مشروط Callbackها
با فعالسازی گزینه -sanitizer-coverage-gated-trace-callbacks، فراخوانی Tracing Callbackها بهصورت مشروط (Gated) انجام میشود. در این حالت، Instrumentation پیش از فراخوانی هر Callback، مقدار یک متغیر سراسری (Global Variable) را بررسی میکند. اگر Tracing غیرفعال باشد، تنها یک انشعاب ساده (Trivial Branch) اجرا شده و از فراخوانی تابع Callback صرفنظر میشود؛ در نتیجه، سربار ناشی از فراخوانی توابع به میزان قابلتوجهی کاهش مییابد. در مقابل، اگر Tracing فعال باشد، Callback متناظر بهصورت معمول فراخوانی خواهد شد.
مسئولیت فعال یا غیرفعال کردن Tracing از طریق تنظیم مقدار این متغیر سراسری بر عهده Runtime است. در حال حاضر، این قابلیت تنها برای حالتهای trace-pc-guard و trace-cmp پشتیبانی میشود.
16.12 غیر فعال کردن instrumentation با استفاده از attribute ((no_sanitize(“coverage”)))
امکان غیرفعال کردن Coverage Instrumentation برای توابع خاص با استفاده از ویژگی __attribute__((no_sanitize("coverage"))) وجود دارد. با اعمال این Attribute، کامپایلر از افزودن Instrumentation مربوط به SanitizerCoverage به تابع موردنظر خودداری میکند. از آنجا که این Attribute ممکن است توسط همه کامپایلرها پشتیبانی نشود، توصیه میشود از آن بههمراه بررسی قابلیت __has_feature(coverage_sanitizer) استفاده شود تا تنها در کامپایلرهایی که از SanitizerCoverage پشتیبانی میکنند، این ویژگی اعمال شود.
16.13 غیر فعال کردن ابزارگذاری بدون تغيير منبع كد (Disabling instrumentation without changing the source code)
در برخی موارد، لازم است SanitizerCoverage تنها روی زیرمجموعهای از توابع هدف Instrumentation انجام دهد، بدون آنکه نیازی به تغییر فایلهای کد منبع باشد. این قابلیت با استفاده از گزینههای -fsanitize-coverage-allowlist=allowlist.txt و -fsanitize-coverage-ignorelist=blocklist.txt فراهم میشود. به کمک یک Allowlist و یک Blocklist میتوان دقیقاً مشخص کرد که کدام فایلها و توابع باید تحت ابزارگذاری (Instrumentation) قرار گیرند. SanitizerCoverage تنها توابعی را ابزارگذاری (Instrument) میکند که هر دو شرط زیر را داشته باشند:
- تابع در فایلی تعریف شده باشد که در Allowlist قرار داشته و همزمان در Blocklist وجود نداشته باشد.
- نام Mangled تابع نیز در Allowlist موجود بوده و در Blocklist قرار نداشته باشد.
قالب فایلهای Allowlist و Blocklist مشابه قالب مورد استفاده در Sanitizer Blocklist است. بهصورت پیشفرض:
- Allowlist تمامی فایلها و توابع را شامل میشود.
- Blocklist هیچ فایل یا تابعی را شامل نمیشود.
یکی از کاربردهای رایج این قابلیت، تعریف پوشهها یا فایلهای منبع موردنظر در Allowlist و مجاز دانستن تمامی توابع آنها است؛ سپس میتوان با استفاده از Blocklist، فایلها یا توابع خاصی را که بهصورت کلی توسط Allowlist انتخاب شدهاند، از فرایند Instrumentation مستثنی کرد.
در ادامه، نمونهای از یک فایل Allowlist آورده شده است:
# Enable instrumentation for a whole folder
src:bar/*
# Enable instrumentation for a specific source file
src:foo/a.cpp
# Enable instrumentation for all functions in those files
fun:*
در ادامه یک نمونه از blocklist:
# Disable instrumentation for a specific source file that the allowlist allowed
src:bar/b.cpp
# Disable instrumentation for a specific function that the allowlist allowed
fun:*myFunc*
استفاده از کاراکتر جایگزین (Wildcard) * در مثال فوق ضروری است، زیرا تطبیق نام توابع پس از فرایند Name Mangling انجام میشود. در صورت عدم استفاده از Wildcard، باید نام کامل Mangled هر تابع بهصورت دقیق در فهرست ذکر شود. همچنین توجه داشته باشید که مسیر فایلهای کد منبع باید دقیقاً با همان مسیری مطابقت داشته باشد که هنگام اجرای Clang در خط فرمان به کامپایلر ارائه شده است.
برای مثال، Allowlist فوق فایل bar/b.cpp را تنها در صورتی شامل میشود که مسیر فایل دقیقاً به همین شکل مشخص شده باشد. اگر همان فایل با مسیر دیگری، مانند ./bar/b.cpp یا در سیستمعامل ویندوز به صورت bar\b.cpp معرفی شود، دیگر با این قانون مطابقت نخواهد داشت و در فهرست قرار نخواهد گرفت. بنابراین، همواره بررسی کنید که Allowlist و Blocklist موردنظر شما دقیقاً مطابق با نام توابع و مسیر فایلهایی باشند که در فرایند کامپایل استفاده شدهاند تا قوانین تعریفشده بهدرستی اعمال شوند.
16.14 پيادهسازي پيشفرض
Runtime مربوط به سنیتایزرها (Sanitizer) (مانند AddressSanitizer، MemorySanitizer و سایر ابزارهای مشابه) پیادهسازی پیشفرضی از برخی فراخوانیهای (Callback) مربوط به SanitizerCoverage را ارائه میکنند. با استفاده از این پیادهسازی، میتوان اطلاعات پوشش (Coverage ) را بهصورت خودکار هنگام خروج (Exit) فرآیند در قالب فایل روی دیسک ذخیره کرد، بدون آنکه نیازی به پیادهسازی اختصاصی این فراخوانیهای (Callback) باشد.
برای مثال:
% cat -n cov.cc
#include
__attribute__((noinline))
void foo() { printf("foo\n"); }
int main(int argc, char **argv) {
if (argc == 2)
foo();
printf("main\n");
}
% clang++ -g cov.cc -fsanitize=address -fsanitize-coverage=trace-pc-guard
% ASAN_OPTIONS=coverage=1 ./a.out; wc -c *.sancov
main
SanitizerCoverage: ./a.out.7312.sancov 2 PCs written
24 a.out.7312.sancov
% ASAN_OPTIONS=coverage=1 ./a.out foo ; wc -c *.sancov
foo
main
SanitizerCoverage: ./a.out.7316.sancov 3 PCs written
24 a.out.7312.sancov
32 a.out.7316.sancov
هر بار که یک فایل اجرایی (Executable) که با SanitizerCoverage ابزارگذاری (Instrument) شده است اجرا شود، در زمان پایان یافتن (Shutdown) فرآیند، یک فایل با پسوند .sancov ایجاد میشود. همچنین، اگر این فایل اجرایی بهصورت پویا (Dynamic) به کتابخانههای اشتراکی (DSO) که آنها نیز با SanitizerCoverage ابزارگذاری شدهاند لینک شده باشد، برای هر یک از این DSOها نیز یک فایل جداگانه با پسوند .sancov تولید خواهد شد.
16.15 فرمت دادههاي Sancov یا Sancov data format
فرمت فايلهاي *.sancov بسيار ساده است:
- ۸ بایت اول به عنوان magic است، كه يكي از مقادير 0xC0BFFFFFFFFFFF64 يا 0xC0BFFFFFFFFFFF32 است.
- آخرين بایت magic اندازه offsetهاي بعدي را تعيين ميكند.
- بقيه دادهها شامل offsetها در باینری/DSO مربوطه است كه در طول اجرا اجرا شدهاند.
16.16 ابزار Sancov
ابزار سادهای با نام sancov برای پردازش فایلهای Coverage ارائه شده است. این ابزار بخشی از پروژه LLVM محسوب میشود و در حال حاضر تنها از سیستمعامل Linux پشتیبانی میکند. همچنین، sancov قادر است عملیات Symbolization را بهصورت خودکار و بدون نیاز به پشتیبانی یا پیکربندی اضافی از سوی محیط اجرا انجام دهد.
برای استفاده از این ابزار، باید فایلهای .sancov (با قالب نامگذاری <module_name>.<pid>.sancov) به همراه مسیر تمامی فایلهای باینری ELF متناظر در اختیار آن قرار گیرند. ابزار sancov با استفاده از نام ماژول (Module Name)، هر فایل .sancov را با فایل باینری ELF متناظر خود تطبیق داده و اطلاعات پوشش کد را پردازش میکند.
USAGE: sancov [options] (|)...
Action (required)
-print - Print coverage addresses
-covered-functions - Print all covered functions.
-not-covered-functions - Print all not covered functions.
-symbolize - Symbolizes the report.
Options
-blocklist= - Blocklist file (sanitizer blocklist format).
-demangle - Print demangled function name.
-strip_path_prefix= - Strip this prefix from file paths in reports
16.17 گزارشهای پوشش (Coverage Reports)
فایلهای .sancov بهتنهایی اطلاعات کافی برای تولید گزارش پوشش یا Coverage در سطح کد منبع (Source-Level Coverage) را در اختیار قرار نمیدهند. بخشی از اطلاعات موردنیاز در اطلاعات اشکالزدایی (Debug Information) موجود در فایل باینری ذخیره شده است. ازاینرو، برای تولید فایل .symcov، ابتدا باید فایل .sancov فرایند Symbolization را طی کند تا آدرسهای ثبت شده به نمادها و موقعیتهای متناظر در کد منبع نگاشت شوند.
sancov -symbolize my_program.123.sancov my_program > my_program.123.symcov
فايل symcov*. ميتواند بر روي سورس كد مرور شود با استفاده از اسكريپت tools/sancov/coverage-report-server.py كه يك HTTP server راهاندازي ميكند.
16.18شاخه خروجي (Output directory)
به طور پيشفرض، فايلهاي sancov.* در شاخه كاري فعلي (current working directory) ساخته ميشوند. اين شاخه با استفاده از ASAN_OPTIONS=coverage_dir=/path قابل تغيير است:
ASAN_OPTIONS="coverage=1:coverage_dir=/tmp/cov" ./a.out foo
ls -l /tmp/cov/*sancov
-rw-r----- 1 kcc eng 4 Nov 27 12:21 a.out.22673.sancov
-rw-r----- 1 kcc eng 8 Nov 27 12:21 a.out.22679.sancov
پاورقی:
[1] https://clang.llvm.org/docs/DataFlowSanitizerDesign.html
[2] https://clang.llvm.org/docs/LanguageExtensions.html#langext-has-feature-has-extension
[3] https://clang.llvm.org/docs/SanitizerSpecialCaseList.html
[4] https://github.com/llvm/llvm-project/issues?q=is%3Aissue%20state%3Aopen%20TySan%20label%3Acompiler-rt%3Atysan