Configuring & Minifying the Kernel¶
Aegis is a single source tree that spans an enormous range of builds — from a
stripped, MCU-class kernel up to a full-featured x86-64 / arm64 system —
selected entirely by a .config, the same way menuconfig tailors a Linux
kernel. The goal is deliberately ambitious: smaller than a Linux tinyconfig
at one end, and reaching for the full breadth of a general-purpose kernel at the
other — from one tree, one build system.
Where the configuration engine lives
The engine is its own repository: github.com/LoricaOS/kconfig.
It is a small, dependency-free C program — no Python, no flex/bison, no
ncurses — so it builds with a plain C compiler and can eventually run on
LoricaOS itself. The kernel-specific Kconfig files, the defconfigs, and
the Makefile hooks live in the Aegis
tree; the tool just consumes them.
How it works¶
kconf reads a tree of Kconfig files (a pragmatic, bool-only subset of
the Linux Kconfig language — LoricaOS has no loadable modules, so there is no
tristate), resolves your selection to a .config, and generates the two
artifacts the build consumes:
| Artifact | For | Example |
|---|---|---|
build/generated/autoconf.h |
C source #ifdef guards |
#define CONFIG_NET 1 |
build/auto.conf |
Makefile object gating | CONFIG_NET=y |
The Makefile -includes auto.conf to gate which source files compile, and
-includes autoconf.h into every translation unit so #ifdef CONFIG_* works
in the source. Dependency resolution matches Linux semantics where they overlap:
depends on gates whether a symbol can be set, select forces its target on,
and values settle to a fixpoint.
Tiers¶
Several defconfigs ship in configs/, and you can write your own:
| Tier | For | Includes |
|---|---|---|
nano |
The minification floor — leanest bootable kernel | Security core + boot + mm + sched + a serial console. No storage, USB, framebuffer, or anything else optional. |
microvm |
A Firecracker / cloud-hypervisor / QEMU-microvm guest | PVH direct boot (no bootloader), no PCI bus, no ACPI, virtio-mmio transport, virtio-blk + virtio-net, ext2 root, networking, serial. The smallest useful Aegis — see MicroVMs below. |
tiny |
Minimal but with the usual PC devices | The core plus block storage, USB, framebuffer — just no net/VM/audio/debug. |
workstation |
A text-only, networked system with minimal bloat | The above plus the full network stack, persistent ext2, procfs, pipes — but no audio, no debug trace, only the drivers a headless text workstation needs. |
full |
Everything on | Every subsystem and driver. Identical to the historical bare-make build. |
The workstation tier is the feature contract for an eventual PicoCalc spin
(see below): everything you want in a pocket text terminal, and nothing you
don't. The microvm tier is the cloud counterpart — a kernel that boots
straight into a microVM the way Firecracker expects.
Using it¶
make tiny_defconfig # smallest bootable kernel
make microvm_defconfig # Firecracker/microvm guest (PVH, no PCI/ACPI, virtio-mmio)
make workstation_defconfig # text-only, networked
make full_defconfig # everything on (the default)
make allnoconfig # every optional prompt off
make oldconfig # keep your .config, fill in new symbols
make # builds the selected .config
# (a bare make with no .config seeds full_defconfig,
# so the historical everything-on build is unchanged)
make test-microvm # build the microvm tier + a test rootfs and boot it
# in QEMU -machine microvm to a ring-3 userspace all-pass
The one thing you can never turn off¶
The security core has no config symbol
The capability subsystem (kernel/cap), the cap_check at the syscall
boundary, and uaccess are the product. There is deliberately no
CONFIG_ knob for them — they stay in the unconditional base build, and a
link-time check keeps them present. A minified Aegis is still, in full, a
capability kernel.
This is the one place LoricaOS departs hard from Linux's tinyconfig
philosophy: here, "tiny" can never mean "drop the security model."
Minification only ever exposes knobs for genuinely optional code.
CONFIG_MMU — the embedded axis¶
The paging-based memory model (VMM, page tables, per-process address spaces,
demand paging, copy_to/from_user with its page-fault exception table) sits
behind CONFIG_MMU, a first-class symbol (default y) from the very first
skeleton. The point is that a no-MMU target is a config axis, not a fork.
The long-term aim is a build that fits an RP2350 (Raspberry Pi Pico 2) —
520 KB of SRAM, a Cortex-M33 with an MPU, not an MMU — so a
Clockwork PicoCalc could run a real
capability-secure OS: a sibling to LoricaOS, or a dedicated PicoCalc spin. That
needs CONFIG_MMU=n to select a flat, MPU-isolated memory backend, a new
arch/armv8m port, and PicoCalc drivers (LCD console, matrix keyboard, SD,
Wi-Fi) — all future work. But the elegant part is already true: the capability
model is software-enforced and MMU-independent, so the thing that makes
Aegis Aegis survives the port. You lose hardware memory isolation (a real,
loudly-flagged trade-off); you keep capability authority.
On the RP2350's 520 KB SRAM the relevant budget is .bss + .data, not
.text — the chip executes code from external QSPI flash via XIP, so code and
rodata live in flash (up to 16 MB) and only static/heap/stack data occupy SRAM.
That is exactly why the arena work matters more than code trimming for that
target: tiny's .bss is already 396 KB — under the RP2350's 520 KB SRAM —
and it still carries x86-only per-CPU / GDT / TSS structures an armv8-M core
wouldn't have, plus a PMM bitmap that sizes to actual RAM (a fraction on an MCU).
Every knob a constrained target needs now exists; what's left is the arch port
itself. A staged pico2_defconfig already records the target's decisions —
MMU=n, every x86/VM subsystem off, arenas sized for 520 KB, and the four
PicoCalc drivers (GRAM char-cell LCD, I²C keyboard, SPI-SD + FAT, CYW43 Wi-Fi)
on the bring-up list. It resolves cleanly through kconf today; it builds once
arch/armv8m + the no-MMU backend land.
MicroVMs — boot in Firecracker¶
If the MMU axis reaches down to the MCU, the microVM axis reaches up to the
cloud. A microVM (Firecracker, cloud-hypervisor, QEMU -machine microvm) is a
deliberately stripped machine: no bootloader, no BIOS, no PCI bus, no legacy
devices, and — on Firecracker — no ACPI at all. That is not a limitation to
work around; it is exactly the minification frontier. Building for a microVM
and deleting the biggest remaining platform chunks are the same work, so the
microvm tier is both the leanest useful Aegis and a first-class deploy
target. It boots to a full ring-3 userland — verified by make test-microvm,
which runs the entire 22-test userspace suite (processes, fork/COW, signals,
exec, mmap, clock_nanosleep, capability enforcement, filesystem) to an
all-pass inside a bare microVM.
Three config axes and a boot path make it work:
PVH direct boot. MicroVMs run no bootloader — the VMM loads the kernel ELF
and jumps to a 32-bit entry named by an ELF note, handing over an
hvm_start_info (the Xen PVH boot ABI). Aegis emits that note and translates the
struct into the same arch-neutral bootinfo the Limine path uses, so it launches
directly in Firecracker and cloud-hypervisor. Limine and multiboot2 still work
for bare-metal / QEMU-pc; PVH is simply a third entry, always present.
CONFIG_VIRTIO_MMIO — the transport. MicroVMs expose virtio devices not on
a PCI bus but as flat MMIO register blocks at fixed addresses (the base differs
per VMM). The virtio stack is split into a transport-neutral core plus PCI and
mmio back-ends, so every device driver — blk, net, rng, … — works over either
transport unchanged. Devices are discovered from the Linux-standard
virtio_mmio.device=<size>@<base>:<irq> kernel-cmdline entries (which Firecracker
appends automatically), with a fixed-window scan of QEMU microvm's transport
array as a fallback.
CONFIG_PCI — droppable now. With virtio-mmio there is no PCI bus to
enumerate, so CONFIG_PCI=n swaps the whole PCIe/ECAM path for a stub reporting
an empty bus. Every PCI-only device (NVMe, AHCI, xHCI/USB, PVSCSI, HD-audio)
depends on PCI, so they leave with it. This is minification the microVM makes
correct: dead weight on a machine that has no PCI at all.
CONFIG_ACPI — droppable for Firecracker. Firecracker ships no ACPI tables.
CONFIG_ACPI=n swaps acpi.c for a stub that reports no MADT/MCFG; the kernel
falls back to BSP-only SMP and drives its timekeeping tick from the LAPIC
timer instead of the (absent) 8254 PIT IRQ — so nanosleep and every timeout
still work with no I/O APIC. PCI depends on ACPI (x86 PCIe config space comes
from the MCFG table), so this drops the bus too.
The result is a ~233 KB-.text kernel that boots a real capability-secured
userland the way Firecracker boots one — raw ELF in, virtio-mmio out, nothing
legacy in between:
make microvm_defconfig && make
qemu-system-x86_64 -machine microvm -global virtio-mmio.force-legacy=false \
-kernel build/aegis.elf -append "boot=text" \
-drive id=root,file=rootfs.img,format=raw,if=none \
-device virtio-blk-device,drive=root -nographic
cloud-hypervisor is covered by the same PVH boot with ACPI + PCI + virtio-pci left on (it uses a PCI bus), so a single tree serves the whole microVM matrix.
Pluggable disk filesystem — ext2 is now optional¶
The root/disk filesystem sits behind a small vtable (fs_ops, resolved through
a single g_rootfs pointer), so the disk backend is a config choice and exactly
one is linked:
CONFIG_ |
Backend | Notes |
|---|---|---|
FS_EXT2 (default) |
ext2 | The historical backend, full Unix ownership + permissions (DAC). ~34 KB. |
FS_FAT |
FAT32 | Read-write, ~⅓ of ext2's code. The format every SD card and PC speaks. |
| (neither) | nullfs | No disk at all — the VFS serves only initrd (RO) + tmpfs; any disk path fails. For a diskless embedded build. |
FS_FAT depends on !FS_EXT2 — both provide the same root backend, so you pick
one. The FAT driver is a complete read-write filesystem: create/read/write/
unlink/truncate, mkdir/rmdir, rename/move, and long filenames (VFAT LFN,
both reassembled on read and generated on create), with FAT date fields decoded
into inode times. It is on-disk-compatible with standard tooling — a volume
written by Aegis mounts and reads correctly under mtools/Linux/Windows, and
vice-versa.
FAT has no Unix ownership or permissions, so chmod/chown are accepted
no-ops and the passwd/shadow anchors report absent. That is not a security
downgrade: on Aegis, authority never came from file-mode bits — it comes from
the capability layer at the syscall boundary, which is identical on either
backend. FAT simply drops a DAC layer Aegis was already treating as advisory.
This is the storage story for the PicoCalc spin: FAT on the SD card, no ext2, no DAC overhead, everything mountable on a host for flashing and recovery.
make nano_defconfig # ext2 off by default in nano → nullfs
echo CONFIG_FS_FAT=y >> .config
echo CONFIG_VIRTIO=y >> .config # (need a block transport to reach a disk)
echo CONFIG_VIRTIO_BLK=y >> .config
make oldconfig && make # links the FAT backend instead of nullfs
What it buys you (measured)¶
Gated so far — the network stack, the trace ring, the Hyper-V stack, the HDA driver, and the whole VirtIO family (per device) — across the three tiers on x86-64:
| tier | .text (code) |
.bss (static RAM) |
|---|---|---|
full |
585,886 B | 3,089,924 B |
workstation |
551,182 B | 2,403,396 B |
tiny |
453,910 B (−22.5 %) | 396,516 B (−2.69 MB, −87 %) |
nano |
338,982 B (−42 %) | 344,548 B (−89 %) |
These figures are the baseline that established the subsystem-gating mechanism.
Two changes since compound on top of them: --gc-sections (dead-code
elimination, always on — ~15 % off full's .text alone) and a
CONFIG_CC_OPTIMIZE_FOR_SIZE (-Os) knob, both on in the lean tiers. With
those plus the microVM axis (no PCI, no ACPI), the microvm tier lands at
≈233 KB .text / ≈676 KB .bss — a networked, ext2-rooted, userspace-
complete kernel smaller than the old nano floor, and nano itself now builds
to ≈164 KB .text.
nano is the leanest bootable x86_64 build — serial console only, no block
storage, no USB, no framebuffer (the 80 KB fb.o alone), no VirtIO/Hyper-V/
audio/trace/self-tests. It still boots to the no-init panic (rendered by the
serial-only stub panic_halt).
The .bss collapse comes from making the fixed static arenas numeric knobs
(kconf's int symbols). At the defaults they cost, per instance/table:
| arena | knob | default | in tiny |
|---|---|---|---|
tmpfs /tmp + /run |
RAMFS_MAX_INODES × …PAGES_PER_FILE |
~556 KB each | 32×32 → 13.8 KB each |
| per-CPU (TSS/GDT/areas) | MAX_CPUS |
1024 → ~250 KB | 8 → ~2 KB |
| PTY pool | PTY_MAX_PAIRS |
16 → ~140 KB | 4 → 35 KB |
| kernel log ring | KLOG_SIZE_KB |
64 KB | 8 KB |
| ext2 block cache | EXT2_CACHE_SLOTS |
16 → 66 KB | 4 → 16 KB |
| PMM boot bitmap | PMM_BOOT_GB |
4 → 128 KB | 2 → 64 KB |
tiny's 396 KB .bss already fits under an RP2350's 520 KB SRAM — and it
still carries x86-only per-CPU/GDT/TSS structures an armv8-M port wouldn't have.
Every tiny build still boots to the no-init smoke panic with the capability
core intact. CONFIG_NET was the proving ground — the most coupled subsystem;
everything since (the 640 KB trace ring, the Hyper-V stack, the HDA driver, the
VirtIO devices) followed the same mechanism with far less friction. The next
targets — xHCI/USB, VMware, and the static ramfs arenas — are the same again.
Not just tiers — deeply customizable¶
The tiers are only defconfigs; each one just sets a collection of
independent CONFIG_* symbols. Anything a tier cuts, you can cut or keep on its
own. Families are gated at per-device granularity, Linux-style: a
transport/core symbol plus one knob per member that depends on it. VirtIO is
the worked example — CONFIG_VIRTIO is the transport, and CONFIG_VIRTIO_BLK,
CONFIG_VIRTIO_GPU, CONFIG_VIRTIO_NET, … each stand alone:
make tiny_defconfig # everything off
echo CONFIG_VIRTIO=y >> .config # add the transport back…
echo CONFIG_VIRTIO_BLK=y >> .config # …and *only* virtio-blk
make oldconfig && make # compiles virtio_blk + virtio_pci + a
# no-op stub for the other 10 devices
So "tiny", "workstation", and "full" are starting points, not a cage — the real
surface is the full Kconfig tree, drivable with make menuconfig or by hand.
For adopters: how a subsystem is severed¶
Gating a subsystem cleanly is a repeatable pattern, proven first on the hardest-coupled one:
- Makefile gate.
auto.conf'sCONFIG_*variables drop the subsystem's source files (and anything that depends on it) from the build. sys_ni_syscall-style stubs. For the syscalls and cross-boundary symbols the core still references, a small stub TU — compiled only when the subsystem is off — returns-ENOSYS/ inert values, so the syscall table and callers link unchanged (the Linuxni_syscallpattern).#ifdef CONFIG_*hooks. A handful of init / IRQ / poll call-sites are guarded directly, so a disabled subsystem is genuinely absent, not merely inert.- Occasional extraction. Where a general facility was co-located with a
subsystem (e.g.
poll/select/epollliving inside the socket file), it is moved to its own always-compiled unit — cleaner code, and the subsystem file then gates without collateral.