لینک کوتاه مطلب : https://hsgar.com/?p=4068

تجزیه و تحلیل هسته OpenBSD با دانش اختصاصی دامنه | توسط کریستین لودویگ | ژوئن، 2022

در این مقاله می‌خواهم نشان دهم که چگونه می‌توانیم آن را تحلیل کنیم OpenBSD هسته با دانش خاص دامنه spl(9) قفل کردن اولیه ها، برای یافتن خطاهای برنامه نویسی.

این کار با حمایت مالی Genua GmbH. Genua از OpenBSD به عنوان پایه بسیاری از محصولات خود استفاده می کند. آنها به توسعه دهندگان این امکان را می دهند که دو بار در سال به مدت یک هفته یک عقب نشینی داشته باشند تا پروژه های خود را هک کنند. این کار نتیجه آن است.

هسته OpenBSD به زبان برنامه نویسی C نوشته شده است. این بدان معناست که برنامه نویسان باید به تنهایی از مصرف منابع مراقبت کنند. به عنوان مثال، اگر یک قفل را در یک تابع بگیرید، باید در تمام شرایطی که تابع برمی‌گردد، قفل آزاد شود. کامپایلر C قبلاً کد را بررسی می کند، پس چرا آن را از قفل کردن اولیه ها آگاه نکنیم؟ به این ترتیب زمانی که یک قفل توسط یک تابع گرفته شده است، اما در بازگشت هیچ تابعی آزاد نشده است، می تواند یک هشدار منتشر کند.

برای مثال تابع زیر را در نظر بگیرید:

void function(void) {
lock();
perform_work();
unlock();
}

حال تصور کنید که قبل از انجام کار باید ابتدا یک پیش شرط را بررسی کنیم. کد را می توان به شکل زیر تغییر داد:

void function(void) {
lock();
if (!precondition)
return;
perform_work();
unlock();
}

می‌بینید که وقتی پیش‌شرط برآورده نشده باشد، عملیات باز کردن قفل را از دست می‌دهیم. ممکن است در مثال بالا واضح باشد، اما وقتی یک بلوک بزرگ و پیچیده از کد دارید که زیر یک قفل اجرا می‌شود، ممکن است یک عملیات باز کردن قفل از دست رفته بلافاصله قابل مشاهده نباشد.

OpenBSD مدتی است که از LLVM/clang استفاده می کند. این مجموعه کامپایلر امکان استفاده از پلاگین ها را برای گسترش کامپایلر و یادگیری ترفندهای جدید می دهد. ما می‌توانیم از این مکانیسم برای آگاه کردن کامپایلر از قفل‌های اولیه استفاده شده در هسته OpenBSD استفاده کنیم. ساده ترین قفل برای تجزیه و تحلیل این است spl(9). تنها یک قفل و یک عملیات باز کردن قفل در یک قفل تک، سراسری وجود دارد. این بدان معناست که ما نیازی به تمایز بین اشیاء قفل مختلف نداریم. همچنین، عملیات تلاش برای قفل وجود ندارد، که تجزیه و تحلیل را پیچیده تر می کند. پس بیایید با بررسی spl(9) شروع کنیم. به عنوان اولین قدم ابتدایی، ما فقط باید تعداد عملیات قفل و تعداد عملیات باز کردن قفل را در هر شاخه شرطی از همه توابع بشماریم. اگر آنها یکسان باشند، عملیات قفل به طور یکنواخت متعادل است و خطای قفل وجود ندارد.

خوب، این یک دیدگاه بسیار ساده لوحانه از جهان است. در واقع، کد بسیار پیچیده تر است. اما بیایید ببینیم که تا کجا ما را می رساند.

LLVM/clang از نوشتن افزونه ها در C++ پشتیبانی می کند. همچنین اتصالاتی به زبان های دیگر وجود دارد که قابل توجه ترین آنها پایتون است. حتی به نظر می رسد که نسخه پایه clang OpenBSD از افزونه ها پشتیبانی می کند. همچنین نمونه هایی از LLVM بالادست وارد شده است. با این حال، مستندات مربوط به نحوه نوشتن یک افزونه نسبتاً کم است. و من انتظار ندارم که OpenBSD حتی پیوندهای زبان را ارسال کند.

