Три підходи до керування пам'яттю: ручне, збирач сміття, володіння
Живе демо: один і той самий баг у C та в Rust
Фокус: ціна помилки, а не синтаксис жодної з мов
Стек і купа
Стек — пам'ять функції: локальні змінні, звільняється сама при виході з функції
Купа (heap) — динамічний час життя: виділяється і звільняється явно (malloc/free) або керовано
Вказівник на купу можна скопіювати, а виділена ділянка — одна фізична адреса
Помилки керування пам'яттю майже завжди стосуються саме купи
Стек дешевий і безпечний за конструкцією; купа — джерело всіх подальших проблем
Стек проти купи
Ознака
Стек
Купа
Розмір
фіксований, відомий під час компіляції
динамічний, часто невідомий заздалегідь
Виділення
O(1): зсув вказівника стека
пошук вільного блоку аллокатором
Час життя
область видимості функції
явно (free) або через володіння
Типові дані
локальні змінні, i32, bool
Box, Vec, String, об'єкти
Джерело помилок
переповнення стека (рекурсія)
use-after-free, double free, витік
Стек дешевий і безпечний за конструкцією; купа — джерело всіх подальших проблем
Класичні помилки керування пам'яттю
💀
Use-after-freeвикористання пам'яті після її звільнення
✂️
Double freeповторне звільнення тієї самої ділянки
🌊
Buffer overflowзапис за межі виділеного блоку
🕳️
Memory leakвиділили і забули звільнити
🧷
Dangling pointerвказівник на вже звільнену ділянку
⚡
Data raceдва потоки, ті самі дані, хоч один запис (Л6)
П'ять перших формують левову частку критичних уразливостей CWE Top 25 десятиліттями
Use-after-free на C
c
int *p = malloc(sizeof(int));
*p = 3101; // 0xC1D
free(p);
printf("%d\n", *p); // UB: читаємо звільнену пам'ять
*p = 100; // UB: пишемо у звільнену пам'ять
Компілятор мовчить: жодного попередження за замовчуванням
Чому use-after-free «працює» на тестах
1
mallocаллокатор віддає блок; вказівник p — просто число
2
free(p)блок повернуто в пул; байти фізично ще лежать на місці
3
Читання *p на тестізначення ще там — тест зелений, компілятор мовчить
4
Прод під навантаженнямблок перевикористано під інший об'єкт
5
Крах або експлойтчитання дає сміття, запис ламає чужі дані
Санітайзери (ASan, Valgrind) ловлять це під час виконання — і лише на пройдених шляхах
Ціна помилки
≈70%
CVE високої критичності Microsoft
помилки безпеки пам'яті (MSRC, 2006–2018)
≈70%
серйозних багів безпеки Chromium
Google: та сама частка для Chrome та Android
№1
CWE Top 25 (2021–2023)
CWE-787 Out-of-bounds Write
0
попереджень gcc за замовчуванням
на use-after-free із попереднього слайда
Ціна — не в моменті написання рядка, а коли рядок зустрічає реальний, часто зловмисний вхідний потік
Три підходи до керування пам'яттю
Підхід
Мови
Хто перевіряє
Ціна в рантаймі
Помилки пам'яті
Ручне (malloc/free)
C, C++
програміст
немає
можливі всі п'ять
Збирач сміття (GC)
Java, Python, Go, JS
рантайм
паузи stop-the-world, трасування
здебільшого виключені
Володіння (ownership)
Rust
компілятор
немає
виключені на етапі компіляції
Rust — спроба взяти безпеку GC-мов і швидкість C без компромісу в рантаймі
Ідея Rust: володіння перевіряється компілятором
У кожного значення є рівно один власник (owner)
Власник виходить з області видимості — компілятор сам вставляє звільнення пам'яті
Використати «мертве» значення — помилка компіляції, а не помилка виконання
Саме це розберемо в Л5–Л7: володіння, запозичення, Option/Result
Той самий баг у Rust не компілюється
rust
let p = Box::new(42);
drop(p); // явно звільняємо пам'ять
println!("{}", p); // error[E0382]: use of moved value: `p`// компілятор відмовляється зібрати цей код —// баг знайдено ДО того, як програма хоч раз запустилась
Помилка виявлена на етапі компіляції, а не на етапі експлуатації
Одна помилка — дві ціни
Ліворуч C: free(p) лишає p висячим, *p — UB уже в проді. Праворуч Rust: після drop(p) використання p — error[E0382], бінарник просто не збереться
Живе демо
Компілюємо і запускаємо приклад use-after-free на C (gcc, без прапорів санітайзера) — програма щось виводить, поведінка не гарантована
Компілюємо той самий приклад із санітайзером (-fsanitize=address) — тепер помилка виявляється, але тільки під час виконання
Пишемо еквівалентний код на Rust з Box і drop(p) перед використанням
Запускаємо `cargo build` — компіляція падає з error[E0382] ще до запуску
Очікувано: C: непередбачувана поведінка або крах у рантаймі. Rust: помилка компіляції, програма взагалі не збирається
Санітайзер проти перевірки компілятора
ASan / Valgrind (C, C++)
Ловить під час виконання
Лише пройдені шляхи виконання
Потребує покриття тестами
Сповільнює програму в кілька разів
Баг уже в бінарнику
vs
Перевірка володіння (Rust)
Ловить під час компіляції
Усі шляхи виконання одразу
Не залежить від тестів
Нуль накладних витрат у рантаймі
Бінарник із багом не з'явиться
Санітайзер знаходить помилку після запуску; компілятор — до появи бінарника
Компілятор — союзник, не ворог
Перевірка запозичень (borrow checker) здається суворою новачкам — це нормально, це її робота
Кожна помилка компіляції — це баг, який НЕ потрапить у прод
Ціна: трохи довше писати перший робочий варіант коду
Виграш: use-after-free, double free, data race стають структурно неможливими
Встановлення Rust через rustup
sh.rustup.rscurl … | sh — один інсталятор
→
rustupканали stable · beta · nightly
→
stableкурс працює лише на stable
→
rustc + cargo--version — перевірка
→
rustup updateоновлення тулчейна
Один інструмент (rustup) керує компілятором, Cargo і документацією одразу
Перший проєкт: cargo new
bash
cargo new lockpick
cd lockpick
tree .
# lockpick/# ├── Cargo.toml ← маніфест: назва, версія, залежності# ├── Cargo.lock ← точні версії залежностей (генерується автоматично)# └── src/# └── main.rs ← точка входу
cargo new одразу ініціалізує git-репозиторій, якщо він ще не існує
Команди Cargo, які знадобляться щодня
Команда
Що робить
Коли
cargo check
перевіряє компіляцію без бінарника
найшвидша ітерація під час написання
cargo build / run
компілює / компілює й запускає (debug)
коли треба виконати
cargo test
запускає юніт- та інтеграційні тести
перед кожним комітом; автотести ЛР
cargo add <crate>
додає залежність у Cargo.toml
потрібен зовнішній крейт
cargo fmt
форматує за офіційним стилем
перед комітом
cargo clippy
лінтер антипатернів
перед рев'ю
Перший юніт-тест
rust
fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn adds_two_positive_numbers() {
assert_eq!(add(2, 3), 5);
}
}
`#[cfg(test)]` виключає тестовий модуль з релізного бінарника
Cargo.toml проти Cargo.lock
Cargo.toml — маніфест
Пишеться вручну
[package]: name, version, edition = "2021"
[dependencies]: serde = "1.0"
"1.0" означає ^1.0 — сумісні 1.x.y
↔
Cargo.lock — зафіксовані версії
Генерується автоматично
Точні версії, що реально зібралися
Комітиться в git для бінарників
Гарантує відтворювану збірку
Семантичне версіювання: мажорна.мінорна.патч; ^ дозволяє оновлення в межах мажорної
Демо: тест, що падає → тест, що проходить
cargo new lockpick && cd lockpick
Дописуємо функцію add(a, b) і тест, що навмисно очікує неправильний результат
cargo test — бачимо FAILED і повідомлення "assertion `left == right` failed"
Виправляємо очікуване значення в тесті, запускаємо cargo test знову — усі тести ok
Очікувано: Перший запуск: test result: FAILED. Другий запуск: test result: ok. 1 passed; 0 failed
Маршрут модуля 2
Л4 · ЛР4мотивація · Cargo і перший тест
→
Л5 · ЛР5володіння · borrow checker
→
Л6 · ЛР6запозичення · struct і enum
→
Л7 · ЛР7Option/Result · читання файлу
Наступні три лекції — як саме працює перевірка: володіння, запозичення, Option/Result
Тестування програми може показати наявність помилок, але ніколи — їхню відсутність.
— Едсгер Дейкстра, «Notes on Structured Programming» (1970)
Підсумок
Помилки керування пам'яттю (use-after-free, double free, переповнення) — джерело більшості критичних вразливостей
C/C++ довіряють програмісту й перевіряють це в кращому разі в рантаймі
Rust переносить перевірку володіння на етап компіляції — без накладних витрат у рантаймі
Наступні три лекції — як саме працює ця перевірка: володіння, запозичення, Option/Result
🥚 У прикладі use-after-free у виділену ділянку записано 3101 — це 0xC1D у шістнадцятковій системі. Число не випадкове: воно ще не раз зустрінеться в матеріалах курсу.
VTFK · TERMLINK · L04← → · space · f — на весь екран