Moxnox is an NT-like, POSIX-compatible hobby OS which runs on btrfs and is capable of self-hosting. That means:
- Its design, such as the choice of syscalls, is modelled after that of Windows NT...
- ...yet at the same time, it should be capable of running software written for Unix-like systems, provided that they have been recompiled for it, and comes with a full GNU base system
- It is mature enough that you can build Moxnox on Moxnox
- And it runs on everybody's favourite Linux filesystem
Version 1.0 is around 130,000 lines of C++, and runs on amd64 and aarch64.
The btrfs code works for basic purposes, but has not been battle-hardened. Please don't try to mount anything but throwaway filesystems.
To test Moxnox in qemu, download moxnox-amd64-1.0.img.gz and moxnox-kernel-amd64-1.0.efi. Run gunzip moxnox-amd64-1.0.img.gz to decompress the image.
Then:
qemu-system-x86_64 -M q35 -enable-kvm -cpu host -m 1024 \
-smp cores=4,threads=1 \
-kernel moxnox-kernel-amd64-1.0.efi \
-append "root=/dev/sda" \
-device ich9-ahci,id=ahci -drive id=disk,file=moxnox-amd64-1.0.img,if=none \
-device ide-hd,drive=disk,bus=ahci.0 \
-nic user,model=virtio-net-pci \
-device qemu-xhci \
-device usb-kbd,id=kbd \
-device usb-tablet,id=tablet \
-serial stdio
You will need to increase the memory from 1024 MB if you are trying anything strenuous, like self-hosting.
For ARM, something like the below should work:
qemu-system-aarch64 -M virt -cpu cortex-a76 -m 1024 \
-smp cores=4,threads=1 \
-kernel moxnox-kernel-aarch64-1.0.efi \
-append "root=/dev/sda" \
-device ich9-ahci,id=ahci -drive id=disk,file=moxnox-aarch64-1.0.img,if=none \
-device ide-hd,drive=disk,bus=ahci.0 \
-nic user,model=virtio-net-pci \
-drive if=pflash,unit=0,file=/usr/share/qemu/edk2-aarch64-code.fd,readonly=on \
-device qemu-xhci \
-device usb-kbd,id=kbd \
-device usb-tablet,id=tablet \
-serial stdio
First, use gparted or similar to create a new partition. 32 GB should be
plenty. I'm calling it /dev/nvme0n1p99 below - don't get this wrong.
Make sure you have a recent versions of btrfs-progs, as we don't support some of the default settings used by the older versions.
Prepare the root drive:
# mkfs.btrfs /dev/nvme0n1p99
# mkdir /mnt/moxnox
# mount /dev/nvme0n1p99 /mnt/moxnox
# cd /mnt/moxnox
# wget https://github.com/maharmstone/moxnox/releases/download/v1.0/moxnox-amd64-1.0.tar.gz
# tar -xvzf moxnox-amd64-1.0.tar.gz
# rm moxnox-amd64-1.0.tar.gz
# cd
# umount /mnt/moxnox
Copy the kernel:
# mount /boot/efi
# cd /boot/efi
# wget https://github.com/maharmstone/moxnox/releases/download/v1.0/moxnox-kernel-amd64-1.0.efi
To install the boot entry, do something like:
# efibootmgr -c -d /dev/nvme0n1 -p 1 -L "Moxnox" -l '\moxnox-kernel-amd64-1.0.efi' -u 'root=/dev/nvme0n1p99'
efibootmgr likes to change the boot order afterwards, this probably is not
what you want. The names of block devices should be more or less the same on
Linux and Moxnox.
The "EFI" file is actually a polyglot file that also conforms to the Linux boot protocol, so it should also work on non-EFI GRUB. I have not tested this. How to set this up is left as an exercise for the reader.
For amd64, everything is compiled for x86-64-v3, which covers more or less
anything manufactured since 2015 or so. Moxnox automatically uses and enables
AVX512 if it detects it. For qemu, you need to specify the q35 machine type;
the default pc machine is ancient, and doesn't have PCI Express. microvm
works too.
For aarch64, we're using LSE atomics, which require ARMv8.1. That means the
Raspberry Pi 5, but not any earlier models. Moxnox only supports ACPI, not
Device Tree, so for the Pi you will also need https://github.com/worproject/rpi5-uefi.
Unfortunately there is no raspi5 machine type in qemu, but I have had success
with virt, and sbsa-ref works too.
Device support:
- Block devices: NVMe, SATA, SDHCI (i.e. SD cards), virtio-block, USB mass storage
- Network cards: virtio-net, RTL8168, RTL8125
- Input devices: USB HID, PS/2 keyboard, PS/2 mouse, virtio-input
- Graphics: virtio-gpu, GOP framebuffer, VBE, VGA text console (pass
text=80x25on non-UEFI boots) - Serial ports: virtio-console, x86 serial port, PL011 ARM serial port
- USB: XHCI
The easiest way to build is to pull the Docker containers maharmstone/moxnox-ci-amd64
or maharmstone/moxnox-ci-aarch64, which contain the cross-compilers for
x86_64-pc-moxnox or aarch64-pc-moxnox.
Then in the container:
$ git clone https://github.com/maharmstone/moxnox
$ cd moxnox
CMake will compile whichever of the submodules are checked out. For a minimal system:
$ git submodule update --init --depth 1 ports/acpica/repo ports/freetype/repo ports/gnu-efi/repo ports/harfbuzz/repo ports/libpng/repo ports/mingw-w64/repo ports/zlib/repo ports/musl/repo ports/tre/repo
For all of the ports, which might take a while:
$ git submodule update --init --recursive
Then:
$ mkdir build
$ cd build
$ cmake -DCMAKE_TOOLCHAIN_FILE=amd64.cmake -GNinja ..
or
$ cmake -DCMAKE_TOOLCHAIN_FILE=clang-aarch64-native.cmake -GNinja ..
Make sure you specify Ninja - we need this as we're using C++ modules. CMake defaults to Unix Makefiles, which don't work.
Finally:
$ ninja
gcc, llvm, and rust are not compiled unless explicitly requested due to
their size. hexyl and ripgrep also need to be specified explicitly due to
their dependency on rust.
To build on Moxnox, as above, but you don't need a toolchain file: cmake -GNinja ...
gcc, rust, hexyl, and ripgrep can't yet be compiled in Moxnox, but
everything else works (eventually). You may need a lot of RAM and even more
patience. If CMake complains about rust-libc, make sure that you don't have
the Rust submodules checked out.
The kernel and first-party programs are licensed under the GPL. The first-party
DLLs are licensed under the LGPL. The contents of the modules directory are
MIT.
See the ports submodules for details of their particular licences.
Current ports:
- bash
- binutils
- bison
- bzip2
- cairo
- cmake
- coreutils
- curl
- diffutils
- expat
- file
- findutils
- flex
- freetype
- gawk
- gcc (amd64 only)
- git
- gmp
- gperf
- grep
- gzip
- harfbuzz
- hexyl
- jsoncpp
- less
- libarchive
- libiconv
- libpng
- librhash
- libuv
- llvm
- m4
- make
- mako
- markupsafe
- mesa
- mpc
- mpfr
- nano
- ncurses
- netcat
- ninja
- openssh
- openssl
- patch
- perl
- pixman
- python
- pyyaml
- readline
- ripgrep
- rust
- sed
- tar
- vim
- wget
- xz
- zlib
- zstd
Loads - but this is version 1.0. In particular:
- A GUI
- Support for different timezones
- Support for different keyboard layouts
- Locales and non-UTF-8 character sets
- Any sort of security
- Multiple users and groups
For performance issues and similar:
- The userspace heap allocator could be a lot quicker (this is the bottleneck when self-hosting)
- TCP is slow, because we don't negotiate an MSS and keep to the default 64KB window
- We ought to be doing early writeback on memory pressure
- There should be an LRU system to drop clean pages from the page cache...
- ...made worse by the fact that the dentry cache never gets pruned, so inodes stay open until deleted
And obviously there should be more ports and hardware support.
The contract is that the syscalls and low-level interfaces match those of NT, but not the daemons or how the kernel itself is structured. This is in contrast to what ReactOS aims to do, which is an exact recreation of Windows.
So:
- Binaries are PE format rather than ELF, and have a
.exeor.dllextension - C++ exceptions are done through SEH
- POSIX signals are done through Asynchronous Procedure Calls
- All the non-kernel kernel-like stuff gets done in
ntdll.dll - The rcx, rdx, r8, r9 calling convention matches that of Windows
- And its C eccentricities match those of Windows:
sizeof(long) == sizeof(int32_t),sizeof(wchar_t) == sizeof(char16_t),sizeof(long double) == sizeof(double).
There's an awful lot of stuff in NT, but the interesting stuff that's missing is tokens, the Registry, and I/O completion ports.
NT has a complex system where there's a virtual filesystem but it's hidden
(unless you use winobj), with the true path of a file hanging off the name of
its block device (so C:\Windows\System32 might actually be \Device\HarddiskVolume1\Windows\System32).
I've not replicated any of that. Paths are POSIX-like: slashes rather than
backslashes, / as the root filesystem, and mounted directories. Unlike real
Windows, NtCreateFile resolves . and .., to work better with the dentry cache.
Other things I've deliberately not copied are the various forms of transactions, which were a product of their time, and the idea of paged kernel memory, which causes more problems than it solves.
We have implemented 642 of the ~1,200 functions in POSIX 2024. This is the vast
majority of what's used in practice, with gnulib papering over some of the
cracks. Unlike Windows, fork is there. I've used musl for its mathematical
and cryptographic functions, and mingw for printf and some other things,
and tre for its regexes.
The POSIX functions are split over ucrtbase, iphlpapi, and ws2_32, as on
Windows, with anything that Windows doesn't implement put in mnposix.
I've borrowed liberally from Linux's best ideas: the dentry cache, large folios, mapping the whole of physical memory, evdev.
Other POSIX shortcomings:
- AF_UNIX is half-complete.
socketpairworks, including for SCM_RIGHTS (sshuses this), but the normal client-server code doesn't work mkfifocreates an inode, but I/O to it does not get redirected to a buffer as it ought to do- APCs (i.e. signals) only get delivered when a thread returns from a syscall.
So a program doing
while (true) { }is currently unkillable.
There's nothing that automatically starts daemons yet, so manually.
First generate the host key:
# ssh-keygen -A
There's no passwords yet, so you will need to add a /root/.ssh/authorized_keys
file so that you can connect. If you have trouble, double-check that /root is
owned by root, as sshd will ignore this file otherwise.
Then start sshd using its full path:
# /bin/sshd.exe
No. Or at least, not yet.
We deliberately do not implement any Win32 DLLs, bar a few functions in
kernelbase that the mingw runtime wants. A future improvement would be to
strip these out entirely, then port across Wine's versions of the Win32 DLLs.
Some parts of Wine, like wineserver and ntdll, we would not need as we are
already taking care of it ourselves.
None of the substantive code was written by AI.
I used AI to help with the mechanical parts of porting (e.g. translating
autotools to CMake, replacing ioctl calls with tcgetwinsize, translating
the C structs into Rust), as well as to write some of the smoke tests (e.g.
cairo-test, mesa-test).
Where Claude really shines though is in reviewing your own commits. You give it a hash, and it tells you about the silly memory leak or deadlock that you left in, before you can make a fool of yourself.
For funsies. Plus the only way of really learning how something works is to have a go at writing it yourself.
Also, the NT kernel is criminally underrated, and pretty much unknown to
hobbyists. It had features that Linux took years and years to come up with its
own design for, such as eventfd and pidfd.
It's a shame that Windows keeps shying away from full POSIX compatibility. They had a half-complete version in the early days, then more recently had WSL1, only to then abandon it. And Nano Server was another cool idea, only again to be itself abandoned. So this is my take on what I wish Windows had been like.
