nos_
ENRU
x86-64 · arm64 · C++20 · Rust

Что такое 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, который загружается как модуль.
  • Блог: истории о запуске на железе, отладке и проектных решениях.