Смекни!
smekni.com

Побудова надійних операційних систем, що допускають наявність ненадійних драйверів пристроїв (стр. 4 из 9)

У монолітних системах зазвичай відсутня можливість виявлення збійних драйверів «на льоту», хоча є дані про деякі дослідження в цій області [25]. Тим не менше, заміна на льоту ядерного драйвера є складною справою, оскільки до часу заміни він може утримувати ядерні блокування або знаходитися в критичному ділянці.

Обмеження зловживань переповнювання буфера

Відомо, що переповнення буферів є рясним джерелом помилок, наявністю яких інтенсивно користуються віруси і черв'яки. Хоча наша розробка спрямована радше на боротьбу з помилками, а не із зловмисними кодом, деякі засоби нашої системи надають захист від певних видів зловживань. Оскільки наше ядро є мінімальним, і в ньому використовується тільки статичне розміщення даних, виникнення проблеми малоймовірно в найбільш чутливої частини системи. Якщо переповнення буферу трапляється в одному з користувацьких процесів, то проблема не є надто серйозною, оскільки сервери і драйвери, що виконуються в режимі користувача, володіють обмеженими можливостями.

Крім того, в нашій системі виконується тільки код, розташований в сегментах тексту, які доступні тільки з читання. Хоча це не запобігає можливість переповнення буфера, ускладнюється можливість зловживання, оскільки надлишкові дані, що знаходяться в стеці або купі, неможливо виконати як код. Цей захисний механізм є виключно важливим, оскільки він запобігає зараження вірусами і черв'яками та виконання їх власного коду. Сценарій найгіршого випадку змінюється від взяття безпосереднього управління до перезапису адреси повернення в стеку та виконання деякої існуючої бібліотечної процедури. Найбільш відомий приклад такої ситуації часто називають атакою шляхом «повернення в libc» («return-to-libc»), і цей спосіб атаки вважається набагато більш складним, ніж виконання коду в стеці або купі.

На відміну від цього, в монолітних системах купуються повноваження супер, якщо переповнення буферу відбувається в будь-якій частині операційної системи. Більш того, в багатьох монолітних системах допускається виконання коду в стеці або купі, що істотно спрощує зловживання переповнювання буфера.

Забезпечення надійного IPC

Добре відомою проблемою механізмів обміну повідомленнями є управління буферами, але в нашому варіанті комунікаційних примітивів ми повністю уникаємо цієї проблеми. У нашому механізмі синхронної передачі повідомлень використовуються рандеву, в результаті чого усувається потреба в буферизації і управлінні буферами, а також відсутня проблема вичерпання ресурсів. Якщо одержувач не очікує повідомлення, то примітив SEND блокує відправника. Аналогічно, примітив RECEIVE блокує процес, якщо немає повідомлення, що очікує свого отримання. Це означає, що для заданого процесу в таблиці процесів у будь-який час повинен зберігатися єдиний вказівник на буфер повідомлення.

На додаток до цього, у нас є механізм асинхронної передачі повідомлень NOTIFY, який також не є чутливим до вичерпання ресурсів. Повідомлення є типізовані, і для кожного процесу зберігається тільки один біт для кожного типу. Хоча обсяг інформації, яку можна передати таким чином, обмежений, цей підхід був обраний з-за своєї надійності.

До речі, зауважимо, що у своєму IPC ми уникаємо переповнювання буфера шляхом обмеження засобів комунікації короткими повідомленнями фіксованої довжини. Повідомлення є об'єднанням декількох типізованих форматів повідомлень, так що розмір автоматично вибирається компілятором, як розмір найбільшого допустимого типу повідомлень, який залежить від розміру цілих чисел і покажчиків. Цей механізм передачі повідомлень використовується для всіх запитів і відповідей.

Обмеження IPC

IPC – це потужний механізмом, який потребує строгого контролі. Оскільки наш механізм передачі повідомлень є синхронним, процес, що виконує примітив IPC, блокується, поки обидва учасника не стануть готовими. Користувальницький процес може легко зловживати цим властивістю для завішування системних процесів шляхом посилки запиту без очікування відповіді. Тому є інший примітив IPC SENDREC, що комбінує в одному виклик SEND і RECEIVE. Він блокує відправника до отримання відповіді на запит. З метою захисту операційної системи цей примітив є єдиним, який можна використовувати звичайним користувачам. Насправді, в ядрі для кожного процесу підтримується бітовий масив для обмеження примітивів IPC, які дозволяється використовувати даному процесу.

Крім того, в ядрі підтримується бітовий масив, що визначає, з якими драйверами і серверами може взаємодіяти даний процес. Ця маска посилки повідомлень являє собою механізм, що запобігає безпосередню посилку повідомлень драйверам від користувацьких процесів. Натомість цього, їм дозволяється спілкуватися тільки з серверами, що забезпечують POSIX-дзвінки. Однак маска посилки повідомлень використовується також і для запобігання посилки (непередбаченого) повідомлення, скажімо, від драйвера клавіатури аудіо-драйверу. Знову шляхом суворої інкапсуляції можливостей кожного процесу ми можемо в значній мірі запобігти поширенню неминучих помилок в драйверах і їх вплив на інші частини системи.

