Research distro

Moonwater, the rock to build upon

A research Linux distribution: its own shell and coreutils, software from five other distributions, one script that decides what each machine does, and one command to control the system.

Open source, Apache-2.0 · In development since June 2025

Commands
209, in one program
Software
Five distributions
Behaviour
One machine script
Control
One CLI
Runs on
x86_64, arm64, riscv64
Size goal
Under 15 MB
// 01 · Speed

Its own shell and coreutils, faster than GNU's.

Moonwater writes its own shell, coreutils and util-linux, along with tools such as awk, sed, tar and zstd: 209 commands in one program. Underneath is its own C library, with 353 routines written by hand in assembly.

grep -r -n struct over the Linux source tree
Moonwater122 ms
ripgrep177 ms
GNU grep1,377 ms
chmod -R over the Linux source tree
Moonwater16 ms
GNU118 ms
wc -L over 30 MB of words
Moonwater7M cycles
GNU204M cycles
paste -s -d, over the benchmark records
Moonwater0.37M cycles
GNU18.2M cycles

Lower is faster. Moonwater's own benchmark runs, recorded in its commit history, September 2026. Moonwater's grep and chmod ran on 16 threads; GNU grep and chmod are single-threaded, and on one core Moonwater's chmod takes 116 ms. The wc -L figure is on a CPU with AVX-512 VBMI2. The shell checks make's configure script (sh -n) in 24.5M cycles, against bash's 31M and dash's 19M.

Each program with a standard counterpart is tested against it: both run on the same inputs, and status, output, effects on disk and diagnostics are compared. The shell's suite has 28,142 cases, each run under bash, bash --posix or dash; its two latest runs matched on 25,707 and 25,733.

// 02 · Bowl

Software from five distributions, on one system.

Bowl runs the package managers of Alpine, Arch, Debian, Fedora and Nix directly on Moonwater, with no VM. What they install runs as an ordinary program on the same kernel, next to Moonwater's own shell and coreutils.

  • Alpineapk3.7 MB
  • Archpacman126 MB
  • Debianapt49 MB
  • Fedoradnf70 MB
  • Nixnix27 MB
MoonwaterOne kernel, with its own shell, coreutils and util-linux

Each bowl lives under /bowls. Sizes are the x86_64 downloads.

$ bowl setup fedora
[Bowl] downloading https://dl.fedoraproject.org/…
[Bowl] landing Fedora at /bowls/fedora
[Bowl] /bowls/bin/dnf -> /bowls/fedora/usr/bin/dnf
[Bowl] Fedora is ready. dnf makecache

$ dnf -y install hello
…
$ hello
[Bowl] /bowls/bin/hello -> /bowls/fedora/usr/bin/hello
Hello, world!

bowl setup checks there is room, downloads the distribution, unpacks it and puts its package manager on the path. After that a program's name is enough: the shell finds it in its bowl and adds a launcher for it to /bowls/bin.

Fast, for the programs you run Only the loader and libc

Bowl binds in the loader and C library the program needs and starts it. Moonwater's own /bin stays, and no process sits in between.

Isolated, for the package managers The whole distribution

pacman, apt, apk, dnf and nix run inside their distribution, with their own process list, hostname and IPC. The host's /sys and /proc/sys are read-only to them. This is chosen by the program's name, not by a flag.

Bowl does no instruction emulation and no syscall proxying. It is not a security sandbox: a bowl's programs run as root. On a live stick bowls live in memory; moonwater install puts them on a data partition, and moonwater wipe keeps them.

// 03 · Machine script

One file decides how the machine behaves.

Every button, lid, cable and boot is an event, and main.moonwater.sh answers it. Watch one laptop's file at work.

Machine events
  • poweroff
  • reset
  • lid_close
  • lid_open
  • dock
  • headphone
  • tablet
  • mute
  • micmute
  • volume_up
  • brightness_up
  • sleep
  • resume
  • rfkill
  • canvas
