A small Linux system and web panel for the Netgear Stora MS2110, running entirely from the device's 256 MB NAND flash so that both drive bays stay free for data and the disks can spin down.
The stock firmware is long unsupported, and OpenMediaVault — the usual replacement — does not fit in flash. Installing it on a hard disk means the system disk never sleeps and several gigabytes of every disk go to system partitions. StoraOS exists to avoid both.
| Component | Choice |
|---|---|
| Base system | Debian 13 (trixie) armel, --variant=minbase |
| Kernel | Linux 6.12 LTS, built for this board, 4.4 MB uImage |
| Root filesystem | UBIFS on UBI in NAND, zstd compressed |
| File sharing | SMB (ksmbd, in-kernel), NFSv3, DLNA (minidlna) |
| Admin panel | nasd — a single static Go binary, no dependencies |
The panel handles disks (partitioning, formatting, mounting, hot-swap, RAID1), shares and users, SMART, fan control, front-panel LEDs with a night schedule, package updates and live charts. It is one process: no database, no nginx, no container, nothing to keep in sync with it.
It also installs Debian packages without apt, and never wakes a sleeping disk to tell you how the disks are doing. Both are explained under design notes below — they are the two places where 128 MB of RAM and a spun-down drive stopped being constraints and started being the design.
Hardware. Marvell Kirkwood 88F6281 (ARMv5TE, single core at 1 GHz, no FPU), 128 MB RAM, 256 MB NAND, two SATA bays, gigabit Ethernet. These numbers explain most of the design decisions in this repository.

Everything the box is doing, on one screen. Both disks asleep, the fan at
its floor, 59 MB of RAM in use out of 117.

A sleeping disk reports no temperature, because reading it would mean
waking it. That is the honest answer rather than a blank.

