Чи можливо програмно оцінити безпеку для довільного коду?


10

Останнім часом я багато думав про безпечний код. Нитка безпечна. Безпечний для пам'яті. Сейф, який не збирається вибухнути, в обличчя. Але задля ясності в питанні, давайте використовувати модель безпеки Руста як наше визначення.

Часто забезпечення безпеки є дещо важливою проблемою, оскільки, як підтверджено потребою Руста unsafe, існують дуже розумні ідеї програмування, наприклад, паралельність, які неможливо реалізувати в Rust без використання unsafeключового слова . Навіть незважаючи на те, що паралельність може бути абсолютно безпечною із блокуваннями, мютексами, каналами та ізоляцією пам’яті чи що у вас є, для цього потрібно працювати поза моделлю безпеки Руста unsafe, а потім вручну запевняти компілятора: «Так, я знаю, що я роблю Це виглядає небезпечно, але я довів, що це абсолютно безпечно ".

Але, як правило, це зводиться до ручного виготовлення моделей цих речей та доведення їх безпечності за допомогою доказів теореми . І з точки зору інформатики (чи це можливо), і з точки зору практичності (чи це забирає життя Всесвіту), чи розумно уявити програму, яка бере довільний код довільною мовою і оцінює, чи це " Іржа захищена "?

Застереження :

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

2
Довільний код? Ні. Я гадаю, ви навіть не можете довести безпеку найбільш корисного коду через виключення вводу-виводу та апаратних засобів.
Теластин

7
Чому ви нехтуєте проблемою зупинки? Кожен з прикладів, які ви згадали, та багато інших, було доведено, що рівноцінно вирішенню проблеми зупинки, задачі функції, теореми Райса або будь-якої іншої багатьох теорем про нерозбірливість: безпека покажчика, безпека пам’яті, нитка -безпека, безпека винятків, чистота, безпека вводу-виводу, безпека блокування, гарантії прогресу тощо. Проблема зупинки - це одне з найпростіших можливих статичних властивостей, про яке ви могли б хотіти знати, все, що ви перераховуєте, набагато складніше .
Йорг W Міттаг

3
Якщо ви дбаєте лише про помилкові позитиви та готові приймати помилкові негативи, у мене є алгоритм, який класифікує все: "Це безпечно? Ні"
Калет

Вам абсолютно не потрібно використовувати unsafeRust для написання одночасного коду. У них є декілька різних механізмів, починаючи від примітивів синхронізації до каналів, надихнутих актором.
RubberDuck

Відповіді:


8

Про що ми в кінцевому підсумку говоримо тут - це час збирання та час виконання.

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

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

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

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

Ще одним хорошим прикладом може бути дозвіл на те, щоб нулі були частиною мови, оскільки можливі винятки з покажчиком нуля, якщо ви дозволите нулі. Деякі мови повністю усунули цю проблему, запобігаючи тим, що змінні, не явно декларовані як здатні містити нульові значення, оголошені без негайного присвоєння значення (наприклад, візьмемо Kotlin). Хоча ви не можете усунути помилку виконання виключення з покажчиком з нульовим покажчиком, ви можете запобігти його, видаливши динамічний характер мови. У Котліні ви, звичайно, можете змусити зберігати нульові значення, але це само собою зрозуміло, що це метафоричні "покупці, будьте обережні", оскільки вам потрібно чітко заявити про це як таке.

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

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

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


3

Система типу автоматично перевіряє докази деяких аспектів коректності. Наприклад, система типу Руста може довести, що посилання не переживає посилається об'єкт або що посилається об'єкт не змінюється іншим потоком.

Але типові системи досить обмежені:

  • Вони швидко стикаються з проблемами розв'язання. Зокрема, сама система типів має бути рішучою, проте багато систем практичного типу випадково є Turing Complete (включаючи C ++ через шаблони та Rust через риси). Крім того, певні властивості програми, яку вони перевіряють, можуть бути невирішеними в загальному випадку, найвідоміше, що деякі програми зупиняються (або розходяться).

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

