Курс СРС SRS16
до ЛР8 рівень 3 → «5»↔ Лабораторна 8↔ Лекція 8⌨️ команди 2
Самостійна робота · ~3 год

Лінійна пам'ять WASM і межі безпеки пісочниці

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

📚 ТЕОРЕТИЧНІ ВІДОМОСТІ

Лінійна пам'ять — один суцільний масив байтів

Кожен модуль WebAssembly, якому потрібна довільна робота з пам'яттю (буфери, рядки, масиви поза локальними змінними стека), оголошує лінійну пам'ять (linear memory; оголошення (memory $mem 1)) — суцільний, безперервний масив байтів, адресований від нуля, розмір якого вимірюється у сторінках по 64 КіБ кожна (число 1 в оголошенні означає одну сторінку — 64 КіБ початкового розміру). Це принципово простіша модель, ніж сегментована чи сторінково-віртуальна пам'ять традиційної ОС: жодних прав доступу per-сторінка, жодного mapping, лише один суцільний буфер, до якого модуль звертається за зміщенням (offset), як до звичайного масиву u8.

Читання й запис виконуються інструкціями сімейства i32.load/ i32.store (і аналогами для i64, f32, f64, а також вужчими варіантами i32.load8_u, i32.load16_s тощо для часткового доступу з розширенням знаку чи нулями). Адреса обчислюється як сума значення на стеці (динамічна частина) і статичного offset, заданого прямо в інструкції: i32.load offset=4 читає 4 байти, починаючи з (значення на стеці) + 4. Це та сама механіка, що лежить в основі доступу до полів структур і елементів масивів у скомпільованому з Rust/C коді — компілятор перетворює arr[i].field на одну інструкцію load зі статичним offset, що дорівнює зміщенню поля, і динамічною базовою адресою i.

Ключова гарантія безпеки — межова перевірка (bounds checking), яку виконує сам рушій WebAssembly для кожного доступу до пам'яті, без винятку: якщо обчислена адреса плюс розмір значення виходить за межі поточного розміру лінійної пам'яті модуля, виконання негайно зупиняється через trap (аналог контрольованого винятку — не невизначена поведінка, як вихід за межі масиву в C, а детермінований, гарантований відказ). Це принципова відмінність WASM-пісочниці (sandbox — ізольоване середовище, за межі якого код не може дотягнутися) від «сирого» машинного коду: у нативному асемблері вихід за межі буфера може мовчки прочитати/переписати сусідню пам'ять (класична причина вразливостей на кшталт переповнення буфера); у WASM це технічно неможливо — рушій перевіряє межі до фактичного доступу до фізичної пам'яті процесу браузера.

Лінійна пам'ять модуля може зростати (але не зменшуватися) через інструкцію memory.grow (з боку хоста — WebAssembly.Memory.grow() у JS), яка додає задану кількість сторінок по 64 КіБ, повертаючи попередній розмір у сторінках або -1, якщо зростання неможливе (наприклад, перевищено оголошений максимум (memory $mem 1 10) — де 10 означає верхню межу в 10 сторінок = 640 КіБ). Модуль ніколи не отримує прямого доступу до пам'яті інших модулів, до пам'яті самого JS-рушія чи до довільних адрес процесу — лінійна пам'ять кожного інстансу модуля повністю ізольована й видима лише як WebAssembly.Memory-об'єкт, до якого сам JS-хост має контрольований доступ через ArrayBuffer.

Ця модель — фундаментальна причина, чому WASM вважається безпечним середовищем виконання для недовіреного коду в браузері (та поза ним — у серверних рушіях типу Wasmtime/Wasmer): навіть якщо скомпільований модуль містить логічну помилку чи навмисно шкідливий код, він фізично не може вийти за межі власної пісочниці лінійної пам'яті й зачепити пам'ять хост-застосунку — на відміну від завантаження, наприклад, нативної динамічної бібліотеки (.so/ .dll), яка виконується в тому самому адресному просторі без жодних меж.