اما یک ماژول پایتون وجود دارد که یک تجزیه کننده زبان C را پیاده سازی می کند، که می تواند برای تجزیه فایل های منبع C به یک درخت نحو انتزاعی (AST) برای بازرسی استفاده شود. نامیده می شود pycparserو در توضیحات آن آمده است:

pycparser یک تجزیه کننده کامل زبان C است که با پایتون خالص با استفاده از کتابخانه تجزیه PLY نوشته شده است. این کد C را به یک AST تجزیه می کند و می تواند به عنوان جلویی برای کامپایلرهای C یا ابزارهای تجزیه و تحلیل عمل کند.

در حالی که pycparser ویژگی های استاندارد C99 را پیاده سازی می کند، برای برخی از کدها در هسته OpenBSD این کافی نیست. کتابخانه pycparser برخی از ویژگی‌های C یا برخی از ساختارهای مشابه را مدیریت نمی‌کند __typeof. علاوه بر این، زیرسیستم DRM به خوبی از پسوندهای گنو استفاده می کند، زیرا از لینوکس وارد شده است. خوشبختانه وجود دارد pycparserext، pycparser اصلی را گسترش می دهد. توضیحاتش میگه:

قابلیت توسعه یافته برای Eli Bendersky’s pycparser، به ویژه از تجزیه پسوندهای گنو و OpenCL پشتیبانی می کند.

به نظر همان چیزی است که ما می خواهیم. من ابزاری در اطراف pycparserext برای تجزیه و تحلیل هسته OpenBSD نوشته ام. نامیده می شود بررسی کننده تعادل قفل (lbc). در ادامه شرحی از ویژگی های اجرا و درس های آموخته شده است.

بررسی کننده تعادل قفل

کتابخانه pycparserext نمی تواند فایل های منبع ساده را به طور مستقیم مدیریت کند. برای ساختن صحیح یک AST، باید در مورد مسیرهای شامل و تعریف پیش پردازنده بدانیم. از این رو، یک وصله به درخت منبع OpenBSD وجود دارد که پیکربندی (8) و Makefile هسته (برای AMD64) را برای انتشار فایل‌های منبع از پیش پردازش شده تغییر می‌دهد. این فایل ها مستقل هستند – هیچ وابستگی خارجی وجود ندارد. اینها را می توان برای تجزیه و تحلیل به lbc داد.

پس از خواندن فایل، lbc باید آن را بیشتر از قبل پردازش کند تا از شر ساختارهای کد C خلاص شود که pycparserext آنها را درک نمی کند. آنها با چیزی محجوب جایگزین می شوند. تنها چیزی که تاکنون از دست رفته پشتیبانی از یک برنامه افزودنی گنو برای اپراتور سه تایی است. اظهاراتی مانند foo = foo ?: bar; نمی توان به درستی تجزیه کرد. این اپراتور در کد DRM که از لینوکس گرفته شده است استفاده می شود. پس از پردازش ورودی، pycparserext یک AST از فایل می‌سازد و lbc شروع به تجزیه و تحلیل هر تابع موجود در آن می‌کند. کتابخانه pycparserext یک الگوی بازدیدکننده را پیاده سازی می کند. lbc فقط توابعی را برای گره های AST اجرا می کند که جالب هستند. اینها شامل گره های فراخوانی تابع برای بررسی اینکه آیا هر یک از موارد اولیه قفل فراخوانی شده است یا خیر.

AST تولید شده درختی است که ابتدا در عمق راه می رود. راه رفتن روی درخت، یا بازدید از هر گره در شرایط pycparser، می تواند به عنوان یک تکرار ساده در هر خط کد منبع مشاهده شود. آنالایزر lbc هنگام پرواز چیزهای جالبی مانند حالت قفل را یادداشت می کند. اما هنگامی که جریان کد تقسیم می شود، وضعیت تحلیلگر داخلی نیز باید تقسیم شود. در lbc، این با انشعاب فرآیند جاری محقق می شود. یک برنامه به تجزیه و تحلیل یک بخش ادامه می دهد، در حالی که برنامه دیگر قسمت تقسیم شده را تجزیه و تحلیل می کند. این برای آن مفید است switch موارد وقتی lbc به a می رسد break یا continue بیانیه، باید تکرار بر روی درخت فرعی فعلی را متوقف کند و به مکان قبلی بازگردد. این امر با استثناء محقق می شود. هر تجزیه و تحلیل بدنه حلقه یا دستور سوئیچ نقطه ای دارد که این استثناها در آن جمع می شوند و تجزیه و تحلیل می تواند از آنجا ادامه یابد.