Через ці обмеження типові системи мають тенденцію лише перевіряти досить слабкі властивості, які легко довести, наприклад, що функція викликається зі значеннями правильного типу. Але навіть це суттєво обмежує виразність, тому зазвичай є обхідні шляхи (як, наприклад, interface{}Go, dynamicC #, ObjectJava, void*C) або навіть використання мов, які повністю уникають статичного набору тексту.

Чим сильніші властивості ми перевіряємо, тим менш виразною буде мова, яку типово отримує. Якщо ви написали «Іржа», ви знаєте ці моменти «боротьби з компілятором», коли компілятор відкидає, здавалося б, правильний код, тому що не зміг довести правильність. У деяких випадках висловити певну програму в Русті неможливо, навіть коли ми вважаємо, що можемо довести її правильність. unsafeМеханізм в Руста або C # дозволяє уникнути кордонів системи типу. У деяких випадках відкладення перевірок на час виконання може бути іншим варіантом - але це означає, що ми не можемо відхилити деякі недійсні програми. Це питання визначення. Програма «Іржа», яка панікує, безпечна щодо типової системи, але не обов'язково з точки зору програміста чи користувача.

Мови розроблені разом із системою їх типу. Рідко введено систему нового типу на існуючу мову (але див., Наприклад, MyPy, Flow або TypeScript). Мова намагатиметься спростити запис коду, який відповідає системі типів, наприклад, пропонуючи анотації типу або введення легких доказів структур управління потоками. Різні мови можуть закінчитися різними рішеннями. Напр. Java має поняття finalзмінних, які присвоюються рівно один раз, подібно до не mutзмінних Руста :

final int x;
if (...) { ... }
else     { ... }
doSomethingWith(x);

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

let x = if ... { ... } else { ... };
do_something_with(x)

Це виглядає як дійсно незначний момент, коли з'ясовується завдання, але чітке визначення масштабів є надзвичайно важливим для доказів, пов’язаних із життям.

Якби ми застосували систему Java у стилі Rust до Java, у нас виникли б набагато більші проблеми, ніж це: об’єкти Java не анотовані протягом життя, тому нам доведеться трактувати їх як &'static SomeClassабо Arc<dyn SomeClass>. Це послабило б будь-які отримані докази. У Java також немає концепції на рівні типу незмінність, тому ми не можемо розрізняти &і &mutтипи. Нам доведеться ставитися до будь-якого об’єкта як до Cell або Mutex, хоча це може передбачати більш сильні гарантії, ніж насправді пропонує Java (зміна поля Java не є безпечною для потоків, якщо це не синхронізовано і мінливо). Нарешті, у Rust немає концепції успадкування в стилі Java.

Системи типу TL; DR: типу - докази теореми. Але вони обмежені проблемами рішення, що стосуються рішення, та проблемами щодо ефективності. Ви не можете просто взяти систему одного типу та застосувати її до іншої мови, оскільки синтаксис мови цілі може не надавати необхідної інформації та тому, що семантика може бути несумісною.


3

Наскільки безпечно безпечно?

Так, такий верифікатор майже неможливо написати: ваша програма просто повинна повернути постійний UNSAFE. Ви будете мати рацію 99% часу

Тому що навіть якщо ви запускаєте безпечну програму Rust, хтось може все-таки витягнути штекер під час його виконання: так що ваша програма може зупинитися, навіть якщо це теоретично не передбачається.

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

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

Жарт убік, автоматизована перевірка

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

Цей аналіз, до речі, також виконується деякими компіляторами заради оптимізації.

Безумовно, можна піти на крок далі, а також проаналізувати паралельність і зробити відрахування щодо поширення обмежень по декількох потоках, умов синхронізації та перегонів. Однак дуже швидко ви зіткнетеся з проблемою комбінаторного вибуху між шляхами виконання та багатьма невідомими (введення / виведення, планування ОС, введення користувача, поведінка зовнішніх програм, перебої тощо), що призведе до зменшення відомих обмежень для їх голого мінімум і ускладнюють робити будь-які корисні автоматизовані висновки щодо довільного коду.


1

Тьюрінг вирішив це ще в 1936 році зі своїм документом про проблему зупинки. Один з результатів полягає в тому, що просто неможливо написати алгоритм, який 100% часу може проаналізувати код і правильно визначити, зупиниться він чи ні, неможливо написати алгоритм, який може 100% часу правильно визначте, чи має код якесь певне властивість чи ні, включаючи "безпеку", проте ви хочете його визначити.

Однак результат Тьюрінга не виключає можливості програми, яка в 100% часу може (1) абсолютно визначити код безпечним, (2) абсолютно визначити, що код небезпечний, або (3) антропоморфно підняти руки і сказати "Чорт, я не знаю." Компілятор Руста, взагалі кажучи, в цій категорії.


Отже, поки у вас є варіант "не впевнений", так?
TheEnvironmentalist

1
Висновок полягає в тому, що завжди можна написати програму, яка може заплутати програму, що аналізує програму. Вдосконалення неможливо. Практичність може бути можливою.
NovaDenizen

1

Якщо програма загальна (технічна назва для програми, яка гарантовано припиниться), теоретично можливо довести будь-яке довільне властивість над програмою з урахуванням достатньої кількості ресурсів. Ви можете просто вивчити кожний потенційний стан, у який може увійти програма, і перевірити, чи порушує хтось із них ваше майно. Мова перевірки моделі TLA + використовує варіант цього підходу, використовуючи теорію множин для перевірки ваших властивостей проти наборів потенційних програмних станів, а не обчислення всіх станів.

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

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

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

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.