Audience: whoever maintains MiSTer-devel/Linux-Kernel_MiSTer. This is the companion
to the loop= patch carried in
board/mister/de10nano/linux-patches-upstream/0100-init-support-for-init-loop-device.patch,
which is included in the exported 6.18 tree.
Short version. Fork commit
3d95de58f
("Support for init loop device.") does not apply to 6.18, and not because of context drift —
patch fuzz would not save it. Both halves of the code it hooks into were rewritten, and
three of the primitives it is built on no longer exist. Making it work again is a real
forward-port, not a re-anchor. We did that work and it is in the exported tree. Separately,
this Buildroot build does not use it: we boot the same card layout from a stock kernel with
no patch at all, via an initramfs. Both facts are useful to you, for different reasons.
Five independent reasons. Any one of them alone would require code changes.
The patch's loop_setup() is built on them:
if(sys_ioctl(device_fd, LOOP_SET_FD, (long)file_fd) < 0) { ... }
sys_close(file_fd);Init-time work now goes through explicit helpers in fs/init.c, declared in
<linux/init_syscalls.h>: init_mount(), init_mkdir(), init_umount(), and friends.
There is no init_ioctl(), and there cannot be a generic one — an ioctl is
driver-defined, so there is nothing to wrap. Nor is there a back door: vfs_ioctl() is
static to fs/ioctl.c, and do_vfs_ioctl() carries a comment there saying it is "not for
drivers and not intended to be EXPORT_SYMBOL()'d". blkdev_ioctl() is declared only in
block/blk.h, which is block-internal. lo_ioctl() is static.
So LOOP_SET_FD cannot be issued from init/ at all on 6.18 without new plumbing.
The #ifdef CONFIG_BLOCK block inside mount_root() that the patch edits is now the body
of a separate mount_block_root(char *root_device_name), and mount_root() has become a
switch over ROOT_DEV that dispatches to it. 5.15's mount_block_root(name, flags) — the
"mount this device as root, trying each filesystem" primitive — is 6.18's
mount_root_generic(name, pretty_name, flags). The patch calls the old one twice.
Also, root_device_name is no longer a file-global; it is a parameter threaded down from
prepare_namespace().
5.15's prepare_namespace() advanced root_device_name past its /dev/ prefix before use.
6.18's does not, and no equivalent adjustment exists anywhere in the file (grep -rn 'root_device_name += 5' init/ returns nothing). So the name now arrives fully qualified,
and the patch's bind-mount failure message — which prefixes a literal /dev/ — would print
Failed to bind-mount /dev//dev/mmcblk0p1 to /root/media/fat : -2
on the real MiSTer command line. Cosmetic, but it is the kind of thing that survives a "clean" apply and then confuses whoever hits the error path years later.
Since 0fcb70851fbf ("Makefile.extrawarn: turn on missing-prototypes globally") the
exported functions need a visible prototype or they warn. 6.18 has nowhere to put one:
there is no include/linux/loop.h, and no drivers/block/loop.h either — the driver's
private header was folded into loop.c, which reaches straight for <uapi/linux/loop.h>.
This matters more than it looks. The 5.15 patch got LOOP_SET_FD into init/do_mounts.c
via #include <linux/loop.h> resolving to the UAPI header. Any new include/linux/loop.h
shadows that on the include path (LINUXINCLUDE searches include/ before include/uapi/),
so it has to re-include the UAPI header or the ioctl definitions silently disappear.
Worth fixing regardless of version, but 6.18 forces the issue:
- Stack buffer overflow.
sprintf(lname, "/root2/%s", loop_name)intochar lname[32]overflows for anyloop=argument longer than 24 characters. That is reachable from the boot command line, bounded only byCOMMAND_LINE_SIZE. CONFIG_BLK_DEV_LOOP=mdoes not link.loop_max_part()would live in a module that cannot possibly be loaded before the root filesystem is mounted, whileinit/do_mounts.cis always built in. The 5.15 patch links only because MiSTer's own config happens to set=y;allmodconfigwould fail.
This is what the carried patch does. It is offered as a starting point, not as something you have to accept — if you would rather solve it differently, §1 is the part that matters and this section is one worked answer.
| # | Change | Why |
|---|---|---|
| 1 | Export loop_set_backing_fd() from drivers/block/loop.c |
The ioctl cannot be issued from init/ (§1.1). The body is exactly lo_ioctl()'s LOOP_SET_FD case: a zeroed struct loop_config carrying only the backing descriptor, BLK_OPEN_READ|BLK_OPEN_WRITE for the mode. Not passing BLK_OPEN_EXCL keeps loop_configure() on its bd_prepare_to_claim() path, so claim semantics match the userspace route. |
| 2 | Keep the descriptor half in init/ |
loop_configure() fget()s config.fd, and there is no way around that short of splitting loop_configure(). 5.15's m_open() is kept in substance — get_unused_fd_flags() + fd_install() — with its reference leak on the allocation-failure path fixed. |
| 3 | Restore include/linux/loop.h |
Two declarations, re-including <uapi/linux/loop.h> so it is a strict superset of the header it now shadows (§1.4). |
| 4 | Move the hook into mount_block_root(), translate to mount_root_generic() |
Same code at the same point in the boot (§1.2). Drop the literal /dev/ from the failure message (§1.3). |
| 5 | Split the loop= body into mount_loop_root(), with an IS_BUILTIN() stub |
So it compiles out under CONFIG_BLK_DEV_LOOP=m (§1.5). The stub panics with the reason rather than silently falling back to mounting root= — which would try to boot the exFAT data partition as root. |
| 6 | sprintf → kasprintf |
Fixes the overflow (§1.5); slab is up long before prepare_namespace(). |
| 7 | Return real errnos, drop the post-failure LOOP_CLR_FD |
loop_configure() unwinds its own partial state and leaves lo_state == Lo_unbound, so there is nothing to clear. |
One thing that did not need changing, checked rather than assumed: /dev/loop8's minor
is still (max_part + 1) * 8, because loop_add() still assigns
disk->first_minor = i << part_shift with part_shift derived from max_part. Your
loop_max_part() export carries over verbatim.
/dev/loop8 is one past CONFIG_BLK_DEV_LOOP_MIN_COUNT's default of 8 devices, so the
driver has not created it. The filp_open() of the node is what brings it into existence:
blkdev_get_no_open() finds no inode, calls blk_request_module(), and that reaches
loop_probe().
In 6.18 that fallthrough is conditional on CONFIG_BLOCK_LEGACY_AUTOLOAD, whose help
text calls it a historic feature and which pr_warn_ratelimited()s that it "will be
removed". It is default y, so the boot works today. When that symbol goes, loop= stops
working, and it would surface as an unexplained -ENXIO. The carried patch names the symbol
in its open-failure path so that day produces an actionable message rather than a puzzle.
If you want to be ahead of it, raising CONFIG_BLK_DEV_LOOP_MIN_COUNT to 9 (or pointing
loop= at a lower-numbered device) removes the dependency entirely.
Compile-tested only. It has never been booted. A full ARCH=arm vmlinux links
warning-free with MiSTer's board config and carries __ksymtab entries for both new
symbols; init/do_mounts.o and drivers/block/loop.o also build warning-free under
CONFIG_BLK_DEV_LOOP=m. That is the ceiling of what this repo can prove, because the image
we build deliberately does not apply the patch — there is nothing here that exercises the
code at runtime.
That limitation is not theoretical. An earlier revision of this port was built on
fs/init.c's init_dup(), which looks like the sanctioned helper and is not: despite the
name it does not return a descriptor. It allocates one, fd_install()s a reference of its
own, and returns 0. It exists for console_on_rootfs(), whose callers only check for
failure. That version compiled cleanly and would have bound fd 0 — which at that point
in the boot is /dev/console, since console_on_rootfs() runs in kernel_init_freeable()
before prepare_namespace(). loop_configure() would have fget()ed the console,
loop_validate_file() would have rejected the character device with -EINVAL, loop8 would
have stayed unbound, and mounting it as root would have panicked on every boot. It was
caught by review, not by the compiler.
So: please boot it before trusting it.
Buildroot_MiSTer boots the identical card layout on a stock kernel with no patch, using
an initramfs. Recorded in
docs/decisions/0002-initramfs.md; the disposition of your
commit is tracked as carried-upstream-only in
docs/kernel-recon/reconciliation.md — carried for the
exported tree, deliberately not applied to our image. It is explicitly not a drop.
We do not touch U-Boot. uboot.img ships byte-identical, so the command line is
unchanged, loop=linux/linux.img and all:
console=ttyS0,115200 loglevel=4 loop.max_part=8 mem=511M memmap=513M$511M \
root=/dev/mmcblk0p1 loop=linux/linux.img ro rootwait
The only thing that changes is who consumes loop=. A stock kernel parses it via
__setup("loop=", ...). Ours never registers that handler, so the token reaches userspace
untouched on /proc/cmdline, and our /init parses it itself.
| Your kernel does | We do, with stock functionality |
|---|---|
Mount root= as exFAT on /root2 |
mount -t exfat (mainline driver), falling back to mount -t vfat |
create_dev("/dev/loop8") + LOOP_SET_FD open-coded |
BusyBox losetup -f then losetup <dev> <img> — real userspace losetup(8), never a hardcoded loop8 |
mount_block_root("/dev/loop8", flags) |
mount -t ext4 <loopdev> /newroot, autodetect fallback |
init_mount("/root2", "/root/media/fat", MS_BIND) |
mount -o move /mnt/fat /newroot/media/fat |
root_mountflags |= MS_NOATIME|MS_NODIRATIME globally |
the same flags as explicit -o mount options, on the mounts that want them |
Then exec switch_root /newroot /sbin/init.
- A small percentage of zImage. The cpio is 424,448 bytes uncompressed, and the
zImage_dtbit lands in measures 8,994,445 bytes — 7,782,771 bytes (7.42 MiB) of headroom against U-Boot's 16 MiB load budget, whichscripts/check-zimage-dtb.shasserts on every build. (mem=511Mis irrelevant here — it constrains the FPGA mailbox, not this.)
Was 258,560 uncompressed / "~8.05 MiB headroom" before ADR 0026 addedfsck.exfat(116,564 of the current cpio) for the on-demand exFAT repair — so that feature cost about 0.25 MiB of headroom. - An extra boot stage. Predicted at a few hundred milliseconds. Unmeasured.
root=PARTUUID=regression. BusyBoxfindfshandlesUUID=/LABEL=only; yourname_to_dev_t()handledPARTUUID=. Narrow, but real.- Bounded
rootwait. Yours is infinite; ours defaults to 30s and then drops to a rescue shell. More debuggable, but it is a behaviour change.
- No
init/do_mounts.cpatch to forward-port. §1 is the argument for this: that file gets rewritten, and every rewrite is a re-port on a boot-critical path. - Testable without hardware.
scripts/test-initramfs.shboots the real cpio under QEMU across eight cases (fat32, exfat, the on-demand exFAT repair, symlinks,LABEL=, non-ASCII filenames, missing image,rootwaittimeout) and asserts both successfulswitch_rootand correct failure behaviour. Nothing about the in-kernel path was ever testable that way. - Failures are recoverable. Every error path in the kernel version is
pr_emerg()then carry on, ending in a panic. Ours prints a diagnostic banner — parsed cmdline,/proc/partitions,/proc/filesystems,/proc/mounts, last 25 dmesg lines — and drops to a respawning serial shell. The QEMU harness machine-checks the absence ofKernel panicin the negative cases.
Verified under QEMU and on real hardware. scripts/test-initramfs.sh's cases run
under QEMU — that is what covers the negative paths (missing image, rootwait timeout,
rescue shell) which no board test reaches. The positive path is not emulator-only: P1.13
booted this initramfs on a Terasic DE10-Nano on 2026-07-12
(docs/testlogs/p1-first-boot.md), P2.9 reached the MiSTer menu on our own linux.img
the same day (docs/testlogs/p2-menu.md), and a DE10-Nano flashed from our sdcard.img
runs the current image today. What remains unmeasured is the extra boot stage's cost in
milliseconds (see "What it costs").
Nothing, forcefully. Both mechanisms are in the exported tree's history and you can take either view:
- Keep
loop=in-kernel. The carried patch is a working starting point, needs a boot test, and §2's table is the maintenance surface you are signing up for on each rebase. - Move to an initramfs later. §3 is a worked example that keeps your card layout, your
U-Boot, and your command line exactly as they are. The cost is one extra build stage; the
benefit is that
init/do_mounts.cstops being your problem.
The reason the patch is in the exported tree at all is that a 6.18 branch without it would not boot any stock MiSTer — deleting it to keep our two trees identical would have been the wrong trade.