دست و پا گیر ترین قسمت، جابجایی استgoto بیانیه. روشی که pycparser برچسب ها را مدیریت می کند با قرار دادن عبارت بعد از برچسب به عنوان درخت فرعی در زیر گره برچسب است. این بدان معنی است که ما نمی توانیم به سادگی به یک گره برچسب “پرش” کنیم، فقط یک دستورالعمل را که به آن متصل شده است تجزیه و تحلیل می کنیم. به عنوان یک راه حل، lbc همه برچسب ها را در هنگام پرواز جمع آوری می کند و آنها را در آن ارزیابی می کند goto بازدید کننده. برای جهش به عقب، برچسب قبلاً بازدید شده است. دنبال کردن مجدد برچسب به طور بالقوه به یک حلقه بی نهایت منجر می شود. اگر حلقه دارای شکاف هایی باشد، جریان های دیگر از آنها پیروی می کنند. lbc پردازش این جریان را به طور کلی متوقف می کند. در صورت پرش به جلو، lbc تمام گره های AST بعدی را در طول بازدید خود نادیده می گیرد تا زمانی که به برچسب برسد. سپس به طور معمول ادامه می یابد. وقتی برچسب‌ها در حلقه‌ها یا سوئیچ‌ها قرار می‌گیرند، این موضوع به‌ویژه مودار است.

در نهایت، تجزیه و تحلیل کد یا در انتهای یک تابع یا در یک دستور بازگشت متوقف می شود. در هر دو مورد، قفل اولیه باید به شیوه ای متعادل فراخوانی می شد. باید به همان تعداد تماس برای عملکردهای قفل وجود داشته باشد که برای عملکردهای باز کردن قفل وجود دارد. اگر اینطور نیست، lbc شکایت می کند. در panic() عملکرد به روش خاصی انجام می شود. هنگامی که این تابع فراخوانی می شود، برنامه نویس به هر حال منصرف شده است، که نشان می دهد سیستم از کار افتاده است. این همچنین به این معنی است که ما نمی‌توانیم هیچ قضاوت معقولی در مورد وضعیت تعادل قفل داشته باشیم و پردازش جریان را متوقف کنیم.

با توجه به فرضیات فروتنانه ای که با آن شروع کردیم، بررسی کننده تعادل قفل بسیار عالی است. موارد مثبت کاذب زیادی وجود دارد. به عنوان مثال، تمام توابع wrapper در اطراف spl(9) علامت گذاری می شوند. این شامل mtx_enter_try() و mtx_enter_leave()که برای کلاس دیگری از قفل ها استفاده می شوند و بر spl(9) تکیه دارند. پیام های ارسال شده به شرح زیر است:

kern_lock.i: mtx_enter_try(): Error: Unbalanced lock status at return statement. LockStatus(spl=1)

kern_lock.i: mtx_leave(): Error: Unbalanced lock status at end of function. LockStatus(spl=-1)

این بدان معناست که lbc شعبه ای را پیدا کرده است که در آن یک عبارت بازگشتی را مشاهده می کند mtx_enter_try() جایی که spl(9) افزایش یافته است، اما دوباره کاهش نیافته است (شمارش مثبت است). برعکس، در mtx_leave() عملکرد با باز شدن قفل spl(9) یک عدد بیش از حد به پایان می رسد (تعداد منفی است). این نشانه آن است که از این توابع به عنوان لفاف استفاده می شود. واضح است که توابع mutex(9) wrapper هستند. با توابع دیگر، حداقل برای من کمتر آشکار است. به عنوان مثال sleep_setup() و sleep_finish().

بزرگترین منبع مثبت کاذب، سیستم فایل سریع است. این توابع زیادی دارد که spl(9) را قفل می کند، اما سپس یک تابع دیگر را فراخوانی می کند یا دوباره آن را باز می کند. از آنجایی که lbc فقط هر تابع را به صورت جداگانه تجزیه و تحلیل می کند، نمی تواند این کد را به درستی تجزیه و تحلیل کند و موارد مثبت نادرست را علامت گذاری می کند.

یکی از محدودیت های فعلی تحلیلگر با کدهایی مانند زیر است:

void
do_somthing(needs_lock) {
int s;
if (needs_lock)
s = splhigh();
[ ... ]
if (needs_lock)
splx(s);
}

