A reproducible, drop-in linux.img built from a modern Buildroot, with all kernel
patches carried in-tree as Buildroot patch files.
Status: v3 (2026-07-12) — amended by the Phase 0 recon findings (docs/phase0-review.md).
v2's claims were verified against the shipped stock release; Phase 0 then verified them
against the source, and a number did not survive. Every correction is marked
[P0] inline and carries its evidence. Task-level execution plan in TASKS.md.
Target board: Terasic DE10-Nano (Cyclone V SoC, armv7-a Cortex-A9)
Phase 0 headline: the central bet holds. All 12 SONAMEs the stock
MiSTerbinary needs survive at the same major version in Buildroot 2026.08 (docs/package-manifest.md), so §1's "nothing needs rebuilding" premise is now confirmed rather than assumed.All five Phase 0 open questions were decided on 2026-07-12 — see
docs/decisions/(ADRs 0010–0014) and the summary table indocs/phase0-review.md. The headline decision: drop the out-of-tree exfat driver (ADR 0010). That is not a no-op — it makes the P1.10 vfat fallback a fail-to-boot requirement rather than a nicety. The sustainability gate (ADR 0014) is deferred, not waived: it now blocks P4.10 (publication), not Phase 1. Until it is signed, this is a personal-use project.
MiSTer's operating system is currently distributed as an opaque 94 MB archive
(release_YYYYMMDD.7z) containing a 375 MiB ext4 image built from Buildroot 2021.02.4
with glibc 2.31, running Linux 5.15.1 — a kernel forked in November 2021 that has
never merged a single 5.15.y stable release. The Buildroot configuration that produces
it is not published anywhere.
This plan replaces that image with one built from Buildroot 2026.08 and a mainline 6.18 LTS kernel, in a public repository, with CI, with release artifacts published as GitHub Release assets rather than committed blobs.
**Hard deadline: Linux 5.15 reaches end-of-life in December 2026.** MiSTer's kernel base stops receiving *any* upstream security fixes in roughly five months. This is not a hygiene argument; it is a date on a calendar.
The critical enabling insight: the stock MiSTer binary is a normal dynamically-linked
armhf glibc ELF. glibc is backward-compatible. A rootfs with a newer glibc runs binaries
built against an older one. Therefore no MiSTer binary, no core helper, and nothing in
Distribution_MiSTer needs to be rebuilt. We change the OS underneath and everything above
it keeps working.
The critical enabling mechanism: Downloader_MiSTer already applies OS updates from a
linux entry in any configured database. We ship our image through the project's own
hash-verified update channel. No permission, no fork of the cores, no fork of Main_MiSTer.
| # | Goal |
|---|---|
| G1 | A linux.img + zImage_dtb that boots the unmodified, stock MiSTer binary |
| G2 | Modern kernel on a supported LTS with a real security-update path |
| G3 | Modern package set (Buildroot 2026.08) with a real security-update path |
| G4 | No separate kernel repo. All kernel patches live as .patch files in the Buildroot external tree and are applied to a pristine kernel.org tarball |
| G5 | Fully reproducible: pinned Buildroot, pinned kernel + hash, checked-in .config, published SBOM |
| G6 | Release artifacts published as GitHub Release assets. No binaries in git. Ever. |
| G7 | Opt-in distribution via a community db.json — zero cooperation required |
- Not forking cores,
Main_MiSTer,Menu_MiSTer, orDistribution_MiSTer. - Not switching to musl/Alpine. This would break every prebuilt binary in the ecosystem.
- Not switching to a mutable package manager (apt/apk). The image-based update model is correct and should be preserved; the problem is the unpublished config, not the paradigm.
- Not replacing U-Boot in v1 — and, as of 2026-07-13, not porting it to mainline at
all: Phase 5 builds the existing
u-boot_MiSTerfork from source instead (§8, ADR 0017).
Captured by inspecting release_20250402.7z. Anything below is load-bearing.
-
arm,cortex-a9, NEON, VFPv3, EABIhf -
glibc — [P0: the floor is 2.28, not 2.31.] The binary's highest versioned requirement is
fcntl64@GLIBC_2.28(scripts/abi/needed-symbols.py). Newer is fine and is the point. The real hazard the plan originally missed: glibc 2.34 merged libpthread and librt into libc, yet the stock binary stillDT_NEEDEDslibpthread.so.0andlibrt.so.1. Buildroot still installs the compat stubs (glibc.mk:188), so this resolves — but the five symbols that must come through the merge (pthread_create,pthread_join,pthread_attr_setaffinity_np,shm_open,shm_unlink) are named indocs/abi-contract.md, and P2.2 asserts them against a real built rootfs rather than trusting the reasoning. -
Stock
MiSTerbinaryNEEDED:libc.so.6,libstdc++.so.6,libm.so.6,librt.so.1,libpthread.so.0,libgcc_s.so.1,libfreetype.so.6,libbz2.so.1.0,libpng16.so.16,libz.so.1,libImlib2.so.1,libbluetooth.so.3→ Every one of these SONAMEs must be present with the same major version. Modern Buildroot provides all of them.
bootargs = console=ttyS0,115200 $v loop.max_part=8 mem=511M memmap=513M$511M \\
root=$mmcroot loop=linux/linux.img ro rootwait
mem=511Mis load-bearing: the top of DDR is reserved for the FPGA fabric (core framebuffers, scaler, emulated RAM) and for the warm-reboot mailbox at0x1FFFF000(= 511.996 MiB, i.e. above the 511 MiB the kernel is told about). Do not change. [P0:memmap=513M$511Mis inert on ARM.early_param("memmap")is defined only inarch/x86,arch/mipsandarch/xtensa— there is no parser underarch/arm/.mem=511Malone does 100% of the reservation. Both args still must not be touched (they are U-Boot's, and we do not modify U-Boot in v1), but the reservation is attributable tomem=, not tomemmap=.]- Root filesystem is mounted read-only. This is a good design — an immutable root with
writable state on
/media/fatand tmpfs. Preserve it. linux.imgis a file on the FAT partition, loop-mounted as root.- The kernel is booted as a concatenated
zImage_dtb(zImage + appended DTB). Verified from the extracted U-Boot environment (seedocs/verification/stock-release-20250402.md): U-Boot loads the whole file, computes the DTB address from the zImage header's declared-size field (setexpr.l fdt_addr $loadaddr + 0x2C; setexpr.l fdt_addr *$fdt_addr + $loadaddr), injectsbootargsinto the DTB's/chosennode via standard FDT fixup, and runsbootz $loadaddr - $fdt_addr— no initrd, ever. Consequences: plaincat zImage dtbis the correct assembly (the DTB must sit exactly at the declared zImage end, whichcatguarantees);CONFIG_ARM_APPENDED_DTBis not needed (stock does not set it); and anything added at boot time — the initramfs of §5 — must be embedded in the zImage. - The data partition may be FAT32 or exFAT, and existing
u-boot.txtsetups may pointroot=at other devices (USB boot) —u-boot.txtis applied withenv import -t, so it can override any U-Boot variable (defaultmmcroot=/dev/mmcblk0p1), not just$v. Both filesystems, plus NLS codepages, must be built-in (stock:CONFIG_VFAT_FS=y,CONFIG_EXFAT_FS=y). - U-Boot also pre-loads the FPGA (
core=menu.rbf) before Linux, and supports a warm-reboot handshake with Main_MiSTer through reserved RAM at0x1FFFF000(env import -tfrom RAM) — one more reasonmem=511M/memmap=513M$511Mare untouchable.
Full detail with evidence in docs/abi-contract.md (P0.5).
-
MiSTer_fb— [P0: there is no custom ioctl.] The driver exposes exactly one ioctl,FBIO_WAITFORVSYNC, which is standard mainline UAPI, byte-identical in 5.15 and 6.18. This plan previously treated the "MiSTer_fb ioctl ABI" as a bit-level contract to preserve; there is no custom number to preserve, and P1.4 carries no ioctl-drift risk. The actual custom surface is the sysfs parameter/sys/module/MiSTer_fb/parameters/mode./dev/fb0is never mmap'd. -
Audio — [P0: the ABI is not a card name.] Main_MiSTer contains zero ALSA code. Audio reaches hardware via
/etc/asound.conf, which routes the default PCM throughtype file → /dev/MrAudio(created byMiSTer-audio-spi.c) withslave.pcm { type hw card 0 }— card 0 being a patchedsnd-dummy. Thesound/drivers/dummy.chunks are therefore load-bearing: omit them and the system is silent. -
fpga_io.cpppokes the HPS↔FPGA bridges via/dev/memat hardcoded Cyclone V addresses. The singlemmapin the entire program is/dev/mem(shmem.cpp:22).→ requires
CONFIG_DEVMEM=ywithCONFIG_STRICT_DEVMEM=nandCONFIG_IO_STRICT_DEVMEM=n. (Verified: stock hasCONFIG_DEVMEM=yand# CONFIG_STRICT_DEVMEM is not set.) [P0: the rationale here was wrong —STRICT_DEVMEMis not default-y on 32-bit ARM (default y if PPC || X86 || ARM64 || S390), andmulti_v7_defconfighas no DEVMEM line at all. The assertion still must be checked; it just isn't a fight against a default.] -
[P0, new MUST] Main_MiSTer refuses to scan past
/dev/i2c-2(smbus.cpp:214). The ADV7513 HDMI transmitter lives on one ofi2c-0..2. A fourth I²C adapter, or a bus reordering in the DTS we author in P1.7, puts it out of reach and HDMI silently dies. -
Cyclone V cpufreq/overclock driver
-
/media/fatmount point -
/MiSTer.versionat the rootfs root — 6-charYYMMDD(e.g.250402), baked intolinux.imgat build time. Becauselinux.imgis the running root filesystem, the Downloader reads the live system's own/MiSTer.versionand updates when it differs from the last 6 characters of the db entry'sversion. (Note: it is not/media/fat/linux/MiSTer.version— a common misconception.) -
User-file restore contract: on every linux update the Downloader mounts the new
linux.imgread-write and copies/media/fat/linux/{hostname,hosts,interfaces,resolv.conf,dhcpcd.conf,fstab}over the corresponding files under/etcinside the image. Networking is ifupdown-style/etc/network/interfaces+dhcpcd.[P0: "all six must remain regular files — no symlink-into-tmpfs schemes" was wrong. Stock is itself a symlink-into-tmpfs scheme for one of them.] Five are regular files.
/etc/resolv.confis a symlink to../tmp/resolv.conf(verified on the raw ext4 image: inode 112,Type: symlink). Because the Downloader'scopy()follows the symlink, a user's customresolv.confis written to/tmp/resolv.confinside the offline image — which the tmpfs mount over/tmpthen shadows at boot. That restore step has therefore never worked, on any MiSTer, ever.⇒ RESOLVED — Q2, ADR 0011. Invariant A8 is withdrawn; the "no symlink-into-tmpfs schemes" rule above is deleted. We keep the symlink and adopt Buildroot's own skeleton default,
/etc/resolv.conf -> ../run/resolv.conf— preferring an upstream default over code we would have to maintain. Verified safe: nothing in the stock rootfs or Main_MiSTer references/tmp/resolv.confby path,dhcpcdfollows the symlink wherever it points, and the Downloader's restore target is/etc/resolv.confitself (Downloader_MiSTer/src/downloader/constants.py:101), not the destination. The restore therefore remains a silent no-op — deliberately, as parity. The defect is upstream (Downloader following a symlink during restore) and is worth reporting there.Do NOT "fix" this by making it a regular file — that would break DNS at boot.
/is mounted read-only at boot (roin the cmdline; livedmesgat t=1.45s:EXT4-fs (loop8): INFO: recovery required on readonly filesystem), andS41dhcpcd's hook20-resolv.confwrites/etc/resolv.confduring boot, while/is still read-only. The symlink into a tmpfs is the mechanism that makes DHCP-supplied DNS work at all. [A15]⚠ Observation trap, recorded because this project fell into it twice:
/etc/profile:23runsmount -o remount,rw /for every login shell. SSH into a MiSTer and your own session makes/writable, after whichmountreportsrwand you will conclude the read-only constraint does not exist. Usedmesg, notmount, to reason about boot-time filesystem state on this device. -
BusyBox init with
S01syslogd … S99userscript names (verified set: syslogd, klogd, udev, dbus, network, dhcpcd, bluetooth, ntp, proftpd, sshd, smb, user). The stockMiSTerbinary is launched from/etc/inittab::sysinit(backgrounded), not from an init script. Hotplug is eudev, not mdev. -
ext4, volume label
rootfs -
On-device Python is an ABI surface:
Downloader_MiSTerand many community scripts run with the target's interpreter. Stock ships 3.9 (EOL); Buildroot 2026.05 ships 3.14+. Compatibility must be tested, not assumed. -
Stock ships 52 xz-compressed kernel modules (
CONFIG_MODULE_COMPRESS_XZ=y) under/usr/lib/modules/5.15.1-MiSTer/— every WiFi driver, Bluetooth USB, andxone— plus 66 firmware files (P0: the "72" figure counted 6 directories as files) and the full module toolchain (kmod,depmod,modprobe,udevd). Module infrastructure is an existing convention to keep at parity; shipping classes D/E as Buildroot module packages reproduces the stock runtime layout exactly.
[P0: the fork carries 109 commits, not ~60.] And the baseline is now proven: the
fork has no upstream git ancestry at all (zero tags; its history is squashed whole-tree
imports), but content-diffing its version-bump commit aba1ef4c1 against a hash-verified
kernel.org linux-5.15.1 shows 0 files differing and 0 added — so the 109 commits
after it are provably the complete MiSTer delta. Excluding the two vendored WiFi trees,
the entire kernel problem is 143 files. Full triage with per-commit provenance:
docs/patch-provenance.md (P0.4). Triaged:
| Class | Content | Disposition |
|---|---|---|
| A. MiSTer core | MiSTer_fb.c, MiSTer-audio-spi.c, Cyclone V cpufreq/overclock, MiSTer_defconfig |
Carry as patches. The real work. |
B. loop= root patch |
init/do_mounts.c — adds a loop= cmdline param that mounts the FAT partition at /root2, loop-mounts linux/linux.img on /dev/loop8, and uses it as root |
DELETE — replace with an initramfs. See §5. |
| C. HID, now upstream | NSO controllers, hid-nintendo, many xpad IDs |
Drop. Mainline 6.18 has these. |
| D. HID, still out-of-tree | xone, GunCon 2/3, Fanatec, Flydigi Vader, remaining xpad IDs |
Carry as patches, or as Buildroot kernel-module packages where an upstream exists. |
| E. Realtek USB WiFi | rtl8188eu, rtl8188fu, rtl8812au, rtl8821au, rtl8821cu, rtl88x2bu |
Re-source from morrownr upstream as Buildroot kernel-module packages. Do not vendor. |
| F. Misc quirks | mmc LED, btusb VID/PIDs, usb-storage CD-ROM blacklist | Check upstream first; carry the remainder. |
| G. exfat replacement [P0 — new class, not in the original taxonomy] | The fork replaces mainline exfat with the out-of-tree Samsung driver | DROP — mainline exfat + vfat. ADR 0010. Carries three mandatory P1.10 requirements — see below. |
[P0] Class G was the largest unbudgeted finding in Phase 0. Stock's fs/exfat is not
mainline's. The Samsung out-of-tree driver:
- supports symlinks, stored in the FAT
ATTR_SYSTEMbit — so they work on FAT32 as well as exFAT; - mounts FAT12/16/32 and exFAT under a single
-t exfat(which is why theloop=patch gets away with one mount call); - decodes FAT32 filenames as UTF-8 (mainline
vfatdefaults to iso8859-1 ⇒ mojibake).
Mainline exfat and vfat have no symlink support whatsoever — and Main_MiSTer actively
resolves symlinks on /media/fat (file_io.cpp:1592, de->d_type == DT_LNK, since Jan
2019).
⇒ RESOLVED — Q1, ADR 0010: DROP the
driver. Evidence: a live stock MiSTer has 0 symlinks across /media/fat and every
/media/usb0..7. Carrying an out-of-tree filesystem driver to 6.18 and then tracking it to
Dec 2028 is exactly the open-ended burden §13 identifies as fatal to this project.
(Evidence is n=1. Sufficient for personal use; widen it, or ship the detection one-liner in
ADR 0010(d), before any public release.)
Dropping the driver is not a no-op. Three requirements land on P1.10 (A2):
- FAT32 must still mount, or the device does not boot. Mainline exfat cannot mount
FAT32 at all, and the rootfs is a file on that partition (
root=/dev/mmcblk0p1 loop=linux/linux.img). A hardcoded-t exfatturns every FAT32 card into a brick, not into a card that merely lost symlinks. The initramfs must tryexfat, then fall back tovfat. Both are already=yin the stock config. - The vfat fallback needs UTF-8 — spelled
utf8=1, NOTiocharset=utf8. SetCONFIG_FAT_DEFAULT_UTF8=y(stock: not set) or passutf8=1at mount. LeaveCONFIG_FAT_DEFAULT_IOCHARSET="iso8859-1"andcodepage=437alone —Documentation/filesystems/vfat.rst:72saysiocharset=utf8is not recommended (it routes through the NLS byte-at-a-timechar2uni()path instead ofutf8s_to_utf16s(),fs/fat/namei_vfat.c:517). exfat needs nothing — there,iocharset=utf8is the correct spelling (fs/exfat/super.c:38maps it onto the same internal flag) and it is already the default. This does not affect Windows interop in either direction: FAT/exFAT store long filenames on disk as UTF-16 regardless, and the setting governs only the kernel↔userspace byte encoding on the Linux side. - Preserve the behaviourally meaningful mount options:
sync,dirsync(write-through — users power the board off by pulling the plug),fmask=0022,dmask=0022,errors=remount-ro. (namecase=0, visible in stock's/proc/mounts, is ashow_options()echo of a driver default, not a passed option. Mainline exfat is case-insensitive/preserving per spec. Nothing to do — do not re-raise it.)
Neither (1) nor (2) can reproduce on the maintainer's own hardware (238.7 GB exFAT card, 0 non-ASCII filenames — both verified live). They will only ever be caught by the P1.10 test matrix, which must therefore include a FAT32 card with a non-ASCII filename.
Class B is the one that will bite. init/do_mounts.c was substantially refactored in the
6.x series (mount_block_root and friends are gone). Forward-porting that patch is both hard
and unnecessary — see §5.
Forward-porting forces you to read code that has otherwise been copied forward untouched
since 2021, and that turned up six real defects in the stock 5.15 kernel — none of them
regressions we introduced. Two are memory-safety bugs. Full table with evidence:
docs/patch-provenance.md §10.
| Bug in stock | Consequence on a stock MiSTer | |
|---|---|---|
| B1 | hid-gamecube-adapter never cancels the per-port work_connect before kfree()ing the adapter |
Use-after-free. Plugging a controller into the GameCube adapter while the adapter is being unplugged can corrupt kernel memory. |
| B2 | MiSTer_fb tests IS_ERR() on memremap(), which returns NULL on failure |
Oops on first fbcon draw if the framebuffer window ever fails to map. |
| B3 | MiSTer-audio-spi tests == NULL on device_create(), which returns an ERR_PTR |
A failed /dev/MrAudio creation was treated as success. |
| B4 | MiSTer-audio-spi's device_open() computes the ring length even when spi_read() failed |
The diagnostic you read to debug a broken SPI link is itself wrong. |
| B5 | The fork's own K400 patch reuses SetFeature at the wrong feature index |
Wrong HID++ feature written to a Logitech K400. |
| B6 | socfpga-cpufreq's wait_for_fsm() passes a mask where wait_on_bit() wants a bit number — it polls bit 1, not bit 0 |
Harmless today only because the call returns immediately. |
B1–B5 are fixed. B6 is deliberately NOT fixed and is carried verbatim: correcting it changes the timing of a live PLL reprogramming sequence on silicon we have not yet booted, and a forward-port is the wrong place to smuggle in an unvalidated change to clock sequencing. It is tracked for P1.13 hardware bring-up.
(B1 and B4 were found by automated static review on PR #2 — after the ports were already building warning-free and passing every acceptance check. Worth remembering when judging how much a clean build proves.)
Every one of the six is in MiSTer-original or fork-modified code. None is in mainline code. That is not a coincidence and it is the single strongest argument for this project's central posture:
Carry the smallest possible delta against a pristine kernel.org tree, and hand every subsystem back to mainline the moment mainline can hold it.
Out-of-tree code accumulates defects for a structural reason, not because its authors are
careless: it never faces linux-kernel review, it is never touched by upstream's tree-wide
refactors and cleanups, it never gets the free bug-fixes that come from someone else
refactoring a subsystem underneath you, and — as B1 and B4 show — nobody re-reads it, because
"it already works." B5 is the sharpest illustration: it is a bug in a MiSTer patch layered
on top of an upstream driver, i.e. introduced by the act of forking.
The corollary is that the delta is itself the risk surface, so shrinking it is a safety measure, not just hygiene. What Phase 0/1 already handed back:
| Handed back to mainline | Was |
|---|---|
| exfat + vfat (ADR 0010) | an out-of-tree Samsung driver, a permanent maintenance burden |
the loop= root mount (§5) |
a patch to init/do_mounts.c, a hot core file upstream has since rewritten |
| spidev binding (P1.8) | a bespoke altspi entry in spidev_dt_ids[] |
| usb-storage / btusb device IDs (P1.9) | fork patches; landed upstream since 5.15 |
dwc2/core.c, vt.h MAX_NR_CONSOLES |
dead weight — a provable no-op and a few KB |
| G923 force feedback (§9.3) | a ~2000-line out-of-tree FF rewrite that never landed upstream |
109 fork commits → 24 carried patches. What remains is, with one exception, genuinely MiSTer-specific hardware that mainline has no reason to know about: the FPGA framebuffer, the SPI audio ring, the Cyclone V clock-manager overclock. That is the irreducible core, and it is where the remaining risk lives — so it is exactly what deserves upstreaming, review, and tests, rather than another decade of being copied forward untouched.
The one exception worth acting on: 0014-hid-gamecube-adapter. It is not
MiSTer-specific at all — it is a generic USB HID driver for a mass-market adapter, carried
out-of-tree, and it turned out to contain a use-after-free. That is precisely the class of
code that should be upstream. P4 should attempt to upstream it (with B1 fixed), or
replace it with an in-tree driver if one has since appeared. Same reasoning applies to most
of the 0010–0030 HID quirks: device IDs and quirks are the easiest thing in the kernel to
get merged, and every one that lands upstream is one we stop owning.
Classes D and E are already modules in the stock image — the 5.15 fork builds them =m
(52 .ko.xz under /usr/lib/modules, CONFIG_MODULE_COMPRESS_XZ=y) with
kmod/depmod/udevd and 66 firmware files shipped (P0: not 72) — including
xow_dongle.bin, so stock already bundles xone's firmware rather than fetching it,
which settles P3.2's redistribution question. Re-sourcing them as Buildroot
kernel-module packages reproduces the existing runtime layout; what must be kept at parity
is the module toolchain, udev-driven autoload, and the firmware inventory (xone's firmware
additionally has redistribution constraints).
Mainline gained arch/arm/boot/dts/intel/socfpga/socfpga_cyclone5_de10nano.dts in January 2025
(~v6.14). It is deliberately minimal — the submitter's commit message says it is "enough to
make the board boot to Linux with the rootfs on a micro SD card." That is all it does.
| Node | Mainline DTS | MiSTer needs |
|---|---|---|
mmc0, uart0, gpio0-2, i2c0 (adxl345), gmac1 |
enabled | ✓ (but different gmac1 skews: rgmii + explicit txc-skew-ps/rxc-skew-ps, max-frame-size = <3800>) |
\&usb1 |
absent | required — no USB means no controllers, no keyboard |
\&fpga_bridge0/1/2 |
absent | required |
MiSTer_fb (reg = <0x22000000 0x800000>, IRQ 40) |
absent | required |
\&spi0 → MiSTer,spi-audio |
absent | required |
\&spi1 → spibri@0 → /dev/spidev1.0 |
absent | required (see §13, spidev hazard) |
\&i2c2, \&uart1 (DMA props deleted) |
absent | required |
i2c-gpio bus + RTC (pcf8563/m41t81/mcp7941x) |
absent | required (RTC add-on board) |
gpio-leds hps_led0, regulator_3_3v, mmc0 vmmc/vqmmc |
absent | required |
Crucially, every one of these is a device-tree gap, not a driver gap. Mainline's
socfpga.dtsi already defines fpga_bridge0/1/2, usb1 (snps,dwc2), spi0, spi1,
i2c2, and uart1 — they are simply not enabled for this board. The drivers (dwc2,
socfpga fpga-bridge, spi-dw, i2c-gpio, gpio-leds, the RTCs, stmmac) have all been mainline
for years.
Disposition: carry our own DTS as a patch in linux-patches/, based on mainline's and
adding the nodes above. Same mechanism as the driver patches. No extra machinery.
The shipped image is fully legible: 626 shared libraries, 1,885 binaries, and 52 .ko.xz
kernel modules (WiFi, Bluetooth USB, xone — everything else is built in). The SONAME list
reads directly as a Buildroot package list. Better still, the kernel carries
CONFIG_IKCONFIG=y, so the exact stock kernel config is extractable from the shipped
zImage — we have it, along with the exact stock DTB (docs/stock-inventory/).
Shipped versions today — the reason this project exists:
| Component | Shipped | Status |
|---|---|---|
| Linux | 5.15.1 (Nov 2021) | ~190 stable releases behind |
| glibc | 2.31 (Feb 2020) | |
| Samba | 4.14.6 | long EOL |
| curl | 7.78.0 | |
| OpenSSH | 8.6p1 | |
| BusyBox | 1.33.1 | |
| bluez | 5.61 | |
| Python | 3.9 | EOL |
This box runs an SMB server, an FTP server, and SSH, and people put it on home networks.
Size: the rootfs is 93% full (297 MB used of 347 MB). A modern package set will not
shrink. Budget for a 512 MiB linux.img and audit what assumes 400 MB.
Today the kernel is patched to loop-mount linux/linux.img from within init/do_mounts.c.
This is a patch to a core kernel file that upstream has since rewritten.
Replace it with a ~200 KB embedded initramfs. Two build mechanics matter and are easy to get wrong:
- Not
BR2_TARGET_ROOTFS_INITRAMFS— that option embeds the entire target rootfs (~300 MB) into the kernel image. Instead, a second, minimal Buildroot config (configs/mister_initramfs_defconfig: static BusyBox,BR2_TARGET_ROOTFS_CPIO) produces a tiny cpio that the main build's kernel consumes viaCONFIG_INITRAMFS_SOURCE. CI sequences the two builds. - The initramfs must be embedded in the zImage: the stock U-Boot boot command (kept byte-identical, §8) never loads a separate initrd.
And /init must be cmdline-driven, not hardcoded. U-Boot is unchanged, so the cmdline
still carries root=$mmcroot loop=linux/linux.img ro rootwait — and existing u-boot.txt
setups override $mmcroot (USB boot). /init parses root= (the data partition, FAT32
or exFAT) and loop= (image path) from /proc/cmdline, implements rootwait as a
retry loop, and on any failure prints a diagnostic banner and drops to a serial rescue
shell. In sketch:
# real script parses root=/loop= from /proc/cmdline, retries until the
# device appears (rootwait), tries vfat then exfat, rescue shell on failure
#
# [P0] -o sync,dirsync is NOT optional: stock mounts /media/fat sync,dirsync
# from the kernel and /etc/fstab never re-mounts it. Mounting async here would
# be a real power-off-corruption regression, not a tuning choice.
#
# [P0/ADR-0010] BOTH types must be tried, and exfat must NOT be assumed. Stock's
# kernel does a single init_mount(..., "exfat", ...) that works for FAT32 only
# because the out-of-tree driver handles FAT12/16/32 too. Mainline exfat cannot.
# The rootfs is a file ON this partition, so getting this wrong does not lose a
# feature -- it fails to boot.
#
# utf8=1 (NOT iocharset=utf8) is the vfat spelling: Documentation/filesystems/
# vfat.rst:72 explicitly deprecates iocharset=utf8. exfat needs nothing -- utf8
# is already its default. Without this, non-ASCII filenames mojibake and
# Main_MiSTer's stored paths (recents/favorites/MGL) stop resolving.
# [P1.10] Mounted **rw**, deliberately. This looks like it contradicts A15; it is
# what A15 actually requires. losetup opens the backing file O_RDWR, and if that
# fails BusyBox's set_loop() SILENTLY retries O_RDONLY -- whereupon the kernel
# marks the loop DEVICE read-only, which is precisely the state A15 forbids,
# reached through the back door. Read-only-ness belongs on the loop *mount*
# (`mount -o ro`, below), never on the partition or the device.
#
# [P1.10] noatime,nodiratime are stock's, not decoration: do_mounts.c:667 passes
# MS_NOATIME|MS_NODIRATIME. An earlier version of this sketch omitted them.
FAT_OPTS="rw,sync,dirsync,noatime,nodiratime,fmask=0022,dmask=0022,errors=remount-ro"
mount -t exfat -o "$FAT_OPTS" "$rootdev" /mnt/fat || \
mount -t vfat -o "$FAT_OPTS,utf8=1" "$rootdev" /mnt/fat
# [P0] Use losetup -f, NOT a hardcoded /dev/loop8. loop8 is an artifact, not a
# contract: LOOP_MIN_COUNT=8 pre-creates loop0-7 only (loop8 is instantiated on
# demand), and nothing in the stock rootfs or Main_MiSTer references it by name.
#
# [P0/A15] NO `losetup -r`. The loop DEVICE must stay writable even though the
# rootfs is MOUNTED read-only (cmdline `ro`). Stock: /sys/block/loop8/ro == 0.
# /etc/profile:23 runs `mount -o remount,rw /` on every login shell, and a
# read-only loop device makes that remount fail -- so `losetup -r` would leave a
# logged-in user with a permanently read-only rootfs, which stock is not.
# Read-only-ness belongs on the MOUNT, not on the device.
loopdev="$(losetup -f)"
losetup "$loopdev" "/mnt/fat/$looppath"
mount -o ro "$loopdev" /newroot
# Move the data-partition mount so /media/fat is already there. Stock's kernel
# creates /media/fat as a BIND mount (do_mounts.c:677) and NOTHING in /etc mounts
# it -- so if the initramfs does not recreate it, /media/fat simply does not exist.
#
# [P1.10] `mount -o move`, NOT `mount --move`. BusyBox mount has no long options;
# an earlier version of this sketch said `--move` and would have failed at boot.
mount -o move /mnt/fat /newroot/media/fat
exec switch_root /newroot /sbin/initBenefits:
- Eliminates an out-of-tree patch to a hot, frequently-refactored core file. This is the single biggest reduction in long-term maintenance burden in the whole plan.
- Boot semantics become debuggable and testable in userspace — the same cpio boots under a generic QEMU ARM machine in CI against synthetic FAT/exFAT disks (§11).
- The
loop=androot=args keep their exact stock semantics, so every existingu-boot.txtkeeps working;mem=511Mandmemmap=stay untouched. - Buildroot builds it natively as a second minimal config. The only new machinery is sequencing two builds.
Cost: the kernel zImage grows slightly; boot gains a few hundred milliseconds.
Worth it several times over.
A single BR2_EXTERNAL tree. Buildroot itself is fetched, never vendored.
mister-linux/
├── external.desc
├── external.mk
├── Config.in
├── configs/
│ ├── fragments/ (common, de10nano, de10nano-image, kernel-only, de25nano + stacks.mk)
│ └── mister_initramfs_defconfig # stage-1 tiny static-BusyBox cpio (§5)
├── board/mister/de10nano/
│ ├── linux.config # full kernel config (make savedefconfig)
│ ├── linux-patches/ # → BR2_LINUX_KERNEL_PATCH
│ │ ├── 0001-fbdev-add-MiSTer_fb-driver.patch
│ │ ├── 0002-sound-add-MiSTer-audio-spi.patch
│ │ ├── 0003-cpufreq-cyclone5-overclock.patch
│ │ ├── 0004-dts-de10nano-MiSTer.patch # §4.1a — usb1, bridges, fb, spi, rtc
│ │ ├── 0005-spidev-accept-altspi-compatible.patch
│ │ ├── 0010-hid-guncon2.patch
│ │ ├── 0011-hid-guncon3.patch
│ │ ├── 0012-hid-fanatec.patch
│ │ ├── 0013-hid-flydigi-vader.patch
│ │ └── 0020-usb-storage-blacklist-realtek-cdrom.patch
│ ├── uboot-patches/ # empty in v1; P5: build fixes ONLY, never
│ │ # behaviour changes (ADR 0017)
│ ├── rootfs-overlay/
│ │ ├── etc/init.d/S??… # BusyBox init scripts (parity with stock)
│ │ ├── etc/fstab
│ │ └── media/fat/ # mount point
│ ├── initramfs-overlay/
│ │ └── init # the switch_root script from §5
│ ├── genimage.cfg
│ ├── post-build.sh # writes MiSTer.version
│ ├── post-image.sh # assembles release_YYYYMMDD.7z
│ └── readme.md
├── package/ # Buildroot kernel-module packages (P3.1/P3.2)
│ ├── rtl8812au/
│ ├── rtl8821au-morrownr/ # "-morrownr": Buildroot upstream now ships
│ ├── rtl8821cu-morrownr/ # its own same-named rtl8821au/rtl8821cu
│ ├── rtl88x2bu/ # (different forks) -- renamed to avoid
│ ├── rtl8188eu-aircrack-ng/ # the Kconfig/Make collision; see each
│ ├── rtl8188fu/ # package's Config.in for the detail.
│ ├── xone/ # dlundqvist/xone fork (P3.2) -- driver only,
│ │ # unambiguously GPL-2.0-or-later
│ ├── xow-firmware/ # Xbox Wireless Dongle firmware, fetched from
│ │ # Microsoft at build time (ADR 0003) -- never in git
│ ├── cabextract/ # host-only build tool xow-firmware depends on
│ └── linux-firmware-extra/ # P3.3 -- gap-fill for stock firmware.md files no
│ # linux-firmware Config.in sub-option covers; same
│ # tarball/hash as the sibling linux-firmware package
│ # (No u-boot/ submodule: ADR 0017's submodule pin was
│ # superseded by ADR 0024 — the mainline build takes
│ # Buildroot's own hash-verified tarball instead.)
├── scripts/ # inventory generators, ABI checker, CI test suite
├── .github/workflows/
│ ├── build.yml # build + upload Release assets
│ └── publish-db.yml # regenerate and publish db.json
├── renovate.json # dependency automation — §9, final hardening step
└── docs/
├── abi-contract.md # §3, expanded — the thing we must not break
├── patch-provenance.md # every patch: origin, upstream status, owner
├── boot-chain.md # U-Boot contract: zImage_dtb, ATAGs, u-boot.txt env
├── downloader-contract.md # LinuxUpdater: 7z layout, MD5, version semantics
└── decisions/ # ADRs: toolchain, initramfs, xone firmware, HIL rig
BR2_LINUX_KERNEL=y
BR2_LINUX_KERNEL_CUSTOM_VERSION=y
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.18.xx"
BR2_LINUX_KERNEL_PATCH="$(BR2_EXTERNAL_MISTER_PATH)/board/mister/de10nano/linux-patches"
BR2_LINUX_KERNEL_USE_CUSTOM_CONFIG=y
BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE="$(BR2_EXTERNAL_MISTER_PATH)/board/mister/de10nano/linux.config"
BR2_LINUX_KERNEL_DTS_SUPPORT=y
BR2_LINUX_KERNEL_INTREE_DTS_NAME="intel/socfpga/socfpga_cyclone5_de10nano"
# NOT BR2_TARGET_ROOTFS_INITRAMFS — that would embed the whole rootfs (§5).
# The stage-1 cpio from mister_initramfs_defconfig is wired in via
# CONFIG_INITRAMFS_SOURCE in linux.config.
Buildroot downloads a signed, hash-verified kernel.org tarball and applies our patch directory on top. Bumping 6.18.38 → 6.18.39 is a one-line change and CI tells us if a patch stopped applying. That is the entire point: stable updates become mechanical instead of manual.
Note: mainline gained arch/arm/boot/dts/intel/socfpga/socfpga_cyclone5_de10nano.dts in
January 2025 (landed ~v6.14), so 6.18 has it. But it is not sufficient on its own — see
§4.1a. We base our DTS on it and patch in the missing nodes.
Buildroot 2026.05.1 sets BR2_LINUX_KERNEL_LATEST_VERSION to 7.0.11. 7.0 is a normal
(non-LTS) mainline series — it stops receiving stable updates soon after the next mainline
opens, so pinning Buildroot's default would put us on a kernel with a short support tail. We
deliberately pin the 6.18 LTS line instead (below), which stays on a stable .y branch
with security backports.
This is not a Buildroot defect. BR2_LINUX_KERNEL_LATEST_VERSION means "newest kernel at the
time this branch was cut" — it is not a recommendation, and Buildroot's LTS guarantee covers
Buildroot's own package infrastructure and package security fixes, not your kernel choice.
Buildroot expects boards to pin, and nearly every in-tree board defconfig does.
We always pin: BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE.
Greg Kroah-Hartman revised the longterm EOL dates on 25 Feb 2026:
| Kernel | EOL | Notes |
|---|---|---|
| 6.18 | Dec 2028 | LTS. DE10-Nano DTS in-tree. Altera maintains socfpga-6.18.x-lts. |
| 6.12 | Dec 2028 | LTS. Also CIP "Super LTS" (10-year, limited patches). |
| 6.6 | Dec 2027 | LTS |
| 6.1 | Dec 2027 | LTS |
| 5.15 | Dec 2026 | ← MiSTer's current base. Dead in ~5 months. |
Recommendation: 6.18 LTS. The EOL extension erased 6.12's longevity advantage — both now
run to Dec 2028 — so take the newer tree. 6.18 additionally has the DE10-Nano DTS in-tree as a
starting point, and Altera's socfpga-6.18.x-lts branch exists as a cross-check if a
Cyclone V-specific fix is ever needed.
Altera's linux-socfpga adds ALTERA_SYSID, ALTERA_ILC, OF_CONFIGFS, and
FB_ALTERA_VIP_FB2_PLAT. MiSTer uses none of them. Cyclone V SoC support — clk, pinctrl,
reset, fpga-mgr, fpga-bridge, dw_mmc, dwc2, stmmac, spi-dw — has been mainline for a decade.
The vendor tree buys us nothing and costs us a dependency that lags upstream stable.
Use pristine kernel.org tarballs + linux-patches/. Bumping 6.18.38 → 6.18.39 becomes a
one-line change, and CI tells us immediately if a patch stopped applying.
[2026-07-28] This section is superseded from "Phase 5 path" onward by ADR 0024. The from-source bootloader is now built from mainline U-Boot 2026.04, not the 2017.03 fork, as a non-shipping capability artifact — the mainline port surface has been measured rather than estimated, and it is smaller than this section assumed. Do not add the
u-boot/submodule. Design and evidence:docs/uboot-mainline-port.md. Execution:docs/uboot-tasks.md. Everything below about v1 shipping the stock blob byte-identical still stands and is still the decision; only the Phase 5 path changed.
The request was for a modern U-Boot. Here is the honest assessment.
v1 keeps uboot.img byte-identical, carried forward from U-boot_MiSTer (a fork of
U-Boot 2017.03).
Rationale:
- Highest blast radius, lowest user benefit. A bad SPL — wrong DDR calibration, wrong pinmux — presents to the user as a bricked board. This is the first thing that runs.
- The kernel↔U-Boot contract here is
bootz+ FDT + a cmdline. It is version-agnostic. A 6.18zImageboots fine off the 2017 SPL. The two problems are genuinely decoupled; coupling them is a self-inflicted wound. - MiSTer's U-Boot has custom behaviour that must be reproduced before it can be replaced —
at minimum the
u-boot.txtenvironment-from-FAT mechanism (this is how the per-boardethaddrgets set;mr-fusionwrites it during install).
Verified from the shipped artifacts (docs/verification/stock-release-20250402.md):
uboot.img is four identical 64 KiB SPL copies plus a U-Boot 2017.03+ for de10-nano
uImage at offset 256 KiB. The environment is baked in (bootcmd/mmcload/mmcboot, plus
fpgaload/fpgacheck — U-Boot pre-loads menu.rbf into the FPGA and supports a
warm-reboot core handoff via reserved RAM), and u-boot.txt is applied with
env import -t. Critically, the Downloader runs updateboot on every linux update,
which dd-writes the shipped uboot.img over the raw boot partition and erases U-Boot's
saved environment at sector 1. Two consequences: shipping the stock uboot.img
byte-identical is load-bearing (whatever we ship gets flashed), and no state survives in the
saved environment — the effective env is always built-in defaults + u-boot.txt.
Phase 5 path — REVISED 2026-07-13 (ADR 0017): build the existing fork from source; do not port to mainline.
The original Phase 5 plan was a mainline U-Boot port (socfpga_de10_nano_defconfig +
re-implementing u-boot.txt env-from-FAT, fpgaload/fpgacheck, the MiSTer-only mt
command, the warm-reboot mailbox). Every one of those re-implementations is new code in
the one component whose failure mode is a bricked board — reinventing a wheel that
already exists and is proven on every shipped MiSTer. Instead, once everything else is
stable and hardware-tested:
- Source:
MiSTer-devel/u-boot_MiSTer(U-Boot 2017.03 fork), pinned as a git submodule atu-boot/, commit8dcc3484— which is simultaneously theMiSTerbranch HEAD (verified 2026-07-13; the branch has not moved since 2021) and the exact commit the shippeduboot.imgwas proven to be built from (docs/boot-chain.md§3.1, the malformed-env-entry fingerprint). The pin therefore covers precisely the behaviours P0.8 verified. Nothing needs porting. A submodule is a pointer, not a binary (standing rule 1 holds), and it records the source pin in our tree so the full-image build stays reproducible even if the upstream branch moves or vanishes. - Buildroot builds it from the submodule:
BR2_TARGET_UBOOT+UBOOT_OVERRIDE_SRCDIRpointing atu-boot/(viaBR2_PACKAGE_OVERRIDE_FILE), starting from the fork's ownMiSTer_defconfig; outputu-boot-with-spl.sfp, renameduboot.img. Theuboot-patches/directory mirrors the kernel model but is reserved for build fixes only — a 2017 codebase may need coaxing under a 2026 toolchain — never behaviour changes. - A byte-identical rebuild is impossible and is not the goal (
docs/boot-chain.md§3.2: compiled-in non-UTC timestamp, exact 2020 Arm toolchain). The default Downloader channel keeps shipping the stock blob byte-identical (P4.4). The from-source build is validated by behavioural parity: identical default-environment blob (all 20 entries ofdocs/boot-chain.md§3.1), identical SPL/uImage layout and header fields,mtpresent — every remaining diff enumerated and individually explained. - New deliverable — full SD-card image. Phase 5 also produces a flashable
sdcard.img: MBR with p1 = FAT32 data partition and p2 = type0xA2boot partition withuboot.imgwritten raw at its start (the SPL contract,docs/boot-chain.md§2.1). The payload parity target for p1 is "a card as mr-fusion leaves it", inventoried from the mr-fusion source at a pinned release: the P0.6files/linux/payload (linux.img,zImage_dtb,uboot.img,updateboot, config templates),menu.rbfand the stockMiSTerbinary, the standard folder tree, and the baseScripts/set mr-fusion installs (the Downloader script, the WiFi setup script) — plus a recentScripts/update_all.sh, which is deliberately cheap to include: it is a single self-updating file that runs the Downloader under the hood, so pinning "recent" is sufficient and it brings no further on-card dependencies. Everything is fetched at build time, pinned by commit/release + hash, never committed (standing rule 1). Built with genimage as a post-image step; published as a separate release asset, never part ofrelease_*.7zor the db.json channel. This gives the project a from-scratch install path that does not depend on mr-fusion or the Windows SD installer. It must reproduce mr-fusion's per-boardethaddrprovisioning (a first-boot write oflinux/u-boot.txtwith a unique MAC) — or every board would share the compiled-in fallback02:03:04:05:06:07— and should mirror mr-fusion's optional pre-seeding hooks (wpa_supplicant.conf,samba.sh, userScripts/). - Gate on: a hardware test matrix and a documented, drilled recovery procedure. The
built U-Boot ships behind an explicit opt-in flag, separate from the
linux.imgupdate; the SD image defaults to embedding the stock blob.
Change one variable at a time.
- GitHub Actions, containerized, pinned base image.
- Cache Buildroot
dl/andccachebetween runs. make legal-infoon every build → SBOM + license manifest as a release artifact. (Note: the current image ships no manifest, no.config, and no legal-info at all.)- GitHub artifact attestations (
actions/attest-build-provenance) on the image assets, so anyone can verify a downloadedlinux.imgcame from this repo's CI at a given commit.
release_YYYYMMDD.7z # files/linux/{linux.img,zImage_dtb,uboot.img}
linux.img
zImage_dtb
SHA256SUMS
buildroot.config # exact, reproducible
linux.config
legal-info.tar.gz # SBOM
sdcard.img.xz # Phase 5 only (§8, ADR 0017): full flashable SD image —
# separate asset, never referenced by db.json
The repo stays under ~10 MB. For contrast, SD-Installer-Win64_MiSTer is 5.8 GB of
committed archives, Distribution_MiSTer is 1.9 GB, and Main_MiSTer carries 95 committed
binaries (67 MB) with no CI at all.
- Buildroot version pinned (2026.08.x)
- Kernel version + upstream hash pinned; patches in-tree
-
BR2_DOWNLOAD_DIRpopulated from upstream; no vendored tarballs -
buildroot.configandlinux.configpublished with every release - ext4 generation pinned: fixed UUID, pinned filesystem feature set,
SOURCE_DATE_EPOCH,BR2_REPRODUCIBLE=y— 2026-eramke2fsdefaults (random UUID/hash seed, new features) break determinism unless pinned (stock reference:HAS_JOURNAL,METADATA_CSUM,64BIT,FLEX_BG, fixed UUID) - Two independent builders get identical
linux.imghashes — enforced by a CI job that builds twice and compares
Once the pipeline is stable and trusted, Renovate keeps every moving part current automatically. This mechanizes the sustainability commitment of §13:
- Buildroot 2026.08.x tarball version + SHA-256 (custom/regex manager over the pin file)
- Kernel 6.18.y version + hash (custom datasource over kernel.org's
releases.json) - morrownr driver packages and other commit pins (git datasource)
- CI container image digests and GitHub Actions versions
Every Renovate PR gets the full CI treatment — build, patch-apply, ABI checks, double-build reproducibility — so a stable bump that breaks a carried patch is caught in the PR, never on user hardware. Deliberately sequenced last: automate a pipeline only after it has earned trust.
Downloader_MiSTer's LinuxUpdater reads a top-level linux key from any configured
database, compares version (last 6 characters) against the running system's
/MiSTer.version, and applies the archive if they differ. The official entry looks like
this:
"linux": {
"hash": "8dc3acae7d758a80a363fbd7ad31d95d",
"size": 93727644,
"url": ".../SD-Installer-Win64_MiSTer/b8531c78.../release_20250402.7z",
"version": "250402"
}Note it is commit-pinned and MD5-verified — the Downloader does supply-chain hygiene properly. We publish an identically-shaped entry pointing at our GitHub Release asset.
The full updater contract has been verified from Downloader_MiSTer source and the shipped
artifacts (docs/verification/stock-release-20250402.md):
- Version check: the running system's
/MiSTer.versionvs the last 6 characters of the db entry'sversion— inequality, not ordering. - Extraction: a pinned ARM
7zathe Downloader fetches on demand from the SD-Installer repo (MD5-verified) — nothing in the installed image performs it. Onlyfiles/linux/*is extracted; the rest of the archive serves the Windows SD installer. - Apply: user files are copied into the new image (§3 contract),
files/linux/is rsynced over/media/fat/linux/(soupdatebootand the config templates ship with us),updatebootflashesuboot.img, thenlinux.imgis swapped into place and a reboot flag is raised.
Users add one database to downloader.ini and opt in.
Known wrinkle — [P0: worse than "document the ordering". There IS no ordering.]
LinuxUpdater warns "Too many databases try to update linux. Only 1 can be processed"
and takes _linux_descriptions[0]. v2 of this plan said "document the exact
downloader.ini ordering". No such ordering exists, and the user cannot control it:
sorted_db_sections()forcesdefault_db_id— i.e.distribution_mister— to the front of the push queue;installed_dbsis appended in job-completion order across six concurrent workers, not indownloader.iniorder;- a db's job completes as soon as its own
db.jsonis fetched and parsed.
⇒ The winner is whichever db.json parses first. Ours wins only because it is tiny
against Distribution's multi-megabyte catalog. That is an emergent property of relative
payload size, not a guarantee — and it could flip if Distribution's db shrinks or the
Downloader's threading changes.
Two consequences, both load-bearing:
- Keeping our
db.jsonminimal (emptyfiles/folders) is a design rule, not an aesthetic preference. It is the only thing that wins us the race. - Onboarding uses the Downloader's drop-in ini mechanism (
/media/fat/downloader_*.inior/media/fat/downloader/*.ini) rather than an edit to the user'sdownloader.ini.
Full source citations: docs/downloader-contract.md (P0.6). See also open question Q3.
[P0] The update is not atomic, and its success signal cannot be trusted. The flash-phase
shell script (linux_updater.py:157-168) runs without set -e and ends in touch, so
its exit status is touch's. A failed mv, rsync, or updateboot still reports
success and still raises the reboot flag. P4.8's rollback runbook must assume a "successful"
update may have half-failed.
Rollback is trivial and must be documented prominently: remove the db line, re-run the
Downloader, get the official linux.img back. (It works precisely because the version
check is an inequality, not an ordering — a "downgrade" is just a different string.)
Boot the new image and run the unmodified, stock MiSTer binary from Distribution_MiSTer.
If it does not reach the menu, nothing else matters. This test comes first.
CI vs hardware split. QEMU has no Cyclone V machine model, so per-commit CI cannot boot
the real image. CI instead runs: (a) static SONAME/ABI checks of the built rootfs against the
contract; (b) the stock MiSTer binary under a qemu-arm chroot of the new rootfs — it must
get past dynamic linking and early init, failing only at the whitelisted FPGA-access point;
(c) the initramfs cpio booted on a generic QEMU ARM machine against synthetic FAT/exFAT
disks. Real hardware gates each release — manually at first, optionally automated later
with a HIL rig (USB-SD-mux, power control, serial capture) as a self-hosted runner.
- Boot to menu; boot time to menu must not regress (users notice)
- HDMI output across resolutions; analog I/O board; VGA
- USB controllers (xpad, xone, hid-nintendo, GunCon, Fanatec, Flydigi)
- Bluetooth pairing
- WiFi across the Realtek dongle zoo — the single most likely regression area
- Samba, SSH, FTP
- MIDI / MT-32 (fluidsynth, soundfonts)
- Save states, CHD/CD cores,
exFATmount ofMiSTer_Data update.sh,wifi.sh, and a sample of popular community scripts- SD card compatibility spread
- Boot-to-menu time: ≤ current
- Free RAM at menu: ≤ current
- Rootfs free space: ≥ 15% of the 512 MiB image we build — [P0: state the budget against our image, not stock's. Stock's 375 MiB rootfs is only 13.56% free, so "≥ 15%" measured against stock would fail on stock itself.]
| Phase | Work | Exit criterion |
|---|---|---|
| P0 — Recon | Write docs/abi-contract.md. Triage all ~60 kernel commits into classes A–F with provenance. Derive the Buildroot package set from the shipped image. |
Patch triage table is complete and reviewed |
| P1 — Kernel | Buildroot builds 6.18 LTS from kernel.org + linux-patches/. Forward-port MiSTer_fb, audio-spi, cpufreq. Replace loop= with the initramfs (§5). |
Boots to a serial console on real hardware |
| P2 — Rootfs | Buildroot 2026.08 rootfs, glibc, SONAME parity. Read-only root preserved. | Stock MiSTer binary reaches the menu. |
| P3 — Parity | WiFi, Bluetooth, Samba, FTP, SSH, MIDI. CI + release artifacts + SBOM. | Hardware matrix (§11) green |
| P4 — Beta | Publish db.json. Recruit testers. Document rollback. Final pipeline hardening: Renovate dependency automation (§9). |
Sustained opt-in use, no P1 bugs |
| P5 — Full SD image + U-Boot from source | Optional. Build uboot.img from the pinned u-boot_MiSTer submodule; produce a flashable sdcard.img — kernel, linux.img, bootloader, mr-fusion-parity payload + update_all.sh (§8, ADR 0017). |
Fresh card flashed from sdcard.img boots to menu; built U-Boot passes behavioural parity + hardware matrix; recovery procedure drilled |
Sequential. Each phase's exit criterion is a hardware test, not a code review.
The task-level breakdown — per-task model assignments, dependencies, and acceptance
criteria — lives in TASKS.md.
| Risk | Severity | Mitigation |
|---|---|---|
MiSTer_fb / /dev/mem FPGA ABI breaks on 6.x |
High | P2's exit criterion is this test. Fail fast. |
| Realtek USB WiFi drivers don't build on 6.18 | High | Source from morrownr upstream, who track modern kernels. Do not vendor 2021 copies. |
spidev binding hazard — [P0: mis-described.] altspi is not a catch-all bind: it is an explicit one-line entry in spidev_dt_ids[] (drivers/spi/spidev.c:699, added by fork commit 246984fce). The hazard is still real on 6.18 — an unlisted compatible means spidev never probes ⇒ no /dev/spidev1.0 — but the fix is cleaner than feared. |
Low [P0: was Medium] | Since we author our own DTS (§4.1a), retarget the compatible to one mainline spidev already accepts and delete patch 0005 entirely. Also [P0]: this drives a pi-top hub (brightness/lid), not MiSTer's own I/O board — the original "silent loss of I/O-board brightness control" was the wrong peripheral, which lowers the blast radius considerably. |
[P0] exFAT symlinks (class G, §4.1) — stock's out-of-tree exfat driver supports symlinks on /media/fat; Main_MiSTer resolves them; mainline exfat/vfat cannot. |
RESOLVED — Q1, ADR 0010: drop the driver. 0 symlinks across /media/fat and every /media/usb0..7 on a live stock MiSTer. Risk replaced by a smaller, concrete one below. |
|
| [P0] FAT32 media fail to boot after dropping the exfat driver — mainline exfat cannot mount FAT32, and the rootfs is a file on that partition. | High | New, and invisible to local testing (maintainer's card is 238.7 GB exFAT). The P1.10 initramfs must try exfat then fall back to vfat (A2), and the vfat mount must set utf8=1 or non-ASCII filenames mojibake and Main_MiSTer's stored paths stop resolving. The P1.10 test matrix must include a FAT32 card carrying a non-ASCII filename, or neither defect can ever be caught. |
[P0] CONFIG_BLK_DEV_INITRD is OFF in stock — and CONFIG_INITRAMFS_SOURCE depends on it. |
High | P1.3's own instruction ("port the stock config via olddefconfig") would produce a kernel with no initramfs slot at all, silently removing the mechanism §5 depends on to delete the loop= patch. Now constraint A11; P1.3 must turn it on as an explicit divergence. |
[P0] The Downloader's flash phase cannot report its own failure (no set -e; script ends in touch). |
Medium | Not fixable by us — it is in the shipped updater. P4.8's rollback runbook must assume a reported-successful update may have half-failed. Argues for a serial-console recovery guide. |
[P0] Python 3.9 → 3.14 — Downloader_MiSTer pins 3.9 in its own CI and builds against python3.9-dev. It has never been tested on any interpreter we would ship. |
Medium [P0: was a suspicion (A6), now evidenced] | P3.9 is a real gate, not a formality. Report incompatibilities upstream rather than pinning an EOL Python. |
| DTS gaps (§4.1a) — missing USB, FPGA bridges, RTC bus | High | Carried as 0004-dts-de10nano-MiSTer.patch. Drivers all exist mainline; this is config, not code. |
| Rootfs exceeds the image budget | Medium | Grow linux.img to 512 MiB; audit assumptions about 400 MB. |
| Boot regression from the initramfs | Low | Measurable; budget in §11. |
| Bricking via U-Boot | Critical | Deferred to P5, opt-in, recovery documented and drilled. ADR 0017 shrinks the exposure: P5 builds the same source commit stock already runs, not a mainline port. |
| [ADR 0017] 2017-era U-Boot fails to build under a 2026 toolchain | Medium | Expected and contained: build fixes only in uboot-patches/ (provenance-documented, never behaviour changes); worst case, pin the Arm GNU 10.2-2020.11 toolchain the stock binary used (docs/boot-chain.md §3.2). |
| Community fragmentation / abandonment | High | Be strictly drop-in. Ship a working artifact before making an argument. If nobody will commit to tracking 6.18.y stable for years, do not start. |
That last one is not a joke. A stale fork is worse than no fork, because it splits the
"which linux.img are you on?" support surface across a volunteer community. The entire
value proposition of this project is sustained maintenance — which is why §9 ends by
handing routine bumps to Renovate + CI, reducing the steady-state cost to reviewing green
PRs. If even that is not on offer, the honest move is to publish the patch triage and the
ABI contract as documentation and stop there — that alone would be a real contribution.
If this stalls at P1, it should still have been worth doing. Three deliverables have standalone value and are hard for anyone to object to:
docs/abi-contract.md— the first written description of what MiSTer's userland actually requires of its kernel and rootfs. Nobody has this.docs/patch-provenance.md— every one of the ~60 kernel commits classified, with its upstream status. This is the map nobody has drawn.- A 6.18 LTS kernel tree, built by Buildroot from a pristine tarball, that boots the board.
Publish those. Let the artifact make the argument.