A day of history in a ring buffer in RAM. Writing it to disk would wake a
disk every fifteen seconds and defeat the point of the machine.
Also: shares and users.
Taken against a running panel in English — the interface follows the browser's language. Serial numbers, addresses, share names and account names are placeholders: these are documentation images, not a record of somebody's machine. The script that produces them is kept outside this repository, because to hide those values it has to be given them.
| Offset | Size | Contents |
|---|---|---|
0x000000–0x0c0000 |
768 kB | u-boot — read-only, never written |
0x0c0000–0x100000 |
256 kB | u-boot-env |
0x100000–0x700000 |
6 MB | kernel — raw uImage with an appended DTB |
0x700000–0x10000000 |
249 MB | ubi → UBI volume rootfs (UBIFS) |
U-Boot reads the kernel with a plain nand read and never needs to understand
UBI; the kernel mounts UBIFS itself. This is what makes the installation safe:
the bootloader is never reflashed, so a broken image in NAND falls back to
whatever the machine booted from before.
The full procedure is in INSTALL.md — it takes an hour or two and every step up to writing flash is reversible.
The short version: build the kernel and the panel on a PC, build the root filesystem on the device (it is already armel, so there is no qemu anywhere in this repository), write both into NAND from the old system, and add a boot entry that falls back to the old one. You need a machine already running on the Stora — stock firmware, Debian or OpenMediaVault — and a serial console is strongly recommended for the first boot.
One hard prerequisite: the device must already be running a custom U-Boot
(Bodhi's 2017.07-tld-1, from the doozan forum). The procedure needs
nand read, bootm and an environment that fw_setenv can write, and the
bootloader Netgear ships will not do it. Installing U-Boot is the only step
that touches the bootloader; it is out of scope here and has to be done first.
Nothing in the procedure writes the bootloader, so the device cannot be bricked by it.
Three protocols, because no single one reaches every desktop:
| Where the machine shows up | Protocol | Answered by | Icon |
|---|---|---|---|
| Windows, under Media devices | UPnP/SSDP | the panel itself | its own PNG |
| Windows, under Computers | WS-Discovery | wsdd2 |
a Windows category icon |
| macOS Finder, Linux desktops | mDNS | avahi-daemon |
from Apple's fixed set |
Only UPnP carries an icon the device chooses for itself; the other two let you set a name and pick from a list. That is why the panel answers SSDP in-process — a few UDP datagrams and a document its own HTTP server already serves, which is cheaper than another daemon on a machine with 128 MB of RAM — while the other two mechanisms are packages.
The same machine therefore appears more than once, and the names are chosen so
that this reads as one device rather than three. They all come from
internal/netid, which exists because they once drifted apart:
StoraOS (stora) UPnP — the panel, clicking it opens the interface
StoraOS media (stora) DLNA — the media server
stora WS-Discovery — the shares
The WS-Discovery entry keeps the bare host name on purpose: clicking that one
opens \hostname\, which only works if the name resolves.
The icon in Explorer's network view stays generic, and no amount of work on this
side will change that. Windows does not read the iconList from the description
document there — it looks the icon up in its own registry, under
HKEY_CLASSES_ROOT\NetworkExplorerPlugins\<device URN>\DefaultIcon, and the
value points at an icon resource on the Windows machine. A device cannot supply
its own picture; vendors who show a logo there ship a metadata package through
Microsoft. The iconList is still served, because control points other than
Explorer do use it.
wsdd2 ships bound to smbd, which does not exist here — this machine serves
SMB with ksmbd — so rootfs/files/etc/systemd/system/wsdd2.service replaces the
packaged unit. A drop-in is not enough: an empty BindsTo= does not clear the
list the packaged unit set.
All of this was removed for a day in August 2026, when browsing a share from Windows crashed the box six times in one evening. The cause was a use-after-free in ksmbd, not in any of this — discovery only made it happen, because Explorer walks into every share it can see. The kernel is patched (the write-up), so the announcements are back.
Nothing writes to flash in normal operation. /tmp and /var/log are
tmpfs, the journal is volatile, swap is zram. Flash wears out; the panel is not
allowed to be the reason it does.
Nothing wakes a sleeping disk. Statistics come from kernel counters in
/proc and /sys, SMART is only read on request, and the metrics history for
the charts lives in a RAM ring buffer that is lost on reboot. That is a
deliberate trade: a disk that never sleeps was the reason for replacing the
previous system.
ksmbd needs two settings that are not the defaults. Durable handles are
disabled: with them, copying a large file and then re-authenticating sends a
kernel thread into an endless loop in ksmbd_smb_check_shared_mode, eating a
full core, and the thread cannot be killed — only a reboot clears it. And the
panel never restarts ksmbd, only reloads it: restarting the service while a
client is connected wedges the kernel module.
NFS is version 3 only. UBIFS has no export_operations, so it cannot be
exported over NFS at all. The NFSv4 pseudo-root starts at /, which is UBIFS
here, so mountd refuses to build it and every v4 path fails. Linux clients must
mount with -o vers=3; macOS and Windows are unaffected.
Installing packages does not go through apt. apt needs about 60 MB of
anonymous memory to answer any question at all, because it indexes all 155 000
packages in the archive before looking at the one you asked for — even
apt-cache policy bash costs 47 MB. On a machine with 117 MB of RAM that is
most of it, and it is what took the box down mid-install: memory pressure
starved systemd until the hardware watchdog fired. internal/apt does the same
job by streaming the index instead of indexing it — measured at 12 MB to
refresh, 15 MB to list what is upgradable and 17 MB for a complete install
including dpkg. apt's own lists are not kept between runs — the panel rebuilds
them when it needs the fallback and removes them again if it was the one that
downloaded them. It reimplements nothing: dpkg unpacks and orders, dpkg --compare-versions decides which version is newer, sqv checks the archive
signature, xz decompresses the indexes that are only published that way. What
it will not do is decide anything — conflicts, removals, a package appearing out
of nowhere during an upgrade all make it stop and say to use apt. Run it with
nasd apt list|install|upgrade.
The default free-page reserve is too small here, and it costs throughput.
vm.min_free_kbytes is derived from the amount of RAM, which on 128 MB gives
1386 kB — low enough that a plain file copy runs the machine down to the
watermark and reclaim starts competing with the write. Raising it to 8192
measured 38.0 → 41.7 MB/s writing, 48.3 → 56.7 MB/s reading, and 35.8 → 40.0
MB/s with two writers and a reader at once. 16384, which the doozan forum
recommends for boxes this size, adds 0.4 MB/s and costs another 25 MB of
available memory — which is the memory a distribution upgrade needs, so the
gain stops being worth it there.
The drives' own idle timer does not work here, so the panel keeps the clock.
hdparm -S is accepted, the drive advertises the power management feature set,
and the timer never fires — measured with the delay cut to two minutes, no I/O
at all and every service stopped, the disks were still spinning after six
minutes. An explicit standby command works and holds: a parked disk stayed
parked for six minutes with everything running. So internal/spindown counts
idle time from /proc/diskstats — asking a disk how busy it is would be
self-defeating, since every question is a command — and sends hdparm -y when
the configured delay runs out. This is what hd-idle exists for elsewhere; at a
few dozen lines it is cheaper here than a fourth daemon.
A hardware watchdog is armed at 20 seconds. Kirkwood's timer cannot go beyond 21. It does not catch a spinning kernel thread — PID 1 stays alive and keeps feeding it — but it does catch the total freeze where both the network and the serial console go silent.
kernel/ kernel build script, config, device tree and local patches
rootfs/ debootstrap and system configuration (run on the device)
scripts/ diagnostics, NAND backup, flashing, u-boot environment
panel/ nasd — the admin panel (Go, no external dependencies)
tools/ serial console bridge, SMB test clients, interface checks
docs/ write-ups of the failures that shaped the design
assets/ logo
In daily use. Verified on the hardware: boot from NAND, both disks, i2c sensors, LEDs with the night schedule, formatting and mounting from the panel, SMART, the nightly backup, NFSv3 from macOS, DLNA, and 22 GB copied from Windows over SMB in two overlapping streams with nothing in the kernel log. A file written over SMB is visible over NFS and the other way round.
Two things are worth knowing about how this repository is kept. The kernel carries a local fix for a use-after-free in ksmbd that crashed this machine six times in one evening — found here, patched here, and not reported upstream, which was a deliberate decision. And the work of 12 August 2026 was audited by a different model than the one that wrote it; seventeen findings, three of them serious, all fixed and each with a test that fails against the old code: docs/audit-2026-08-12.md.
Not verified: live SATA hot-swap, RAID1 under load, and how the panel's loading indicator actually looks in a browser — there is none on the build machine.
BSD 2-Clause — see LICENSE. It covers every file that does not say
otherwise in its own header. Two do, and both were inherited rather than chosen:
kernel/files/kirkwood-stora.dts comes from OpenWrt and stays GPL-2.0-or-later,
and the patches in kernel/patches/ are GPL-2.0 because the kernel they patch
is. Having them here does not put the rest under the GPL — the panel is a
separate program that execs dpkg and hdparm and links nothing but the Go
standard library.
NETGEAR is a trademark of NETGEAR, Inc. Stora was the name of the hardware; the US registration for it (serial 77823061) is dead, cancelled for a missing declaration of continued use.
This project is not affiliated with, endorsed by, or connected to NETGEAR, Inc. The hardware names are here to say which machine the software is for, and for no other reason. Nothing in this repository is a NETGEAR product.