На відміну від цього, в монолітній системі будь-який драйвер може викликати будь-який шматок коду в ядрі, використовуючи машинну інструкцію виклику підпрограми (або, ще гірше, інструкцію повернення з підпрограми, якщо стек був перезаписаний через переповнювання буфера), що дозволяє проблем, що виникають в одній підсистемі, поширюватися в інші підсистеми.

Уникання тупиків

Оскільки за замовчуванням для IPC використовуються синхронні виклики SEND і RECEIVE, можуть виникати тупики, коли два або більше число процесів одночасно намагаються обмінюватися повідомленнями, і всі процеси блокуються в очікуванні один одного. Тому ми ретельно розробляли протокол уникнення тупиків, що приписує часткове, що сходить впорядкування повідомлень.

Впорядкування повідомлень приблизно відповідає розбивка на рівні, описаного в розд. 2.2. Наприклад, звичайним користувальницьким процесам дозволяється тільки посилати повідомлення з використанням примітиву SENDREC серверів, які реалізують інтерфейс POSIX, а ці сервери можуть запитувати сервіси від драйверів, які, у свою чергу, можуть виробляти виклики ядра. Однак для асинхронних подій, таких як переривання і таймери, потрібні повідомлення, що посилаються в протилежному напрямку, від ядра сервера або драйверу. Використання синхронних викликів SEND для передачі цих подій може легко призвести до глухого кута. Ми уникаємо цієї проблеми шляхом використання для асинхронних подій механізму NOTIFY, який ніколи не блокує викликає бік. Якщо оповестітельное повідомлення не може бути доставлено процесу-адресату, воно зберігається в його елементі таблиці процесів до тих пір, поки він не виконає RECEIVE.

Хоча протокол уникнення тупиків підтримується обговорювалося вище механізмом масок посилки повідомлень, ми також реалізували в ядрі розпізнавання тупиків. Якщо виклик примітиву в деякому процесі непередбачуваних чином привів би до виникнення безвиході, то виконання примітиву не проводиться, і закликають учасника повертається повідомлення про помилку.

Уніфікація переривань і повідомлень

Базовим механізмом IPC є передача повідомлень на основі рандеву, але потрібні й асинхронні повідомлення, наприклад, для надання інформації про переривання, що є потенційним джерелом помилок в операційних системах. Ми суттєво зменшили тут шанси на появу помилок, уніфікувавши асинхронні сигнали та повідомлення. Зазвичай, коли деякий процес посилає повідомлення іншому процесу і одержувач не є готовим, відправник блокується. Ця схема не працює для переривань, оскільки обробник переривань не може дозволити собі блокування. Замість цього використовується асинхронний механізм сповіщень, при використанні якого обробник переривань виробляє виклик NOTIFY для драйвера. Якщо драйвер очікує повідомлення, то сповіщення доставляється безпосередньо. Якщо він його не очікує, то сповіщення зберігається в бітові масиви до тих пір, поки згодом драйвер не виконає виклик RECEIVE.

Обмеження функціональних можливостей драйвера

Ядро експортує обмежений набір функцій, які можна викликати ззовні. Цей ядерний API представляє собою єдиний спосіб взаємодії драйвера з ядром. Однак не кожному драйверу дозволяється використовувати будь-який виклик ядра. Для кожного драйвера в ядрі (в таблиці процесів) підтримується бітовий масив, який показує, які виклики ядра може виробляти цей драйвер. Гранулярні викликів ядра є досить дрібною. Відсутній мультиплексування викликів в один і той же номер функції. Кожен виклик індивідуально захищається власним бітом в бітові масиви. Проте на внутрішньому рівні кілька викликів може оброблятися однієї і тієї ж ядерної функцією. Цей метод дозволяє реалізувати детальне керування доступом до ядра.

Наприклад, деяким драйверам потрібен доступ по читанню і запису до даних, що знаходяться в призначених для користувача адресних просторах, але виклики для читання і запису в цих просторах є різними. Так що ми не мультіплексіруем читання і запис в один виклик з використанням параметра «напрямок». Відповідно, можна дозволити, наприклад, драйверу принтера виконувати виклик ядра для читання даних з користувацьких процесів, але не дозволяти виконання викликів для запису. Внаслідок цього помилка в драйвері, якому дозволено тільки читання, не може призвести до випадкового пошкодження користувацького адресного простору.

Порівняємо цю ситуацію з можливим поведінкою драйвера у монолітному ядрі. Помилка в коді може призвести до запису в адресний простір користувацького процесу замість читання з нього, що зруйнує процес. Крім того, ядерний драйвер може викликати будь-яку функцію в усьому ядрі, включаючи функції, які не повинні викликатися драйверами. Оскільки відсутня будь-яка внутрішньоядерні захист, це практично неможливо запобігти. У нашій розробці жоден драйвер не може викликати ядерну функцію, яка не була явно експортована як частина інтерфейсу між ядром і цим драйвером.