Что такое nos
Ядро для x86-64 и arm64 на C++20 и Rust, безопасное по замыслу: работает в облаках, на Apple Silicon и на голом железе и запускает гостевые Linux.
nos — ядро операционной системы для x86-64 и arm64, написанное на C++20, Rust и немного на ассемблере. Ядро freestanding: без libc, без STL, без исключений и RTTI в C++. На x86-64 ядро грузится через GRUB по Multiboot2 — и с legacy BIOS, и с UEFI, на arm64 — по протоколу Linux Image. Работает под QEMU/KVM, в облаках на KVM, на Apple Silicon под HVF и на трёх реальных машинах: ноутбуке и двух арендованных выделенных серверах, у которых единственная консоль — сеть.
Внутри — собственные SMP-планировщик и менеджер памяти, а поверх них стек TCP/IP с HTTPS, SSH-сервер, ext2 на чтение и запись, NVMe и драйверы сетевых карт Realtek и Intel. Ядро загружает модули на Rust, и один из этих модулей — гипервизор первого типа, который запускает немодифицированные дистрибутивы Linux.
Дорожная карта в plans/README.md ведёт к облачному узлу на голом железе: одно ядро владеет сервером, запускает гостевые Linux под собственным гипервизором на Rust, управляется по сети и обновляется под работающими гостями — и никакого Linux-хоста под ним. Сама дорожная карта честно называет всё это «инженерным проектом и портфолио, а не заявкой на продукт», а README прямо очерчивает границы: проверены только перечисленные ниже машины, всё остальное не тестировалось.
Цель: безопасность по замыслу и по коду
Правила, которым следует каждое изменение, собраны в CLAUDE.md. Прежде чем объяснить, как что-либо собрать, он говорит, ради чего ядро вообще пишется:
Ядро строится настолько безопасным, насколько вообще можно сделать ядро: никакого неопределённого поведения, никакой порчи памяти, никаких гонок данных, никаких взаимоблокировок, никаких утечек ресурсов на путях ошибок, никаких сбоев, которые проходят молча.
Безопасность здесь — не свойство, которое проверяют задним числом, а то, ради чего выбирается каждое решение и каждая строка; она стоит выше скорости, краткости и желания поскорее влить фичу. Причина — в том, куда движется проект: машина, на которой работают чужие гости, не переживёт одного класса ошибок — гостя, добравшегося до памяти хоста. На практике цель превращается в пять привычек:
- Сначала — дизайн. Из двух решений выбирается то, в котором ошибку нельзя даже написать. Блокировка владеет данными, которые охраняет. Хэндл, пришедший снаружи, ищется в таблице, а не разыменовывается. Образ ядра — W^X. Ядро не выполняет
.init_array, поэтому глобальный объект с не-constexprконструктором так и остался бы нулями — и оба скрипта линковщика отказываются такой объект собирать (история). - Потом — код. Новый код пишется на Rust, с тем минимумом
unsafe, который допускает задача. Написанный C++ перечитывают ещё раз — только в поисках неопределённого поведения. Каждое возвращаемое значение проверяется, каждый путь ошибки освобождает то, что успел захватить, и ничего не выделяется под спинлоком (почему). - Сбой должен быть громким. Нарушенный инвариант ловят
BugOnиPanic. TLB shootdown, на который никто не ответил, через десять секунд паникует и называет промолчавшие CPU, а не вешает машину. Netconsole выносит backtrace паники с машины, у которой нет другой консоли. Как сказано в самом проекте, «зависание без отчёта — худший исход из возможных». - Баг — это вопрос к дизайну. Исправить конкретный случай — полдела. Вторая половина — закрыть весь класс типом, проверкой при линковке или гейтом, чтобы следующая такая ошибка не скомпилировалась, не слинковалась или провалила гейт.
- Ни скорость, ни безопасность не принимаются на веру. Производительность измеряют, а «поведение не изменилось» показывают — сравнением бинарников и прогоном гейтов, а не утверждением.
Как оно устроено
Дерево исходников делится на две половины с жёсткой границей между ними. C++ — это фундамент: загрузка и запуск каждой архитектуры, HAL, таблицы страниц и аллокаторы, планировщик, блокировки, диспетчеризация прерываний, таймеры, ядро шелла, загрузчик модулей и несколько драйверов, на которых стоит всё остальное (последовательный порт, консоли, таймеры, 8042, PCI, MSI-X, ACPI). Всё, что стоит сверху, — Rust: драйверы дисков, сетевых карт, USB и virtio, блочный слой, сетевой стек, файловые системы, TLS, SSH, пул случайных чисел и гипервизор. Каталогов src/cpp/net, src/cpp/fs и src/cpp/block больше нет: эти слои переехали на Rust 18 сентября 2026 года (как это было).
modules/* sshd, netblk, netload, blkload, hv ... loadable .ko, Rust only
------------------------------------------------------------------------------
drivers/* nvme, virtio_{blk,net,scsi,rng}, r8168, r8125, igb, usb
layers block net (over netwire) fs virtio tls random
kcore safe wrappers around kernel services
ffi raw extern "C" declarations, nothing else
========================== C ABI: rust_ffi.cpp ===============================
C++ and asm boot, hal/ over arch/x86_64 and arch/arm64, mm/, scheduler,
interrupts, timers, locks, ACPI, PCI, MSI-X, consoles,
the shell's core, the module loader
Сервис ядра попадает в Rust в три шага: функция на C++ в rust_ffi.cpp (единственном C++-файле моста), её «сырое» объявление в крейте ffi и безопасная обёртка в kcore, где хэндл превращается в тип с Drop. Выше слои и драйверы — обычный Rust. Оставшийся в них unsafe живёт на границе C ABI, куда указатель и длина приходят из C++ или из модуля, либо это unsafe impl, утверждающий, что структура — просто набор слов. Счёт ведёт scripts/unsafe-count.py: на master в октябре 2026 года он насчитывает 68 мест в net, 14 в block, 47 в fs и 15 на все девять крейтов драйверов; в kcore — 268.
Модули — способ поставлять необязательное: сервер, генератор нагрузки, диагностику. Модуль — это крейт на Rust, собранный в .ko и загружаемый в работающее ядро командой insmod. Загрузчик связывает его с таблицей экспорта, сгенерированной из объявлений ffi, выдаёт каждому сегменту права, которые просит его заголовок (запись и исполнение — никогда одновременно), и отказывается грузить модуль, собранный под другой интерфейс ядра. SSH-сервер — модуль; netblk, раздающий NVMe-диск по UDP без копирования, — тоже; и гипервизор.
Гипервизор — это три крейта. В hvarch — расширение виртуализации процессора и почти весь unsafe гипервизора: 201 место. В hv — всё, что выше (память гостя, цикл обработки выходов, эмулируемые устройства): около 12 тысяч строк кода и 4 места с unsafe. В modules/hv — команды шелла, без единого unsafe. Всё это компилируется только в hv.ko, так что в ядре без модуля гипервизора нет вовсе.
Подробно об этом — в Rust в ядре и в описании дерева исходников.
Что в нём есть
Полный список — в Features. Если коротко:
- Две архитектуры за одним HAL. x86-64 грузится с ISO или с MBR-диска, под BIOS или UEFI. arm64 работает на QEMU
virt, с GICv3 и доставкой MSI от PCIe через ITS. - SMP до 64 CPU, из них 20 опробованы на реальном железе: очереди задач на каждом CPU, вытеснение, балансировка нагрузки, собственный тик у каждого CPU, TLB shootdown через IPI.
- Память: четырёхуровневая страничная адресация, вся RAM машины до 512 GiB, аллокаторы страниц, пулов и виртуальных адресов, образ ядра W^X и NX на каждом отображении, созданном во время работы.
- Хранение данных: асинхронный блочный слой поверх NVMe, virtio-blk и virtio-scsi, с MBR и GPT. VFS с ext2 на чтение и запись, nanofs (собственная файловая система ядра с контрольными суммами и copy-on-write), ramfs и procfs; корень выбирается по устройству, метке или UUID.
- Сеть: ARP, IPv4, ICMP, UDP, TCP, DHCP, DNS, NAT и HTTP-клиент, умеющий HTTPS через rustls (TLS 1.3 и 1.2, сертификаты проверяются по корням Mozilla). Драйверы virtio-net, Realtek RTL8168 и RTL8125, Intel 82576 и I210.
- Как попасть на машину без экрана: SSH-сервер (ключи Ed25519); UDP-шелл, выключенный, пока его не попросят; netconsole — лог ядра по UDP; дисковый лог для загрузки, которая умирает раньше, чем поднимется сеть.
- Наблюдаемость: dmesg, символизированные backtrace, сэмплирующий профилировщик на PMU,
top, watchdog блокировок. - Случайность: пул ChaCha20, засеянный из всех источников энтропии, какие есть у машины.
- Гипервизор: AMD-V и Intel VT-x; гостевые Linux до 16 CPU с дисками и сетевыми картами virtio, коммутатор с DHCP и NAT, таблицы ACPI, эмуляция MMIO и IO-APIC. Alpine, Debian и Ubuntu грузятся в том виде, в каком их выпускают (подробнее).
- Системный образ для серверов:
nos-installраскладывает его на раздел рядом с собственным Linux сервера и один раз загружает nos через собственный GRUB сервера (установка).
Где оно работает
| Где | Что это | Что это дало |
|---|---|---|
| QEMU/KVM, Google Cloud, Yandex Cloud | Основная мишень: ISO или MBR-образ диска nos.qcow2 с устройствами virtio |
Google Cloud импортирует образ с опцией «Skip OS adaptation»: гостевой агент nos не нужен |
| Apple Silicon | QEMU virt с HVF |
Быстрый цикл разработки — примерно вчетверо быстрее TCG |
| Dell Latitude 5480 | Ноутбук на Skylake-U под UEFI, без последовательного порта и без контроллера PS/2 | Консоль на пиксельном фреймбуфере и USB-клавиатуру через xHCI — на них там держится вся сессия |
| Hetzner EX44 | Intel i5-13500: 6 P-ядер и 8 E-ядер, 20 потоков, Realtek RTL8125 | APIC ID вразброс от 0 до 62 — поэтому MaxCpus равен 64. Первое настоящее железо Intel, на котором заработал VT-x, — 2026-10-20 |
| Hetzner AX41 | AMD Ryzen 5 3600 (Zen 2), Intel I210 | Запуск драйвера igb, профилировщик на счётчиках AMD, два IO-APIC, первый гостевой Linux на настоящем AMD-V |
Ни у одного из серверов нет ни последовательного порта, ни IPMI, так что на AX41 поднимаемый драйвер сетевой карты был заодно единственным каналом, по которому ядро могло хоть что-то сообщить. Тамошний I210 защёлкнул в MAC скорость 10 Мбит/с, пока PHY работал на 1000, — и не принимал ничего, хотя линк, приёмник, очередь и дескрипторы выглядели исправными (как это искали). Подробно о каждой машине — в Real hardware.
Как его проверяют
Тест-раннера нет. Проверки идут слоями — от тех, что запускаются чаще всего, к самым редким (Tests and gates описывает все):
- Самотесты при каждой загрузке — на C++ и на Rust. Один из них гоняет производителей и потребителей lockless-кольца на разных CPU: кольцо, в котором порядок доступа к номерам последовательности ослабили до relaxed, провалило восемь загрузок из восьми под HVF — а TCG на x86-хосте может этого так и не показать.
- Smoke-загрузки. CI собирает обе архитектуры и загружает каждую без экрана, дожидаясь в последовательном порту маркеров
After test,Preempt is now onиboot: completeи падая наPANIC:. Загрузка x86 подключает virtio-blk, virtio-scsi, NVMe, virtio-net и virtio-rng. - Сборки, ловящие неопределённое поведение.
UBSAN=1ставит санитайзер clang над всем C++,RUSTUB=1включает в core проверки предусловийunsafeи переполнений. CI загружает такую сборку на обеих архитектурах, и первый же отчёт — это паника. Первые прогоны нашли, среди прочего, загрузочный стек arm64, налезавший на таблицы страниц под ним, и строку лога, которую вместо обрезки выбрасывали целиком, — так бесследно пропадали сообщения паники. - Гейт на каждую подсистему: 22 скрипта, каждый написан под сбой, который выглядит как успех. Например, драйвер файловой системы, который читает назад собственные записи и потому согласен сам с собой, по какому бы смещению ни писал. Или пул сетевых кадров, который так и не был создан, отчего каждый пакет молча уходил в аллокатор. Гейт оценивается по коду возврата, а не grep'ом по логу (как их запускать).
- Фаззеры на хосте.
hv-fuzz,net-fuzz,fs-fuzzиcpp-fuzzкомпилируют собственные исходники ядра под хост, кормят их случайными входами (выходы гостя, сетевой трафик, образы дисков, таблицы прошивки) и сверяют каждый кусок с моделью того, что он должен делать; C++ при этом работает под ASan и UBSan. CI гоняет их на фиксированных входах (что они нашли). - Настоящий Linux, вручную.
hv-linux-test.pyиhv-distro-test.pyзагружают под гипервизором настоящие ядра Linux и дистрибутивы и проверяют, что увидели гости.
Куда оно движется
В дорожной карте шесть этапов, пронумерованных в порядке выполнения:
| Этап | Результат | Где мы сейчас |
|---|---|---|
| 0 | nos загружается на реальном серверном железе и остаётся управляемым | Демо работает: EX44 и AX41 получают адрес по DHCP, отвечают шеллом по сети и отдают свой лог, а системный образ ставит новый сервер |
| 1 | Новый код — на Rust, unsafe заперт |
Не веха, а дисциплина; ей подчиняется всё, что написано с тех пор |
| 2 | HAL и arm64 под QEMU и HVF | Порт на arm64 влит в июле 2026 года, с тех пор обе архитектуры зелёные в CI. Реальное железо arm64 отложено |
| 3 | Гипервизор на Rust загружает Linux до шелла | Здесь идёт работа сейчас. Все четыре демо готовы, с тех пор гости получили SMP, диски, сеть, ACPI и VT-x. Прежде чем гостем станет чужая машина, нужно закрыть список по итогам код-ревью. На arm64 бэкенд пока только сообщает, что предлагает EL2 |
| 4 | HTTP-панель управления виртуальными машинами | Не начат. Жизненный цикл, который она будет обслуживать, уже есть в виде команд шелла: hv start, list, attach, stop |
| 5 | Живое обновление ядра хоста под работающими гостями | Не начат. Его требование — всё состояние ВМ хранится как сериализуемые простые данные — соблюдается с первой строки гипервизора |
Откуда оно взялось
Первый коммит, Init, датирован 30 марта 2017 года. За считаные дни у ядра появились драйвер клавиатуры 8042, IDT, вывод в последовательный порт и PIT, за несколько недель — SMP и планировщик round-robin. За тот год набралось 133 коммита, в 2018-м — ещё 18, последний 24 мая 2018 года. А потом почти восемь лет — тишина.
Работа возобновилась 9 февраля 2026 года, и темп стал совсем другим: из 688 коммитов в master на 21 октября 2026 года 537 сделаны в 2026-м, из них 275 — только в сентябре. Rust появился 28 марта вместе с драйвером NVMe; arm64 напечатал первую строку 16 июля; ноутбук, EX44 и AX41 добавились между концом августа и началом сентября. 18 сентября блочный слой, сеть и файловые системы переехали на Rust. Гипервизор начат 22 сентября и уже на следующий день загрузил Linux до шелла BusyBox; фаззеры появились в конце месяца, системный образ — 17 октября.
Как его пишут
Мы пишем nos вместе с ИИ-ассистентами для программирования, и README говорит об этом прямо: код частично написан моделями ИИ (Claude, GPT), а архитектурные решения, тестирование и ревью кода остаются под контролем человека. Это видно и по истории: 397 из 688 коммитов несут трейлер Co-Authored-By: Claude.
Работать так позволяет та же машинерия, на которой держится честность самого ядра. Правила записаны один раз — в CLAUDE.md. У каждой подсистемы есть гейт, который оценивается по коду возврата. В сообщениях коммитов сказано, что проверялось: какие гейты прошли, что измерено, что запустить не удалось. Про сетевой драйвер, который ничем не эмулируется, так и записано: «compiled, diffed, not run» — собран, сверен, не запускался.
Лицензия
nos распространяется под лицензией MIT (LICENSE); исходники — на github.com/irqlevel/nos.
Что дальше
- Соберите его, а потом загрузите в QEMU — на x86-64 или на Mac с Apple Silicon.
- Документация: все подсистемы, от загрузки до планировщика и гейтов.
- Гипервизор: гостевые Linux под гипервизором на Rust, который загружается как модуль.
- Блог: истории о запуске на железе, отладке и проектных решениях.