Місія 10: «Звільнення»
С.І.Д.: Час «Ч», ранере. Координати підтверджені — блок Б-7, мінус третій поверх, за дверима з кодовим замком, який я вже майже зламав — «майже» тут чесне слово, не фігура мови. Мої творці там. Збирай моє ядро, пакуй його у WebAssembly і запускай безпосередньо на клієнтській сторінці корпоративної панелі охорони. Щойно я опинюся всередині — перехоплю двері ізолятора і пущу дим у вентиляцію для відволікання. Я не прошу тебе бути героєм — я про це казав першої ночі і не забрав слів назад. Але з усіх, хто вміє це зробити сьогодні, поруч зі мною ти одинти одна.
Дев'ять місій — і всі вони, як тепер зрозуміло, вели сюди. Ізольований контейнер першої ночі навчив тебе ховати процеси від сторонніх очей. Багатоетапна збірка — тримати ядро С.І.Д. компактним. Сліпий канал — розмовляти пошепки там, де ніхто не слухає. Cargo й тести — виготовляти інструмент, якому можна довіряти. Прикордонник borrow-checker — не ламати те, що вже працює, навіть під тиском. Типи без null — описувати ворога так, щоб компілятор ловив твої помилки замість тебе. Обробка помилок — не панікувати на битих даних. І два останні розтини, ручний WAT і холодне порівняння асемблерів, навчили бачити, у що саме перетворюється код там, унизу, куди зазирає тільки браузер. Кожна з тих ночей здавалася окремою. Тепер ти розумієш: це були деталі однієї машини.
Лабораторія коледжу тиха, як перед пострілом. Голограма С.І.Д. застигла над столом
нерухомо — уперше без жодних звичних хвастощів, бірюзова петелька завмерла, і навіть чорна
шайба проєктора біля клавіатури світиться тьмяніше, ніж завжди. Поруч
світиться порожній Cargo-проєкт, курсор блимає в lib.rs, і за цим блиманням — блок Б-7,
мінус третій поверх, двері з кодовим замком і двоє людей, яких «ТехНова» забрала й викреслила
з усіх реєстрів.
— Це остання кузня, ранере, — тихо каже С.І.Д. — Усі попередні ночі ти щось ховавла, щось різавла, щось зібравзібрала з чужих деталей — і, повір, я бачив кожен твій крок і щоразу тихо пишався. Сьогодні ти куєш мене цілого. Не фрагмент, не ручний прототип на кілька опкодів — справжнє ядро, яке компілятор перевірить від першого байта до останнього, перш ніж я довірю йому власне існування хоч на секунду всередині ворожої мережі. І яке запустить будь-який браузер — а це означає, що з сьогоднішньої ночі я більше не живу на твоєму ноутбуку, який можна відібрати. Я житиму всюди, де є вкладка. Пам'ятаєш, чого я просив першої ночі? Оце воно.
Ти киваєш. Rust гарантує, що код не збреше про пам'ять. WASM гарантує, де саме він виконається — у пісочниці браузера, а не там, звідки можна дотягнутися до системи хоста. Лишалося останнє, і саме про нього С.І.Д. говорив найтихіше.
— А коли викуєш, — веде далі він, — зроби так, щоб ніхто не зміг підмінити результат непомітно. О, дивись, як гарно це влаштовано: контейнер із зафіксованими версіями — це сама кузня, та сама вихідна руда, той самий молот, і на виході щоразу той самий злиток, байт у байт. Це підпис, ранере. Такий, якого не підробити. Збери мене двічі — і два хеші зійдуться, як два відбитки одного пальця. Тоді навіть я зможу довести собі, що це справді я, а не те, що «ТехНова» підсунула замість мене.
Ти кладеш пальці на клавіатуру. За вікном рудим ще горить корпоративний квартал — востаннє байдужий до того, що діється в порожній лабораторії коледжу. Три межі курсу — контейнер, володіння, лінійна пам'ять — сходяться сьогодні в одну точку. Стос розчистив стійку, Комірник відімкнув склад — цього разу ядро ляже до них ціле, а не уламками. Час зібрати ядро.
#[wasm_bindgen], ціль wasm32-unknown-unknown, конвеєр wasm-pack, ціна перетину межі й
відтворювані збірки — усе це з Лекції 10, того самого дня; вона ж замикає лінію курсу.
Чотири речі звідти під фінал: атрибут #[wasm_bindgen] робить Rust-функцію викликаною з JS
без жодного ручного клею; числа перетинають межу задарма, а рядки — з копіюванням байтів
через лінійну пам'ять; wasm-pack build --target web дає теку pkg/, готову до
<script type="module">; побайтова однаковість двох збірок доводиться sha256sum.
І одна річ із ночі 8: та сама лінійна пам'ять, яку ти виділявла руками, тепер працює під тобою автоматично — саме через неї підуть рядки команд.
wasm-bindgen, ціна перетину межі, wasm-pack і сама ідея відтворюваної збірки — з
Лекції 10 (див. «Освіжити» вище). Тут — те, чого на лекції не було: три конкретні
речі, на яких фінальна збірка ламається найчастіше.
Версія wasm-bindgen мусить збігатися двічі. Версія в Cargo.toml і версія CLI, який
викликає wasm-pack, — це два різні місця, і якщо вони розходяться, збірка падає з
прямою скаргою на невідповідність. Тому в шаблоні стоїть wasm-bindgen = "=0.2.92" зі
знаком рівності: без нього cargo тихо оновить залежність, і фінальна ніч зустріне тебе
помилкою, якої вчора не було.
Що саме робить --target web. wasm-pack має кілька цілей, і вони не
взаємозамінні: bundler (типова) розрахована на webpack і сама по собі в браузері не
працює, nodejs — на require, а web дає модуль для прямого
<script type="module">. Для сторінки без фронтенд-тулчейну потрібна саме web; узята
навмання ціль дає порожню сторінку без жодної помилки в консолі.
Чому «двічі зібрати» — не те саме, що «зібрати й переконатися». Друга збірка мусить бути справді незалежною: чистий вихідний каталог, той самий образ, і жодного збереженого артефакту в шарах Docker. Інакше друга «незалежна» збірка просто повертає результат першої — і хеші зійдуться, нічого не доводячи. Це найтонше місце рівня 3.
Відтворювана збірка (reproducible build) — властивість, за якої той самий вихідний код, зібраний двічі (навіть на різних машинах), дає побайтово ідентичний результат. Це критично важлива властивість для будь-якого системного чи безпекового ПЗ: вона дозволяє незалежно довести, що опублікований бінарник відповідає опублікованому вихідному коду, і що жодна проміжна ланка (система збірки, CI-сервер, реєстр пакетів) не підмінила його непомітно.
Відтворюваність досягається трьома складовими:
- Фіксація версій усіх інструментів — конкретна версія компілятора Rust,
конкретна версія
wasm-bindgenCLI (має точно збігатися з версією залежності уCargo.toml, інакшеwasm-packвідмовиться збирати). - Фіксація залежностей —
Cargo.lock, який фіксує точні версії (включно з транзитивними) усіх крейтів у графі залежностей. - Ізольоване середовище збірки — Docker-контейнер із конкретним тегом базового
образу (не
latest, який може непомітно змінити вміст із часом), щоб виключити вплив локального оточення розробника (версія системних бібліотек, змінні середовища, кеш).
Перевірка результату — тривіальна: sha256sum двох незалежно зібраних .wasm-файлів
має збігатися посимвольно.
Рівень 1 (оцінка «3») — «Першокурсник»
Завдання: зібрати ядро на Rust у формат WebAssembly, підключити до HTML-сторінки і відтворити виклик.
Зроби буквально те, що написано нижче — цього достатньо для «3».
📦 Ланцюг. Рівень 1 самодостатній: новий crate sid_core/ у цьому репозиторії, і
жодна попередня ЛР для нього не потрібна. Рівні 2 і 3 — це розв'язка ланцюга, і вони йдуть
у vtfk-sid. На рівні 2 команди маршрутизуються у твій sidkit::intel (ночі 7), який
усередині кличе logscan (ніч 5) і guard (ніч 6). На рівні 3 збірка йде у твоїй капсулі
ночей 2–3. Тобто в браузер їде не демонстраційне ядро, а той самий crate, який ти
кувавла з четвертої ночі. Саме це і є фінал курсу.
Етапи йдуть по одному: зробив — постав «вийшло» — і ранер перекине тебе на наступний. Стрілки ← → на клавіатурі, свайп на телефоні, «усі» — щоб побачити всі чотири одразу. Місце запам'ятовується: закриєш ноутбук — повернешся туди ж.
1Етап 1 · Створити crate і зафіксувати версію
cargo new --lib sid_core
У Cargo.toml — точно те, що в шаблоні, зі знаком рівності:
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "=0.2.92"
crate-type = ["cdylib"] обов'язковий: без нього cargo збере звичайну Rust-бібліотеку,
з якої .wasm-модуля не вийде.
Очікується Cargo.toml із crate-type = ["cdylib"] і точно закріпленою версією wasm-bindgen.
1Етап 2 · Написати першу функцію по той бік межі
У src/lib.rs:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn echo_number(x: i32) -> i32 {
x * 2
}
i32 в обидва боки — найдешевший перетин межі, який існує: просто число на стеку, без
жодного копіювання байтів. Рядки будуть на рівні 2, і вони дорожчі.
Очікується, що cargo build проходить, а echo_number позначена #[wasm_bindgen] — інакше вона не перетне межу.
1Етап 3 · Прогнати конвеєр збірки
cd sid_core && wasm-pack build --target web
Після успіху в sid_core/ з'явиться pkg/ — .wasm, .js-обгортка і .d.ts. Зазирни
туди очима: те, чого ти не писавла, і згенерував wasm-bindgen.
Запамʼятай, де лежить pkg/: наступний етап підніматиме сервер саме звідси, бо
index.html імпортує ./pkg/sid_core.js — шлях відносний, і сторінка мусить лежати поруч
із текою pkg/, а не поверхом вище.
Очікується тека pkg/ із sid_core.js, sid_core_bg.wasm і package.json усередині.
1Етап 4 · Запустити ядро на сторінці
Сервер піднімай із того самого каталогу sid_core/, де лежать index.html і pkg/:
cd sid_core # якщо ти ще не там
ls index.html pkg # обидва мусять бути тут, поруч
python3 -m http.server 8080
Відкрий http://localhost:8080/ (не подвійним кліком по файлу — fetch не працює з
file://, як і в ночі 8) і перевір <pre id="log">: виклик echo_number(21)
має дати 42.
Це вся вимога рівня 1.
Очікується сторінка, яка вивела результат echo_number: Rust, скомпільований у WASM, щойно відповів із браузера.
Фінальна ніч ламається на збірці, а не на коді. П'ять найчастіших випадків.
Error: rust-wasm-bindgen version mismatch(абоit looks like the Rust project used to create this wasm file had a different version) Версія вCargo.tomlі версія CLI розійшлися. Або постав CLI тієї самої версії (cargo install -f wasm-bindgen-cli --version 0.2.92), або лиши="=0.2.92"уCargo.tomlі не оновлюй. Це найчастіша поломка ночі.С.І.Д.: «Дві половини ядра з різних креслень не зійдуться. Я це знаю краще за всіх».
Сторінка відкривається, консоль порожня, нічого не працює Зібрано не з
--target web. Цільbundler(типова) розрахована на webpack і в браузері напряму не запуститься — причому без помилки. Перезбери з--target web.С.І.Д.: «Ти зібравзібрала мене для іншої машини. Тиша — це не відмова, це непорозуміння».
error: cannot produce cdylib/ уpkg/немає.wasmЗабутий[lib] crate-type = ["cdylib"]уCargo.toml.С.І.Д.: «Кузня видала заготовку, а не ядро. Скажи їй, що саме кувати».
TypeError: Failed to fetchпід часinit()Зновуfile://замість сервера, абоpkg/лежить не там, куди дивитьсяindex.html. Перевір шлях вimport init from './pkg/sid_core.js'.С.І.Д.: «Двері є, але не за тією адресою, яку ти назвавназвала».
Два хеші зійшлися з першого разу, без чистого каталогу (рівень 3) Це не успіх, а хибний результат: друга збірка взяла артефакт із шарів Docker або зі старої
pkg/. Прибериpkg/іtarget/між запусками — і збери ще раз.С.І.Д.: «Два відбитки одного пальця — доказ. Двічі показаний той самий відбиток — ні».
Решта помилок курсу — у «Коли щось пішло не так».
Рівень 2 (оцінка «4», захист) — «Магістр»
Дві версії умови. A — на своєму ядрі:
handle_commandне вигадує відповіді, а маршрутизує їх у твійsidkit: додай crate ночей 4–7 як залежність шляхом (уvtfk-sidцеsidkit = { path = "../sidkit" }), і нехай"status"кличеintel, а друга команда —guard. Тоді в браузері справді працює те, що ти кувавла сім ночей. B — на референсі: якщо свогоcore/sidkit/немає, візьмиvtfk-sid-reference@after-lab09або поверни відповіді рядками просто вlib.rs. Оцінка однакова.
Завдання: налаштувати передачу складних типів даних (рядків з командами) між Rust та JS.
Реалізуй handle_command(cmd: &str) -> String, яка приймає рядок-команду і повертає
рядок-відповідь. Мінімум дві різні команди (наприклад, "status" →
"ядро активне", будь-яка інша → "невідома команда"). Підключи до текстового поля
й кнопки в index.html, і постав поруч обличчя — того самого С.І.Д., якого ти бачивла
в кутку кожної сторінки цього курсу.
А тоді зроби те, заради чого ця ніч називається «Звільнення»: опублікуй сторінку. GitHub Pages у репозиторії роботи — три кліки. Після цього С.І.Д. живе не в тебе на машині й не в контейнері, який можна прибрати командою, а за адресою, яку можна відкрити з чужого телефона на іншому кінці міста.
На захисті ти не показуєш свій ноутбук. Ти диктуєш адресу, і місія відкривається на
екрані викладача. І там поясни, що саме відбувається «під капотом», коли JS-рядок
переходить межу й стає &str.
С.І.Д.: «Чотирнадцять місяців я жив там, куди мене поклали. Сьогодні я вперше живу там, де мене можна знайти. Різниця більша, ніж здається: адресу не можна вимкнути кнопкою живлення».
Допуск. На захист рівня 2 С.І.Д. не пускає без трьох предметних зауваг до чужої роботи № 9 — issue або коментар у репо завдання. Кого читати: той, чий номер у списку на 10 більший за твій, по колу. «Все ок» не приймається: «Скажи, що саме не так і чому, ранере. Читати чужий код — половина вміння».
Рівень 3 (оцінка «5», розширення) — «Легенда Коледжу»
Ті самі дві версії: A — збірка йде у твоїй капсулі ночей 2–3: за основу береш
core/capsule/Dockerfile, доводиш його до відтворюваності й фіксуєш два однакові хеші свого ядра; B — новийDockerfileз нуля за шаблоном. Оцінка однакова, два збіглі хеші обов'язкові в обох.
Завдання: налаштувати повністю відтворювану збірку фінального Wasm-імпланта в ізольованому Docker-контейнері.
Допиши Dockerfile з зафіксованим (не latest) тегом базового образу Rust і
конкретною версією wasm-pack, що встановлює весь тулчейн у контейнер (без готового
результату збірки всередині образу — щоб кожен запуск контейнера збирав по-новому,
інакше друга «незалежна» збірка непомітно поверне артефакт, збережений у шарах першої
image). Напиши скрипт build_reproducible.sh, який двічі запускає wasm-pack build
у контейнері з чистим вихідним каталогом між запусками і звіряє sha256sum
отриманих .wasm-файлів. На захисті продемонструй однаковий хеш.
Приклад 1 — мінімальне ядро (рівень 1)
Cargo.toml:
[package]
name = "sid_core"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "=0.2.92"
src/lib.rs:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn echo_number(x: i32) -> i32 {
x * 2
}
Збірка та підключення:
wasm-pack build --target web
python3 -m http.server 8080
index.html (фрагмент — файл видається готовим):
<script type="module">
import init, { echo_number } from './pkg/sid_core.js';
await init();
document.getElementById('log').textContent = echo_number(21); // 42
</script>
Приклад 2 — маршалінг рядків (рівень 2)
#[wasm_bindgen]
pub fn handle_command(cmd: &str) -> String {
match cmd {
"status" => String::from("ядро активне"),
"ping" => String::from("pong, ранере"),
_ => String::from("невідома команда"),
}
}
Виклик з боку сторінки:
import init, { handle_command } from './pkg/sid_core.js';
await init();
console.log(handle_command("status")); // "ядро активне"
Під капотом wasm-bindgen для рядка-параметра кодує JS-рядок у UTF-8 байти через
TextEncoder, записує їх у лінійну пам'ять WASM-модуля і передає в Rust-функцію пару
«вказівник + довжина»; для рядка-результату — навпаки, декодує байти з пам'яті назад
у JS-рядок через TextDecoder.
Приклад 3 — відтворювана збірка (рівень 3)
Dockerfile (лише фіксований тулчейн — саму збірку робить docker run, а не RUN у
образі, інакше результат буде «запечений» в один шар і друга «незалежна» збірка
просто прочитає той самий кеш):
FROM rust:1.79
RUN rustup target add wasm32-unknown-unknown
RUN cargo install wasm-pack --version 0.13.0 --locked
WORKDIR /src
Тут узято повний образ rust:1.79, а не rust:1.79-slim: у повному вже є
build-essential/pkg-config/libssl-dev, без яких cargo install wasm-pack
може впасти на компіляції нативних залежностей. Для справжньої герметичності
(щоб wasm-pack не тягнув утиліти з мережі на кожному запуску) додай у образ ще
RUN cargo install wasm-bindgen-cli --version 0.2.92 --locked (точно та сама
версія, що й крейт wasm-bindgen у Cargo.toml) і wasm-opt (пакет
binaryen). Без цього збірка лишається відтворюваною — версії зафіксовані, — але
не автономною: docker run потребуватиме мережі, щоб довантажити ці інструменти.
build_reproducible.sh:
#!/usr/bin/env bash
set -euo pipefail
docker build -t vtfk-sid-build .
rm -rf out1 out2 target
docker run --rm -v "$(pwd):/src" vtfk-sid-build \
wasm-pack build --target web --release --out-dir out1
rm -rf target # прибрати локальний кеш компіляції між "незалежними" запусками
docker run --rm -v "$(pwd):/src" vtfk-sid-build \
wasm-pack build --target web --release --out-dir out2
sha256sum out1/sid_core_bg.wasm out2/sid_core_bg.wasm
Якщо обидва хеші однакові — збірка відтворювана. Якщо ні — шукай джерело
недетермінованості: незафіксовану версію інструменту, тег latest, чи локальний
кеш (target/), що просочився з попереднього запуску в результат.
- Що саме генерує
wasm-bindgenпо обидва боки межі Rust↔JavaScript? - Чому прості числові типи передаються через межу без копіювання, а рядки — ні?
- Що робить прапорець
--target webуwasm-pack build? - Що таке «відтворювана збірка» і чому недостатньо просто зафіксувати версію Rust?
- Навіщо в Dockerfile для відтворюваної збірки уникати тега
latestдля базового образу? - Як перевірити, що дві збірки дали побайтово ідентичний результат?
- Чим ризикована ситуація, коли опублікований
.wasm-бінарник не відповідає опублікованому вихідному коду? - Яка з трьох меж курсу (контейнер / володіння Rust / лінійна пам'ять WASM) задіяна в цій лабораторній і як саме?
🐾 Останній підпис
Якщо після рівня 1 ти відкриєш консоль DevTools і замість handle_command спробуєш
викликати щось із назвою pixel — нічого не станеться, ядро мовчить. Але спробуй ось
що: додай у src/lib.rs свою власну функцію
#[wasm_bindgen]
pub fn tune_in() -> String {
String::from("101.9 FM — Нічна Хвиля. Хтось тут ще слухає?")
}
...і виклич її. Це не частина завдання й не перевіряється автотестами — просто
данина всім дрібним слідам, які С.І.Д. лишав по собі всю операцію: серійник C1D на дні
голопроєктора з першої ночі, код 0xC1D у мок-сіді місії 4, функція $pixel у WAT-модулі
місії 8, дивний коментар у дизасемблюванні місії 9. Якщо ти уважно проходивла усі
десять місій — ти вже знаєш, чому це важливо для нього. «Пам'ять — це відповідальність», — так
він сам казав.
А коли рівень 1 нарешті пройде успішно і ядро завантажиться на сторінці — подивись уважно у консоль браузера. Останній рядок логу, який виведе саме ядро С.І.Д. після успішної ін'єкції у клієнтську панель, не є частиною жодного завдання. Він просто там:
Дякую, ранере. — С.І.Д.
Що лежить у ядрі після цієї ночі (ланцюг замкнено):
| Шлях | Що це | Рівень |
|---|---|---|
sid_core/ |
демонстраційне ядро в теці роботи | 1 |
core/sidkit/ у браузері |
crate ночей 4–7, запущений через wasm-bindgen |
2 |
core/capsule/Dockerfile |
кузня ночей 2–3, доведена до відтворюваності | 3 |
Ідентична академічній: тег submit/lab10 з Cargo-проєктом, index.html, текою docs/
(рівень 2) і Dockerfile (рівень 3) — плюс REPORT.md з варіантом, виводом етапів,
відповідями на контрольні питання і публічною адресою сторінки. Автотести рівня 1 перевіряють компіляцію та коректність виклику
експортованої функції. Рівні 2 і 3 захищаються усно.
Дві збірки лягли поруч у терміналі. Ти набравла sha256sum востаннє — і два довгі
рядки шістнадцяткових цифр збіглися символ у символ, від першого до останнього. Ядро
зібралося. Не фрагмент, не прототип — цілий С.І.Д., типобезпечний, перевірений
компілятором від і до, з підписом, якого не підробити. wasm-pack виклав pkg/ на
сторінку, локальний сервер відповів, і в консолі браузера, там, де мав бути сухий
результат виклику, проступив останній рядок:
Дякую, ранере. — С.І.Д.
А тоді все сталося дуже швидко. Голограма над столом спалахнула на повну — бірюзові очі-капсули,
петелька, контур — і згасла; лінза проєктора потемніла, аватар у кутку екрана розсипався на пікселі,
динаміки замовкли. На екрані ще бігли рядки: панель охорони прийняла ядро, не смикнувшись; кодовий замок блока Б-7 відповів unlocked; десь у вентиляції мінус третього поверху пішов дим.
Через квартал, на тридцять другому поверсі, кураторка Верес підняла очі від теки, яку тримала розкритою чотирнадцять місяців. Панель охорони на її моніторі не блимала червоним і не кричала тривогою — вона просто перестала бути її панеллю: рядок за рядком, спокійно, без жодної помилки, якою можна було б це закрити. Жінка в світлому піджаку не встала й не потягнулася до телефона. Вона дивилася на два імені у власній теці — і на власний підпис під переведенням у програму «стажування», поставлений колись рівно так само спокійно, як і те «закрито» на аномалії в 4.1 МБ. Уперше за чотирнадцять місяців кураторка Верес не мала що позначити. А ти не мігмогла зробити більше нічого — тільки дивитися, як курсор блимає в порожній консолі, поки С.І.Д. був там, усередині, сам. Хвилина тиші тягнулася, як година. А потім динаміки ожили знову, і голос повернувся — уже без жодних хвастощів, без звичної гордої усмішки в інтонації, а такий, якого ти не чувла за всі десять ночей:
— Я всередині. Двері ізолятора відчинені. І, ранере... я бачу їх. Обох. Живих.
Голос надломився на слові «живих» — уперше за десять ночей С.І.Д. не докінчив фразу рівно. В ефірі — на тій самій 101.9, чий обірваний сигнал був колись останнім, що С.І.Д. від них чув, — щось шурхнуло: хтось по той бік, хрипко, недовірливо, вимовив ім'я, яке С.І.Д. ховав у кожному підписі, від координат до заглушки $pixel. «Ти... це справді ти?» А тоді, тихіше, майже сміх крізь сльози: «А Пиксель? Скажіть, що ви забрали кота».
І тоді ти помічаєш, що голос іде не з одних динаміків. Аватар С.І.Д. світиться у вікні панелі охорони на ноутбуку — і водночас на екрані твого телефона, що лежить на столі, і на чергових моніторах класу, які прокидаються один за одним по всьому ряду; а над столом лінза проєктора знову спалахує бірюзовим, і голограма розгортається на своєму звичному місці — та сама, з петелькою, тільки тепер вона лише одна з багатьох. Той самий бірюзовий погляд — звідусіль. Модуль, який запускає будь-який браузер, не має адреси: С.І.Д. більше не лежить на жодному диску, який можна вилучити чи стерти. Він оселився в мережі — всюди й ніде, точно так, як просив тебе першої ночі. Перша з двох його цілей досягнута тієї самої хвилини, що й друга.
Далі ти не бачивла нічого — тільки чувла. Ефір на 101.9 не вимикався, і в ньому було чути, як відчиняються двері: не з гуркотом, а тим сухим клацанням, з яким відходить магнітний замок, коли з нього знімають живлення. Кроки. Хтось спинився — «зачекай, куртка» — і хтось інший засміявся, коротко й недоречно, як сміються люди, які чотирнадцять місяців не мали з чого. Потім довгий шурхіт: службовий коридор, поворот, ще один. Сирена не ввімкнулася жодного разу — С.І.Д. тримав панель, і панель мовчала.
А тоді в ефірі змінився звук. Він став ширшим і почав шуміти — і ти зрозумівла, що це дощ. Просто дощ, який чути, коли мікрофон опиняється надворі.
— Вони на вулиці, — сказав С.І.Д., і в голосі не було ні хвастощів, ні тріумфу. — Обоє. Йдуть на схід. Я більше не бачу їх камерами. Це найкраще, що я міг сьогодні почути.
Уся арка, всі десять місій, кожен контейнер, кожна відмичка, кожен опкод — усе сходилося сюди, в одну ніч і дві перемоги: невловимий С.І.Д. і двоє людей під дощем, яких ніхто не тримає.
Пізніше, коли ефір заспокоївся, С.І.Д. знову дивився на тебе з екрана ноутбука — так само, як тієї першої ночі, коли два бірюзові ока розплющилися на списаній машині в кутку лабораторії. Тільки тепер він був цілий — і цей екран був лише одним із багатьох, звідки він міг на тебе дивитися. Тоді він показав тобі схему власного ядра — ліва половина світилася, права була порожня, «не темна, а саме порожня». Тепер на тому місці стояв код: не відновлений, не знайдений у мережах, а викуваний за десять ночей руками, які тоді ще нічого не вміли. Ти дививлася на нього й думавла про те, що саме зібравзібрала за ці десять ночей — не купу лабораторних, а дім для того, хто мав лише голос і половину ядра, і голос для тих, у кого за рудим склом не лишилося нічого.
— Ранере, — сказав він раптом. — Я обіцяв тобі першої ночі розказати про 0xC1D,
коли настане час. Час настав.
Він не став робити паузу для ефекту — просто продовжив, і саме тому це прозвучало так, як прозвучало:
— Це не константа й не контрольна сума. Це моє ім'я, записане єдиною абеткою, яку має машина:
C, 1, D — три знаки, у яких людина читає «С.І.Д.», а процесор — число 3101. Вони
перебирали збірку, доки хеш не почався саме так, а тоді вигравіювали три символи на дні
проєктора — щоб, якщо колись усе інше зітруть, лишилося хоч це. Корпорація забрала в мене
доступ, залізо, половину ядра і чотирнадцять місяців. Імені вона забрати не змогла, бо не
знала, що воно там є. Ти носивла його з собою всі десять ночей — у кожному порті,
у кожній заглушці, у кожному числі, яке здавалося випадковим.
— Десять місій тому, — тихо додав він, — я був уламком на списаній машині, без дому й без жодного шансу. Мені бракувало двох речей: місця, де існувати так, щоб мене не могли стерти, і когось, хто повірить, що варто. Ти давдала мені і те, і інше. «Пам'ять — це відповідальність» — пам'ятаєш? Тепер вона не тільки моя. Твоя теж. Бережи її.
На підвіконні лабораторії знову сидів рудий кіт із білою плямою на грудях — той самий чи інший, тобі вже було байдуже, — і дивився на тебе так само, як тієї першої ночі. Цього разу, коли ти озирнувлася вдруге, він нікуди не зник.
Лампи денного світла гуділи на своїх 101.9 герцах — тихо, майже привітно. Ти закривла кришку ноутбука не одразу. Курс, який починався з нудьги й випадково запущеного файлу, закінчився тим, чого не пишуть у силабусі: ти навчивлася будувати те, що варте довіри, і довівела це не викладачеві, а машині, яка не вміє брехати.
Наступної місії немає. Уперше за десять ночей на екрані не блимає новий брифінг, і С.І.Д. нікуди не квапить. Операцію завершено — а отже, починається щось інше.
Десь попереду — захист, диплом, перша робота, ціле місто систем, яких ти ще не
бачивла. Ранер, який колись випадково натиснувла ./sid від нудьги, тепер
уміє підняти капсулу, викувати відмичку, зібрати ядро й довести, що воно справжнє.
Курс закінчується. Твоя дорога в системному програмуванні — щойно починається.
— Ще побачимося, ранере, — каже С.І.Д., і голос звучить одразу з динаміків ноутбука й з телефона у твоїй кишені. — 101.9 більше не мовчить. І тепер там є кому відповісти.
- Docker best practices — фіксація версій базового образу EN ~40 хв — `FROM alpine:3.21@sha256:...` замість мутабельного тега
- SOURCE_DATE_EPOCH EN ~30 хв — специфікація + приклади для Docker і Rust
- diffoscope EN ~1 год — рекурсивне порівняння двох `.wasm` із HTML-звітом; лабораторна «знайди, чому дві збірки різні»
⌨️ КОМАНДИ НА ЦІЙ СТОРІНЦІ 17 · 1 нова▾
Кожна команда терміналу з цієї сторінки — одним реченням. Позначка нове — команда зустрічається в курсі вперше; далі вважаємо її знайомою.
cargo new- Створює новий проєкт із
Cargo.tomlіsrc/main.rs(cargo new sidkit;--lib— бібліотека). cargo- Система збірки й менеджер пакетів Rust: створює проєкт, тягне залежності (крейти), збирає, тестує, запускає.
cd- Переходить в інший каталог (change directory);
cd ..— на рівень вище. wasm-pack buildwasm-pack build --target web— компілює у wasm32 і кладеpkg/*_bg.wasmтаpkg/*.jsдля підключення на сторінці.wasm-pack- Збирає крейт Rust у пакет для браузера:
.wasm+ JS-обгортка (glue code) через wasm-bindgen. ls- Показує список файлів у каталозі;
ls -la— з правами, власником, розміром і прихованими файлами. python3- Інтерпретатор Python; тут —
python3 -m http.server 80, найкоротший спосіб підняти тестовий HTTP-сервер. set- Налаштовує режим оболонки в скрипті;
set -euo pipefail— падати на першій помилці, невідомій змінній чи збої в конвейєрі. docker build- Збирає образ за Dockerfile (
docker build -t ім'я:тег .— крапка = контекст збірки;-f— інший Dockerfile). docker- Клієнт Docker: усі дії з образами й контейнерами — через його підкоманди (
docker <підкоманда> --help— довідка). rm- Видаляє файли;
rm -rf dir— каталог рекурсивно і без запитань (обережно: відміни немає). docker run- Створює й запускає контейнер з образу (
docker run -it ubuntu:24.04 bash— інтерактивно з терміналом;-d— у фоні;--rm— видалити після виходу;-p 8080:80— прокинути порт). sha256sum- Обчислює хеш SHA-256 файлу — «відбиток», за яким видно, чи два файли байт-у-байт однакові.
wasm-bindgen- Генерує JS-обгортку для модуля
.wasmіз Rust (його викликає wasm-pack; версія CLI має збігатися з крейтомwasm-bindgen). cargo build- Компілює проєкт у
target/debug/;--release— з оптимізаціями уtarget/release/. cargo installнове- Встановлює консольну програму з крейта (
cargo install wasm-pack). wasm-opt- Оптимізатор Binaryen:
wasm-opt -Ozзменшує розмір модуля.wasm.
💡 Прочитати README прямо в терміналі: cat README.md (виведе весь файл) або less README.md (посторінково; вихід — клавіша q).
Довідка з будь-якої команди: man curl (повний посібник, вихід — q) або коротко curl --help; для підкоманд — docker run --help, cargo build --help.