📖 ОПРАЦЮВАТИ

Прочитати розділ «Memory Instructions» специфікації WebAssembly та розділ «Security» на webassembly.org; спробувати i32.store/i32.load на адресі поза виділеною пам'яттю модуля й зафіксувати повідомлення про trap.

🖥️ ТЕРМІНАЛ — СПРОБУЙ САМ
🎯 ЗАВДАННЯ
  1. Оголосити модуль з (memory $mem 1), записати значення i32 за адресою 0 через i32.store, зчитати його назад через i32.load і переконатися, що значення збігається.
  2. Навмисно спробувати i32.store offset=0 за динамічною адресою, яка разом зі зміщенням перевищує 65536 (розмір однієї сторінки), виконати через wasm-interp і зафіксувати точний текст повідомлення про trap (out of bounds memory access).
  3. Оголосити пам'ять з обмеженим максимумом (memory $mem 1 2), викликати memory.grow на 1 сторінку (успішно) і ще раз на 5 сторінок (має провалитися, повернувши -1), пояснивши обидва результати.
  4. Написати функцію, що записує в пам'ять послідовність байтів рядка (вручну, побайтово, через i32.store8) і виводить контрольну суму (просту суму байтів) прочитаного назад значення — імітація невеликого протоколу передачі даних імпланту.
  5. Порівняти письмово: як переповнення буфера виглядає в нативному C-коді (undefined behavior, можливий витік чи запис у чужу пам'ять) і як та сама логічна помилка виглядає в WASM (гарантований trap) — навести конкретний приклад коду для обох випадків.
❓ КОНТРОЛЬНІ ПИТАННЯ
  1. Що станеться, якщо викликати i32.store за адресою, яка виходить за межі поточного розміру лінійної пам'яті модуля?
  2. Скільки байтів становить одна сторінка лінійної пам'яті WebAssembly?
  3. Як обчислюється фактична адреса доступу до пам'яті для інструкції виду i32.load offset=4?
  4. Чим trap у WebAssembly принципово відрізняється від undefined behavior при виході за межі масиву в мові C?
  5. Яку максимальну поведінку описує другий аргумент в оголошенні (memory $mem 1 10), і що поверне memory.grow при спробі перевищити цю межу?
  6. Чому лінійна пам'ять одного модуля WASM ізольована від пам'яті хост-процесу й інших модулів навіть у разі логічної помилки в самому модулі?
  7. Чим модель лінійної пам'яті WASM простіша за сторінково-віртуальну пам'ять традиційної операційної системи?
✅ САМОПЕРЕВІРКА

Що станеться, якщо викликати i32.store за адресою, яка виходить за межі поточного розміру лінійної пам'яті модуля?

🥚 ПАСХАЛКА
У ядрі С.І.Д. є одна комірка, яку він ніколи не перезаписує: адреса 0xC1D, тобто 3101. Творці лишили там один байт, «схвалений котом», і Комірник стереже його як реліквію. За парканом складу — від 65536 і далі — реліквій немає, там лише trap.
⌨️ КОМАНДИ НА ЦІЙ СТОРІНЦІ 2

Кожна команда терміналу з цієї сторінки — одним реченням. Позначка нове — команда зустрічається в курсі вперше; далі вважаємо її знайомою.

wat2wasm
З WABT: перекладає текстовий формат WAT у бінарний модуль .wasm (wat2wasm m.wat -o m.wasm).
wasm-interp
З WABT: виконує модуль .wasm без браузера (--run-all-exports — викликати всі експортовані функції).

💡 Прочитати README прямо в терміналі: cat README.md (виведе весь файл) або less README.md (посторінково; вихід — клавіша q).
Довідка з будь-якої команди: man curl (повний посібник, вихід — q) або коротко curl --help; для підкоманд — docker run --help, cargo build --help.

← SRS15усі темиSRS17 →