تحلیلگر جریان را در هر شاخه در تابع تقسیم می کند. یک قسمت شاخه شرط if را تجزیه و تحلیل می کند، یکی دیگر شاخه شرط دیگری را که در این موارد خالی است. که منجر به چهار جریان مختلف می شود.

+-----+-------------+-------------+
| Nr. | top-if | bottom-if |
+-----+-------------+-------------+
| 1 | if-branch | if-branch |
| 2 | if-branch | else-branch |
| 3 | else-branch | if-branch |
| 4 | else-branch | else-branch |
+-----+-------------+-------------+

در جریان مشکلی وجود ندارد 1 و 4. هر دو جریان یا شاخه را می گیرند یا نمی گیرند. در هر دو مورد، قفل در پایان عملکرد متعادل است. دو مورد دیگر کد را تنها با یکی از شاخه های گرفته شده تجزیه و تحلیل می کنند. نتیجه یک وضعیت قفل نامتعادل در پایان عملکرد است. اگرچه، هنگام نگاه کردن به کد، مشخص است که فقط جریان دارد 1 و 4 معتبر هستند. یکی از راه‌ها این است که همه شرایط را جمع‌آوری کنید و توجه داشته باشید که آیا قبلاً گرفته شده‌اند یا خیر. البته این فقط در صورتی کار می کند که دقیقاً همان شرایط دوباره ظاهر شود، مانند مثال ساده ما در بالا.

من هم رسیدگی نکردم spl0()، که بعد از استفاده می شود fork() یا در طول cpu_configure() برای فعال کردن مجدد وقفه ها از هر سطحی که قبلا بوده است. این تابع استفاده می شود، زیرا سطح قبلی در پشته دیگری ذخیره شده است. از دیدگاه تحلیلگر، این یک عملیات باز کردن قفل است، بدون تماس قفل منطبق.

با وجود کاستی‌ها، اشکالات قانونی با آن ابزار یافت شد. در بخش بعدی به آنها پرداخته شده است.

Lock Balancing Checker برخی از اشکالات واقعی را در پایه کد OpenBSD پیدا کرد. با این حال، هیچ یک از آنها زمین را درهم نمی شکند.

یک عبارت بازگشت از دست رفته در یک مسیر خطا بود برای درایور رابط شبکه برای igc(4) یافت شد، که منجر به باز کردن دوبار می شود. باید قبول کرد که هیچ ضرری ندارد. در تئوری قفل spl(9) را می توان چندین بار بدون مشکل باز کرد. اما همچنان یک خطای معنایی باقی می ماند.

if_igc.i: igc_init(): Error: Unbalanced lock status at end of function. LockStatus(spl=-1)

تماس از دست رفته به splx() عملیات باز کردن قفل در یک مسیر خطا بود در درایور رابط شبکه بی سیم urtwn(4) یافت می شود. این جدی تر است. این ممکن است منجر به مسدود شدن وقفه های خاص شود و به طور بالقوه از پیشرفت سیستم جلوگیری کند. نتیجه نهایی یک دستگاه آویز است.

if_urtwn.i: urtwn_rx_frame(): Error: Unbalanced lock status at return statement. LockStatus(spl=1)

مشکل مشابهی برای آن شناسایی شده است uaq (4) و mcx (4).

متأسفانه، من نتوانستم یک پلاگین LLVM/clang بنویسم، همانطور که در ابتدا ذکر شد. اگر می توانستیم این کار را انجام دهیم و آن را به ساخت هسته متصل کنیم، اعتماد بیشتری به کد می داد. خوب، این بدان معناست که ابتدا از شر مثبت کاذب خلاص شوید. اما این یک تمرین برای خواننده باقی می ماند. 🙂

من نشان داده‌ام که تجزیه و تحلیل استاتیک هسته OpenBSD با بازده دانش خاص دامنه در اشکالات یافت می‌شود. ابزاری وجود دارد که حتی می توان آن را به انواع قفل ها تعمیم داد. MP-Lock با رابط API مشابه به ذهن می رسد.

باز هم از Genua تشکر می کنم که به من اجازه داد چنین پروژه جالبی را در ساعات کاری هک کنم، که به نفع محصولات آنها و جامعه OpenBSD است.

لینک منبع

ارسال یک پاسخ

آدرس ایمیل شما منتشر نخواهد شد.