/root/main.moonwater.sh
function moonwater_init {  moonwater link join office} function moonwater_dock {  if [ "$1" = on ]; then    moonwater priority internet wired  else    moonwater priority internet wifi  fi} function moonwater_lid_close {  moonwater canvas off} function moonwater_lid_open {  moonwater canvas on} function moonwater_poweroff {  poweroff}
moonwater_init
once, after boot
moonwater_<event>
that event
moonwater_event
every event
moonwater_end
as the machine stops
Runs, as root

Any shell command, moonwater commands, and programs installed with Bowl.

Same file, other machines
function moonwater_init {
  case $1 in
  live|disk*)
    moonwater wipe
    chromium --kiosk \
      --user-data-dir=/tmp/kiosk "$URL" &
    ;;
  esac
}
Wipe at every boot, then one program
First/root/main.moonwater.shYour script, if it exists and passes the checks
Otherwisemain.moonwater.shCompiled into the kernel module at build time: the system's fallback

The built-in script lives in the module, so a machine whose disks never appear still has a working power button. Yours is used only if it is a regular file owned by root, writable by no one else, not a symlink and at most 64 KiB. A key the script does not bind still types.

// 04 · CLI

Moonwater CLI: One CLI controls the entire system.

moonwater manages where the system lives, what runs at boot and what the machine's events do.

Systeminstalluselivewipe
Events and bootbindbind initbind exit
Desktopcanvaskeyboard
Networkwifibluetoothpriority internet
Timetimetimezonentp
Remotelinklink shelllink run
$ moonwater keyboard se
[Moonwater] keyboard se

$ moonwater bind mute
[Moonwater] mute: /root/main.moonwater.sh:8

Settings live in two places: the boot image, and /root on the data partition. When the machine script handles an event, moonwater bind points at its line.

// 05 · Defaults

Sane defaults.

A new machine sets its time zone, starts NTP and brings up its network by itself. Time zone, clock, keyboard and network preference are each one moonwater command, saved in /root.

Time zoneAutomatic

Asked of Cloudflare over HTTPS when a network appears. moonwater timezone ZONE makes it manual.

DNS1.1.1.1 first

Then the resolver the network hands out, the only one that knows local names.

ClockNTP from boot

pool.ntp.org, then Google's and Cloudflare's servers, until the kernel reports the clock synchronised.

NetworkDHCP on the best link

A link with carrier beats one without. When cable and Wi-Fi are both up, the cable wins.

DisplayLargest mode

Canvas starts at boot and drives each screen at the largest mode it lists.

Keyboardus

Not detected. Set it once with moonwater keyboard se.

$ moonwater timezone
[Moonwater] timezone Europe/Stockholm, Central European Time (auto, from Cloudflare)
  local 2026-09-26 14:02:11 CEST
$ moonwater ntp
[Moonwater] ntp on, pool.ntp.org, sampling on, synchronised
$ moonwater priority internet
[Moonwater] internet prefers wired
$ moonwater keyboard
[Moonwater] keyboard us

None of it was set by hand. Each setting is one moonwater command to read it back, and the same command with a value to change it.

// 06 · Link

A terminal or a command on another machine, by key. There are no users and no passwords, and a paired machine gets a shell or runs a command only once you allow it.

# on each machine: its key
$ moonwater link key

# give each the other's key and address
$ moonwater link pair server KEY 10.0.0.2

# on the server: let the laptop in, grant by grant
$ moonwater link allow laptop shell run

# then, from the laptop
$ moonwater link shell server
$ moonwater link run server uname -a
  • Grantsshell, run, files and log, each allowed per machine
  • Keystrokeseach sent in its own datagram, at once
  • Groupsmembers on the local network find each other over mDNS and pair themselves
  • Either endMoonwater, or Linux running the Moonwater shell
  • Needsa direct address; there is no NAT traversal
// 07 · Measured

A clock that learns its own crystal.

Every computer's clock runs a little fast or slow, and syncing it once an hour doesn't fix that. Moonwater asks three time servers, five exchanges each, and believes the one with the tightest error bound. Then it works out how fast its own crystal runs and corrects the rate, so the clock stays right between syncs, not just at them.

Five request and reply exchanges between Moonwater and an NTP server. One is delayed by a queue; the one with the shortest round trip is kept.Moonwater Server Round tripqueuedkept
Five exchanges with one server: the one with the shortest round trip is kept, so a reply stuck in a queue can't drag the answer off
Three servers each give an offset, give or take its root distance. The answers of A and B overlap; C agrees with neither and is set aside. Of the two that agree, B has the least root distance and is kept.Offset, give or take its root distanceagree Server AServer Bkept: least distance Server Cset aside
Three servers, each give or take its error bound: the tightest one that agrees with the others wins
Distance from true time, ordinary clock
chrony0.66 ms
Moonwater0.93 ms
systemd-timesyncd3.05 ms
Distance from true time, crystal running 100 ppm fast
Moonwater0.86 ms
systemd-timesyncd28.6 ms
Worst answer over 80 measured syncs
Three servers, tightest bound0.28 ms
First server that answers16.3 ms

Measured on 26 September 2026 from one machine in Sweden. Each system ran in its default configuration in identical KVM guests on the same host and network: chrony and systemd-timesyncd as Debian 13 ships them. True time is the median of Netnod's stratum-1 servers, read from the same guest once a minute. The first two charts are medians over the second half of an hour-long run. The crystal error was added by changing the kernel's tick. chrony resets that tick itself, so it has no result for that chart. The worst-answer chart compares Moonwater's server choice with trusting the first server that answers, as a basic client does.

moonwater ntp sampling on|off, on by default. The first answer after boot sets the clock outright, so it's right from the start. After that, small corrections are eased in and never jump time backwards, and the crystal's rate is learned at every sync: one 100 ppm fast was learned to within 1 ppm in 13 minutes. Syncs start 4 minutes apart and stretch to 30 as the rate settles. Answers are checked field by field, and a server that says slow down, or whose own error exceeds a second, is dropped. A server named in /root/ntp.server is used on its own.

// 08 · Parts

The parts

Base

The base is built into the kernel

The shell, coreutils and util-linux ride in the kernel image, and the desktop is in the Moonwater module, so the base keeps working whatever else is installed.

Wipe

Start over, keep the setup

moonwater wipe empties /home and /root but keeps the settings, the machine script and /bowls.

Permissions

Floodlight, as text

What each program may run, spawn and reach on the network, in one place: cat to read, echo to change, then seal.

Build

One C program builds it

The build tool runs on Moonwater's own stack. On Linux, compiling it needs only a C compiler and an assembler.

Desktop

A desktop in the kernel

Canvas draws with the CPU, on any display the kernel can drive.

Time zones

420 zones, no zoneinfo directory

Each tzdata zone maps to the POSIX rule in its own file's footer.

Foundations

353 routines in assembly

In its own C library; 351 of them for all of x86_64, arm64 and riscv64.

Programs

Spark

No relocations, no dynamic linking, no section table.

// 09 · Kconfig

Built to be configured.

Moonwater has its own Kconfig. Turn off a part a machine does not need and it is gone from the build: disabled commands are dropped from the binary, not just from the path.

# no desktop; the kernel console stays
CONFIG_MOONWATER_CANVAS=n

# the shell, without the utilities
CONFIG_MOONWATER_UTILITIES=n

# a kernel only, with no bundled userspace
CONFIG_MOONWATER_SHELL=n
CONFIG_MOONWATER_UTILITIES=n

# build with your own machine script inside
CONFIG_MOONWATER_MACHINE_SCRIPT="kiosk.sh"
// 10 · Try it

Try it

No download yet: three commands build it and boot it in a virtual machine.

# get the source
$ git clone https://github.com/dawnlarsson/moonwater
$ cd moonwater
# build it and boot it in a virtual machine
$ sh build.sh --run
$ 
Virtual machine
sh build.sh --run

Builds it, then boots it in a virtual machine on this computer.

Another architecture
sh build.sh --arch arm64 --run

The same for arm64, or riscv64.

USB stick
sh build.sh --usb

Builds it, lists the drives and prints the commands that write the stick.

It builds on Linux, or anywhere else with --host naming a Linux machine to build on. Real hardware boots it through UEFI, with Secure Boot off.