Skip to content

Building & Installing a Kernel

Amix has a monolithic SVR4 kernel, and stock Amix has no loadable modules ✅ (one third-party exception) — every driver is statically linked in. To add a driver (or change anything kernel-level), you drop an object file into the /usr/sys tree, register it in the kernel switch tables, relink the kernel with make, copy the result to /stand, and write it into the boot partition with make bootpart. Then reboot.

The short version, on a live 2.1 system, is:

# 1. add your driver .o to its subdir Makefile, edit master.d/kernel.c (see below)
# 2. clean stale objects, then relink
rm -f amiga/config/unix.o master.d/exp unix    # remove stale objects
make                                            # -> relocunix
# 3. install the new kernel and write the boot partition
cp relocunix /stand
make bootpart KERNEL=relocunix
# 4. create the device node and reboot
mknod /dev/<name> c <major> <minor>
shutdown -i6

This page is the mechanical build-and-install procedure. For what a driver is and the table schema you edit, read the driver model overview first. For a complete real patch set used as the running example here, see the VA2000 framebuffer case study.

The /usr/sys tree

The kernel is not shipped as one big source tree you compile from scratch. It ships as precompiled object libraries under /usr/sys ✅, and you relink them with your additions. Some .o files come with source, others are object-only ✅ — you cannot rebuild the object-only ones, only link against them.

What is in source and meant to be edited:

  • master.d/kernel.c — the kernel configuration file holding the device switch tables (cdevsw[] / bdevsw[]), the interrupt tables (int2_tbl[], plus a level-6 table), and the boot-time init table (init_tbl[] / io_init[]). Provided in source ✅. (Ditto paper, "Adding a device driver.")
  • per-subdirectory Makefiles — e.g. amiga/driver/Makefile, that link the object files in each subtree into the kernel. You add your driver's .o to the relevant one. ✅

Note: The exact subdirectory names below come from the modern repos (the VA2000 patch set), not the 1990 paper, so treat the layout as ✅ for 2.1 systems and the general procedure as the paper's ✅ spec.

Relevant subtrees seen in the VA2000 patch set (Amix 2.1) ✅:

Path Role
master.d/kernel.c switch tables + extern decls + init table
amiga/driver/Makefile links driver objects (add your .o here)
amiga/console/scrdev.c, c0.c, screen.c console / screen-device sources (RTG example)
amiga/config/unix.o a generated object — delete before relinking (see below)
master.d/exp a generated symbol/export file — delete before relinking
unix the previous link output — delete before relinking
/stand where the bootable kernel image is staged

Step 1 — add your driver object to a subdir Makefile

Compile your driver natively (a single-file char driver builds with the SVR4 cc, e.g. cc va2000.c ✅ for the VA2000), then add the resulting .o to the Makefile in the subtree it lives in. For the VA2000 the install script patches amiga/driver/Makefile to add va2000.o ✅.

Keep the driver source in the tree too, so the object can be rebuilt later — the paper recommends placing the driver .o and ideally its source in a subdir under /usr/sys and adding it to that directory's makefile ✅.

STREAMS drivers like Hydra build natively too — make in the driver dir, then make force to relink (elf2brel converts the kernel to boot format as part of that); see the Hydra network-driver case study and writing a STREAMS driver. The rest of this page covers the native path.

Step 2 — edit master.d/kernel.c (the switch tables)

Register the driver in the table(s) it belongs to. The switch tables are indexed by major number ✅ — the slot you put your entry points in is the device's major number.

What you typically add, in order (the VA2000 patch does exactly this) ✅:

  1. extern declarations for your entry points, near the top.
  2. A cdevsw[] slot (character driver) — for the VA2000, slot 68 ✅ — wiring open/close/read/write/ioctl/mmap/poll, with nodev/notty/nostr/nullflag for the entry points you don't implement.
  3. A bdevsw[] slot instead, if it's a block driver (core entry strategy()).
  4. An interrupt-table entry in int2_tbl[] if the device raises a level-2 autovector interrupt (e.g. alongside parintr, a2090intr, a2091intr) ✅.
  5. An init-table entry in init_tbl[] / io_init[] if the driver needs one-shot boot-time init — for the VA2000, va2000init is added to io_init[] ✅.

Warning (ordering): the extern declarations must precede io_init[] in kernel.c, or the build fails ✅ (VA2000 gotcha).

Warning (pre-POSIX shell): if you script these edits, remember Amix /bin/sh is pre-POSIX — no $(...) command substitution and no grep -q ✅. Use backticks and redirect to /dev/null instead. This bites anyone porting a modern install script.

See the driver model overview for the full cdevsw/bdevsw struct layout and the meaning of nodev/notty/nostr/nullflag.

Step 3 — clean stale objects, then make

Always remove the generated artifacts of the previous link before relinking, or you can link a stale kernel ✅:

rm -f amiga/config/unix.o master.d/exp unix

Then relink:

make

A successful make produces the bootable kernel image. The name has changed across versions 🟡:

  • The 1990 Ditto paper calls the kernel image rdbunix ✅.
  • Modern 2.1 systems and the repos call it relocunix ✅ — a historical rename.

So on a 2.1 system you are producing relocunix. (The repos run make install and treat the output as relocunix; the VA2000 script does make install → relocunix ✅.) If you are on an earlier release, verify which name your tree emits 🟡 — see the open question on the kernel-image name.

Step 4 — install the kernel and write the boot partition

The kernel that actually boots lives in the boot/bootstrap partition (a ~2 MB partition the installer creates, BOOTSIZE=2 MB ✅), not just on the filesystem. Two steps:

cp relocunix /stand
make bootpart KERNEL=relocunix
  • cp relocunix /stand stages the new image where the boot tooling expects it ✅.
  • make bootpart KERNEL=relocunix writes the kernel into the boot partition so the Superkickstart bootstrap can load it on next power-on ✅.

This matches the install-time flow: the installer builds/patches the kernel, then runs make bootpart KERNEL=relocunix to write the boot partition ✅ (reconstructed from the root.adf install scripts). For how the bootstrap then finds and decompresses that kernel, see the boot process.

Always keep the old /unix as a fallback ✅. The paper's procedure explicitly says to retain the previous kernel so you can boot it if your new one panics. Don't overwrite your only known-good kernel. (Note the distinction: /unix is the on-disk kernel file; the bootable copy is what make bootpart writes — preserve a working one of each.)

Step 5 — create the device node

A driver does nothing until there's a /dev node with the matching major/minor. The kernel keys only on the numbers, not the name ✅:

mknod /dev/<name> c <major> <minor>     # character device
mknod /dev/<name> b <major> <minor>     # block device

Concrete, from the VA2000 patch set ✅:

mknod /dev/va2000 c 68 0

c = character, b = block; the major must equal the cdevsw[]/bdevsw[] slot you edited in Step 2. (Reference major numbers: the SCSI hard-disk block driver is major 18, the parallel port char driver is major 21, the floppy block driver is major 16 ✅; see the device list.)

Step 6 — reboot

shutdown -i6

shutdown -i6 brings the system to run level 6 (reboot) ✅. On the way back up the Superkickstart ROM loads the kernel you wrote into the boot partition. If it panics, reboot and select your retained fallback kernel.

Worked example — the VA2000 6-file patch set ✅

The asokero/va2000-amix char framebuffer driver is the cleanest concrete instance of everything above. Its install script patches six kernel files and then runs the build/install/reboot sequence:

# File patched Change
1 amiga/driver/Makefile add va2000.o
2 amiga/console/scrdev.c RTG screen type
3 amiga/console/c0.c RTG screen type
4 amiga/console/screen.c RTG screen type
5 master.d/kernel.c extern decls + cdevsw[] slot 68 + va2000init in io_init[]
6 (node, not a file) mknod /dev/va2000 c 68 0

Then, end to end ✅:

# (build the driver object first)
cc va2000.c

# clean + relink
rm -f amiga/config/unix.o master.d/exp unix
make install                       # -> relocunix

# create the device node
mknod /dev/va2000 c 68 0

# write the boot partition and reboot
cp relocunix /stand
make bootpart KERNEL=relocunix
shutdown -i6

The driver itself uses autocon() for Zorro II board discovery, uiomove(), and copyin()/copyout() ✅. The full annotated walk-through is in the VA2000 case study; for adding the same kind of driver to an install floppy instead of a live disk, see adding drivers to a boot disk.

Known pitfalls

  • Stale objects. Forgetting rm -f amiga/config/unix.o master.d/exp unix can relink an old kernel ✅.
  • extern ordering. Declarations must come before io_init[] in kernel.c ✅.
  • Pre-POSIX /bin/sh. No $(...), no grep -q in build/install scripts ✅.
  • Kernel-image name. rdbunix (1990 paper) vs relocunix (2.1 / repos) — a historical rename; verify which your tree emits 🟡.
  • No fallback. If you overwrite your only working kernel and the new one panics, you have to reinstall. Keep the old /unix ✅.
  • Object-only files. You can't rebuild the object-only .os in /usr/sys; you can only link against them ✅. Anything that needs their source can't be changed by the community.
  • Boot-partition vs filesystem. A kernel sitting on the filesystem is not enough; it must be written into the boot partition with make bootpart to actually boot ✅.

The "D245 boot-breaker" — an emulator-only kernel-image corruption

This is a load-bearing gotcha for anyone relinking the Amix kernel, not just A4091 work. It was first-party reproduced locally on Amix 2.1c under Amiberry. If you ever see a kernel Guru at boot with D245 4C41, this section is the answer.

What it is not (a corrected misconception) 🔴

🔴 Kernels that grew past a certain size Guru'd at boot with D245 4C41. This is not a bug in the SCSI driver's completion poll loop, and not the bootstrap relocator misbehaving. The constant 0xD2454C41 decodes as "RELA" | AT_DeadEnd — it is the bootstrap relocator's own Alert() in amiga/boot/rel.c, fired when rel() is handed a corrupt kernel image to relocate. So D245 is a symptom of a bad kernel binary, not a bug in the relocator or in any driver. ✅

The longword breaks down as: 0x52454C41 = ASCII "RELA"; setting bit 31 (AT_DeadEnd, the "unrecoverable" flag exec ORs into a deadly alert) yields 0xD2454C41, displayed as D245 4C41 in the Guru requester. ✅

What it actually is — an emulator MMU defect, not ld ✅ (root-caused 2026-07-26)

🔴 Not ld. The corruption is not introduced when ld writes the linked kernel to disk, in any variant: ld is innocent and computes correct output every time. The corruption is injected by the emulator, inside the guest kernel's copy loop, while the guest is demand-paging.

✅ The mechanism. On the 68030, a bus-fault format-$B stack frame packs two unrelated fields into the 16-bit word at frame offset 0x34: mmu030_state[2] in the low byte and the write-back status wb3_status in the high byte. The emulator's RTE extracted wb3_status correctly but restored the whole word into mmu030_state[2]. When the RTE's own retry access faulted again — routine under heavy paging, rare otherwise — the next frame was built carrying the stale wb3_status, so an (An)+ post-increment side effect that had already been undone was undone a second time: a silent 4-byte rewind of an address register, mid-copy, with no exception raised.

Every observed event landed at one guest PC: the Amix kernel's MOVES.L (A0)+ copyin loop. So what actually happened is that the kernel's copy of ld's write() buffer was rewound — which is why the damage looks like a linker bug, why cp/dd/FTP of the same bytes are always clean (no paging pressure, no faults), and why real hardware never shows it.

✅ What the damage looks like (this supersedes the "~8 KB block shifted by 8 bytes" description, which was one small-delta member of a wider family): every damaged region is a byte-exact displaced copy of content from earlier in the same file. Per-region displacements measured 12–9012 bytes, always a multiple of 4, never a multiple of the guest page size (2048 B), and constant across page boundaries — which is precisely what excludes every page-granular explanation (lost dirty page, wrong swap slot, stale frame, recycled buffer). Damage begins at file offset ≡ 0x19f–0x1a4 (mod 2048) and ends on an 8192 boundary.

✅ The rate is configuration-dependent, and quoting one number is a mistake. At a3000mem_size=8 it reproduces at 85% of relinks; at 16 MB the same measurement pooled 0/354 (95% upper bound 0.84%). Measurement itself suppresses it: adding I/O around each link drove the observed rate 85% → 45% → 35% → 28%. Always state the capture mode with the rate.

✅ Fixed. Masking the restore to the field's real 8-bit width (both RTE variants) takes the 8 MB corruption rate from 55–59% on matched controls to 0/39, with the fault/paging traffic unchanged — i.e. it removes the damage, not the workload. The defect was present in upstream WinUAE as well as Amiberry; it was reported upstream and the maintainer's own fix (masking the local immediately after the two halves are separated) compiles to byte-identical code and measured 40/40 clean.

✅ What was ruled out along the way, each by measurement rather than argument: the emulator's disk stack (every guest write replayed byte-exactly and every logged read returned disk truth, at both 8 and 16 MB), ld's inputs (byte-identical between a clean and a corrupt round), and the MMU's own fault-resume machinery and descriptor M-bit handling.

The fix — build until the checksum is stable ✅

✅ A clean ld output is byte-deterministic; each corruption is random and unique. Therefore a checksum (sum -r) that recurs is the deterministic clean kernel. tools/build-clean-kernel.sh (runs on the Amix box) relinks (link-only, no install) until a sum -r value repeats, then confirms with tools/checkunix.c, leaving a verified-clean relocunix:

sh /root/build-clean-kernel.sh    # exit 0 = clean relocunix ready; 1 = could not stabilize in 25 builds

Never make install an unverified kernel. A corrupt kernel written to the boot partition will brick the boot disk (see the safety rule on the boot process page). Always clean-gate first, and install onto a backup/throwaway disk. ✅

Two complementary detectors exist:

Detector What it checks Trade-off
tools/checkunix.c native big-endian .symtab integrity (flags out-of-range st_shndx) fast, runs on the box, but symtab-only — misses .rela/.text shifts ✅
tools/relsim.py host-side reimplementation of the boot relocator rel() checks .symtab and relocation records — a kernel-record oracle ✅

Scope correction (2026-07-16) ✅: relsim.py (and the native reltest) validate the boot relocator's source semantics against the kernel's records — they are kernel-record oracles, NOT will-this-disk-boot oracles. The on-disk boot2 loader is a free variable outside their scope: a relsim-green kernel still D245s under a name-based boot2 (see the two boot2 lineages). Disk-level assurance needs a boot-chain check: the build pipeline laminates the proven flags-based boot2 into /stand/boot2.boot before the bootpart rebuild and fail-closes on bootchain-verify.py (RDB walk → UNI\0 slice → boot1@+0x000, IBLK@+0x400, boot2@+0x600 sha check, IBLK@+0x2600, kernel ELF@+0x2800; ib_chksum = folded 16-bit SVR4 sum). A kernel gated this way booted 5/5 on a real A4000 + Z3660 (2026-07-15) ✅.

Measured rate (2026-07-20) ✅: a fixed-N measurement on a RAW (non-VHD) disk image put the emulated relink-corruption rate at 85% (17/20 rounds) on the unfixed emulator at 8 MB — refuting the hypothesis that dynamic-VHD block-remapping explained it. Every linked-but-corrupt round kept nm -h -u empty: the ~8 KB block-shift never perturbs the symbol table, so sum-recurrence is the load-bearing arm of the gate for SCSI/combined kernels (the nm-empty arm covers the larger cdfs kernel's distinct silent-symbol-drop mode). One round in 20 was the separate intermittent ld "Unresolved Symbol" nolink mode, which the gate's retry already handles ✅. (That 85% is the 8 MB figure; see the configuration dependence above.)

Oracle coverage, scored against 21 real captured corruptions (2026-07-25) ✅. This is the number that matters when choosing a gate, and it is uncomfortable:

Oracle Caught
nm -h -u (undefined symbols) 0 / 21
checkunix (symtab st_shndx) 7 / 21
relsim / relocation-record analysis 15 / 21
byte-diff against a known-good link 21 / 21

A symbolic two-arm bar (nm + checkunix) passes 14 of those 21 — it would have declared two-thirds of them STABLE. Adding a relocation-record arm takes that to 6. It does not take it to zero, because a pure .text-content displacement is invisible to every symbolic check. Byte-diff against a reference link is the only complete oracle (and where a gate can use it, sum -r recurrence also scores 21/21 — every corrupt link had a unique checksum — which is why that arm stays load-bearing on the kernels where it converges). Read a green symbolic gate as "not corrupt in any way visible from here", never as "byte-correct".

Note also that relsim-class tools must fail closed: a kernel whose section-header table is damaged (the dominant shape — the table is the last ~400 bytes of relocunix and gets overwritten wholesale) can crash the analyser rather than being reported as corrupt.

Who is exposed to a cross-toolchain bug — the build-model boundary ✅

When the cross toolchain's tdivs divide-with-remainder mis-assembly was found (see toolchain), the blast radius was bounded by the build model, and the boundary is worth recording ✅:

  • The kernel objects and all native drivers never carried the bug — the harness is a source-push + compile-on-box model; the box's own SGS as assembles them.
  • Only host cross-built artifacts were exposed: the SVR4 pkg engine, the cross libgcc.a that a cdfs-carrying kernel links for its 64-bit soft-arithmetic (__udivdi3/__umoddi3/__lshrdi3), and the cdfs kernel exp relocatable.
  • A plain SCSI / SCSI+net kernel links zero cross artifacts and was end-to-end clean.

The practical consequence closed a long-open metal mystery: the kernel's __udivdi3 returning an internal normalization intermediate on real hardware (garbage st_blocks from cdfs) was the mis-assembled 64-bit-dividend form inside the old cross libgcc.a. After the toolchain fix and a libgcc/exp rebuild + kernel relink, the same machine + same disc read every value correctly, and a userland exerciser statically carrying the rebuilt __udivdi3 passed on metal with the exact dividends that used to fail (2026-07-21) ✅. Standing style rule stays: prefer shifts to 64-bit division in Amix kernel code — now as a speed/robustness preference, not a correctness workaround ✅.

Build checkunix natively (cc -O -o checkunix checkunix.c) and run it on the box; run relsim.py on the host against a pulled kernel ELF:

# on the host, against a kernel pulled off the box:
python3 tools/relsim.py relocunix

Build-system corollary — make does not reliably recompile a changed .c ✅

✅ Plain cd /usr/sys; make does not reliably pick up an edited source file: the per-subsystem exp prelink chain has incomplete dependencies, so a changed .c (or a changed /stand/CONFIG) is silently ignored. After editing, for example, amiga/kernel/support.c, you must rm the stale .o and the subsystem exp and amiga/exp before make:

cd /usr/sys
rm amiga/kernel/support.o amiga/kernel/exp amiga/exp     # stale .o + subsystem exp + top-level exp
make

Then confirm the change took effect by the kernel sum changing — if sum -r relocunix is unchanged, your edit was not compiled in. ✅

Two more SVR4-box gotchas that bite build scripts ✅:

  • /tmp is wiped on reboot — keep build scripts and saved checksums in /root, not /tmp.
  • SVR4 grep has no \| alternation — use separate grep invocations instead of one alternation pattern. (This is in addition to the pre-POSIX /bin/sh limits noted above.)

The A3000 onboard-SCSI DMA bounce patch — nearly every kernel needs it ✅

If your kernel boots by reading its root disk through the A3000 onboard SCSI, build it with the a3091.c chip-mem DMA bounce patch — whatever else is in it. ✅ The failure it cures has been measured on the emulated A3000; on a real A3000 the patch is a safe over-approximation 🟡 (see the reach refutation below). This is not an A4091 requirement and not a cdfs requirement: a3091.c is the A3000 onboard SCSI driver (super-DMAC + WD33C93 at 0xDD0000), it is in the stock amiga/alien Makefile, and it links into every kernel. It lives under amix-a4091/src/kernel-patches/ only because that project did the work ✅.

On the emulated A3000, /etc/ddsize on a disk behind this driver hung for good, after a3091: 0x1 0 0x0 ✅ (ddsize can hang).

a3091.c.patch (commit e70c1d7; patch -p0 on amiga/alien/a3091.c) adds a high-address DMA bounce: any buffer at or above 0x1000000 is copied through a per-unit chip-RAM buffer, which is always DMA-reachable. Stock startdma() sets device->sac = cp->addr — the caller's 32-bit buffer address — directly, with no bounce, unlike the A2091-card sibling a2091.c, which already bounces any buffer ≥ 0x1000000 (16 MB) through AllocMem(cp->tc, MEMF_CHIP) (copy-out before a write, copy-back after a read, freed in stopdma). The patch ports that same chip-mem bounce into a3091.c ✅.

Who is exposed (the build policy — carry the patch in every kernel — is ✅):

Machine Roots through Needs the patch?
Emulated A3000 rooting on c6d0s1, card 0 A3000 onboard SCSI Yes ✅ (measured) — regardless of which add-on HBA the kernel carries
Real A3000 rooting on c6d0s1, card 0 A3000 onboard SCSI Shipped as a safe over-approximation 🟡 — no failure without it has been seen on real hardware
A3000 + A4091 (A4091 sorts to card 1) A3000 onboard SCSI As the two rows above
A4000 + Z3660 rooting on the piscsi mailbox the Z3660 No — a3091 is linked but never carries the root read

So a kernel built without the patch can boot fine on a Z3660-rooted A4000 and still fail on an emulated A3000 that roots through its onboard SCSI ✅.

Build-harness note. A patch keyed to a driver repo is only applied when that repo is part of the build. This one belongs in every kernel that roots through the A3000 onboard SCSI (it is boot-critical on the emulated A3000), whichever driver repos it carries. If your harness selects patches by repo, mark this one unconditional and fail closed when its file is missing ✅.

The s5mountroot VOP_OPEN error 6 panic — how to read it ✅

Without the bounce patch, a kernel that roots through the A3000 can fail like this ✅:

s5mountroot VOP_OPEN error 6
WARNING: nfs_mountroot called
PANIC: vfs_mountroot: cannot mount root: errno 30

Read the first line, not the last ✅. rootfstype is the empty string in every kernel here, so vfs_mountroot iterates vfssw[1 .. nfstype-1], calls each row's mountroot op, stops at the first that returns 0, and prints the last return value it saw. So:

  • errno 30 is not diagnostic. It is whatever the last vfssw row returned — with cdfs linked that row is cdfs, and a read-only filesystem declining a root mount with EROFS(30) is unremarkable. On a kernel without cdfs the same panic prints whatever nfs_mountroot returned. Don't chase errno 30 ✅.
  • s5mountroot VOP_OPEN error 6 is the whole story. s5 is row 1, tried first. error 6 is ENXIO from opening the root block device, before any filesystem question is asked — so ufs (row 2, the actual root type) fails identically one line later, silently, and the walk runs off the end. A healthy boot prints neither line ✅.

The mechanism ✅. ddopen() (amiga/alien/dd.c) has two ENXIO sources — sdopen() (no controller on that card) and sdpartition() (the RDB/PART walk). sdpartition() (amiga/alien/sdpart.c) reads the RigidDiskBlock into a file-static 512-byte buffer (static union block block;) and dd.c's startio() hands the SCSI layer vtop() of that kernel static as the DMA destination. With stock a3091.c the super-DMAC is pointed straight at it, and in the failing kernels the transfer completes without an error and the buffer reads back all-zeros — no RDSK, so getrdb() walks all 16 blocks and returns ENXIO.

Savestate-diff corroboration ✅ (independent, 2026-07-10): comparing an Amiberry savestate of a booting kernel against a panicking one, the same RDB buffer held a valid RDSK/PART block named UNIX_Root in the booting kernel and was all-zeros in the panicking one, while queue[0].f (the registered card's dispatch function) was populated in both — so the card is registered and autoconfig is fine; the DMA read itself is what fails.

It is not a size threshold — measured ✅

The tempting story is that cdfs (~64 KB) pushes the kernel past a size limit. It does not. Section sizes read out of four linked kernels, with the load-image offset of sdpart.c's block ✅:

kernel drivers / patches loaded image block at boots?
64119 z3660scsi, stock a3091 1 016 636 0x0EB924 yes
56225 z3660scsi + cdfs, stock a3091 1 087 259 0x0FCD04 PANIC
01159 a4091 + cdfs, patched a3091 1 151 887 0x0FCD78 yes
55619 z3660scsi + cdfs, patched a3091, stock scsi.c 1 087 911 0x0FCF90 yes

Read the last row against the second: same builder, same drivers, same cdfs object, a loaded image 652 bytes larger — the only substantive difference is that a3091.c bounces — and it boots. Both booting cdfs kernels also put the RDB buffer at a higher address than the panicking one.

So the failure is not a size threshold, and not "the buffer moved up" as such. It is that with a direct DMA the buffer's address matters at all ✅. With the bounce it does not, at any address — which is why every patched kernel boots from a higher buffer address than the unpatched one that fails.

The image is not a variable either: a 2×2 over two different base images and two kernels reproduces the panic text and its backtrace addresses exactly in both panicking cells, and a host-side diff of the two images' whole /usr/sys is byte-identical except amiga/alien/ ✅.

Unlike the D245 boot-breaker (a random emulator-injected corruption, covered above), this failure is deterministic: the same kernel fails the same way every time.

VOP_OPEN error 6 has two known causes on an A3000 ✅. Either no card 0 is registered — the stock kernel's load-address guess skipped the internal SCSI (quirks §57; queue[0].f is empty) — or this DMA failure: card registered, queue[0].f populated, RDB buffer all zeros. The savestate diff above is what tells them apart.

Distinct from the error-5 panic. The s5mountroot VOP_OPEN error 5 (EIO) seen on an A4091-only machine was a different fault — the "phantom A3000" device-dispatch bug, where the root read went to a non-existent internal SCSI — see the boot process and the autocon() phantom-A3000 special case. Don't conflate them.

"The super-DMAC cannot reach RAM at or above 0x1000000" is refuted ✅ (corrected 2026-10-02). The ≥ 0x1000000 (16 MB) threshold is a2091's proven-safe cutoff, never a measured a3091 boundary, and the evidence on this page already contradicts a reach limit: the stock-a3091 kernel 64119 in the table reads its RDB into a buffer loaded above 0x07000000 and boots, and the bench the failure was seen on is an emulator whose A3000 super-DMAC model is a full 32-bit master — Amiberry's src/a2091.cpp initialises it with the 24-bit DMA mask off and no option to turn it on (dma24bit exists only for the add-on cards) ✅. Real hardware agrees ✅: the public 68040/68060 port (asokero/amix-040-060-port, STATUS.md) runs on an A3000 whose accelerator RAM sits at 0x08000000, loads the kernel there (load base 0x08000000, e.g. its 2026-09-06 Zorro III acceptance and the 68060 rows of 2026-09-28) and drives the A3000 SCSI with the A3091/SDMAC driver; its 2026-08-30 68060 record (docs/REALHW-ISSUE54-STAGED-260830-06.md) prints the super-DMAC's address counter at sac=96D99F0 — about 150 MB up — with the root disk on the same controller still answering. Its "DMA coherence (A3091 / SDMAC)" row is hardware-verified with cache-maintenance hooks, and the chip-RAM bounce in its documents belongs to the A2091, not the A3091. So the super-DMAC reaches RAM far above 16 MB on a real A3000, and the bench failure was real but was not an address-reach failure. What it was is open 🔴: the leading hypothesis is a stale data cache — the CPU reading cached contents of the static buffer instead of what the DMA put in memory — which the chip-RAM bounce cures as a side effect, because the copy comes from a different, freshly written buffer. The port's need for explicit cache maintenance around SDMAC transfers on the 68040/68060 is consistent with that family 🟡, but the savestate diff (buffer all-zeros in memory) does not obviously fit a cache-only story for the 68030 bench, so the specific cause stays 🔴 until a cache-flush experiment decides it. The bounce remains the shipped fix, as a safe over-approximation.

The separate GSIO scsi.c.patch — userspace only ✅

amix-a4091's scsi.c.patch is a different thing and is not a boot requirement: it enlarges the userspace /dev/scsi GSIO bounce iobuf to 64 KB ✅. The GSIO path bounces every transfer through that iobuf, which is why userspace /dev/scsi reads can be multi-sector even though the in-kernel SCSI path cannot (see the DMA gotcha below). The in-kernel cdfs read path goes through sdqueue and does not need it ✅. A kernel that boots without scsi.c.patch but with the bounce patch has been built and booted, which is what isolates the two ✅.

⚠️ scsi.c.patch plus a patched a3091 is a latent userland-reachable panic. With the 64 KB iobuf, gsioctl() accepts transfers up to 65 536 bytes; on a bench box all Fast RAM sits at 0x07000000, i.e. above the patch's >= 0x1000000 bounce threshold, and a transfer larger than BOUNCESZ (one 4 KB page) reaches panic("a3091: DMA too big"). The patch's own comment assumes GSIO "bounces through a low kernel iobuf that never trips the >=0x1000000 test", which is not true on this memory map ✅ (the code facts) / 🟡 (the reachable panic — reasoned from source and the bench memory map, not yet reproduced).

Two kernel gotchas that bite any module (found porting cdfs)

Both were hit while porting cdfs, but both are general — any kernel module doing the same thing is exposed ✅.

Gotcha: the kernel's bzero under-clears a region larger than ~1 KB ✅

Zeroing a buffer larger than ~1 KB with the SVR4 kernel's bzero — or with a memset that delegates its c == 0 case to bzero — leaves the tail uncleared (stale garbage) on this m68k build ✅. It bit cdfs twice from a single ~1.1 KB struct clear: an uncleared has_child_link clobbered a filesystem node's extent (empty/garbage root), and an uncleared is_relocated made the ISO directory walk skip every child (empty directory). Fix: a memset that clears with its own byte loop (amix-cdfs ad09035). Any module relying on bzero/memset to clear a > 1 KB buffer is exposed — audit for it. (Working theory: a size/alignment/int-truncation bug in bzero around ~1 KB.)

Gotcha: in-kernel SCSI DMA corrupts transfers larger than one 2048-byte block 🟡

On the A3000's internal SCSI (WD33C93 + Super DMAC, the a3091 driver) — most likely on the emulated A3000, which is where the CD work ran 🟡 — an in-kernel SCSI transfer (struct sdcom + sdqueue()) of more than one CD sector (2048 B) comes back as kernel text / garbage and can wedge the box; single-block (2048 B) transfers are reliable (PVD, mount reads, and RDB reads are all single-block and fine) ✅. cdfs's block-cache read-ahead issued one 16 KB (8-sector) DMA → garbage → empty/garbage directory + hang; the fix caps the media transfer to one sector and lets the chunk loop split multi-sector requests (amix-cdfs a5c6915). It is not an A4091 limit (the 53C710 is a full 32-bit bus master). It may share a cause with the a3091 boot failure above (whose "high-address reach" explanation is refuted; the leading hypothesis there is cache coherence) 🔴. Caveat: the same day a separate reentrancy bug in the in-kernel SCSI issue path, which also scrambled in-flight transfers, was fixed (amix-cdfs 2172772), so how much of the multi-sector failure was that bug is not established 🟡.

The userspace /dev/scsi GSIO path is not affected — it bounces through the 64 KB iobuf (scsi.c.patch) and does do multi-sector reads; this limit is specific to the in-kernel sdqueue path ✅. 🔴 Open: the real multi-sector limit on this path was never characterized, so the one-sector cap stays.

The concrete in-kernel cdfs fixes for both gotchas above live in the amix-cdfs repo — the byte-loop memset (ad09035) and the one-sector DMA cap (a5c6915).

Verify /stand/unix after an install ✅

The stock /usr/sys/Makefile install rule copies the new kernel to /stand/NEWrelocunix, moves any existing /stand/unix to /stand/OLDunix, links NEWrelocunix as /stand/unix and runs make in /stand to rewrite the boot partition ✅ — so when it completes, /stand/unix is the new kernel. The Amiga bootstrap boots the boot-partition image written from /stand/unix, so an install that stopped part-way (or a wrapper script that reports success regardless) silently re-boots the old kernel, and any "verified on the new kernel" result is worthless. Check before you reboot, and if they differ, do the copy explicitly and rebuild the bootpart:

sum -r /stand/unix /usr/sys/relocunix   # the two sums must match before you reboot
# if they do not:
cp /usr/sys/relocunix /stand/unix
cd /stand; make            # (or: cd /usr/sys; make bootpart KERNEL=relocunix)
sum -r /stand/unix         # now matches relocunix

This is the same hazard the D245 clean-gate guards against, one step later in the pipeline: there sum -r proves the link is clean; here it proves the image you're about to boot is the one you just built. ✅

Verify which kernel actually booted 🟡

Always identity-check which kernel actually booted (a version marker, or sysfs(GETFSIND, …)) before trusting a test result.

Scripting the kernel.c edits with ed — the half-patch trap ✅

Sooner or later everyone automates Step 2: a script pushes an ed heredoc that inserts the extern declarations, address-searches the target cdevsw[] row, changes it, and writes the file. This section is uniformly first-party ✅ — every failure mode below was reproduced with GNU ed over fixture copies of the real stock file layouts (2026-07-30/31). It applies to any stock file you patch in place — master.d/kernel.c, master.d/filesys.c (vfssw[]), the subdir Makefiles — and, in general, to anyone patching SVR4 sources with ed.

A missed address search is not a no-op. ed prints its famous ? — but the commands that already ran stay applied to the buffer, and the trailing w still writes the file. In the canonical two-edit body that means the extern insert lands, the row change silently never happens, and kernel.c is now half-patched: the driver links into the kernel while its cdevsw[] slot still holds the stock filler. Nothing fails to build, nothing panics; the only symptom is a device that never answers.

/ANCHOR-A/a         <- edit 1, the insert: applies
  (new extern line)
.
/ANCHOR-B/c         <- edit 2, the row change: on a miss, ed prints ? ...
  (replacement row)
.
w                   <- ... and the write still happens: a half-patched file

A name-wide idempotency guard then makes it permanent. The natural wrapper guard — "the driver's name is already somewhere in the file, skip" — is satisfied by the surviving extern insert alone, so every later run reports already-patched and skips the repair, forever. The half-patch is invisible to the very tooling that created it.

Three measured variants of the same trap ✅:

  • The wrong-row overwrite. An address that searches for the row's /*N*/ comment tag changes whatever line carries that tag. A claimant that kept the bare tag (community trees do — see the registry's collision notes) is overwritten outright, with no error.
  • The renumber skew. After a driver changes major number, a name-wide guard running on a tree integrated before the renumber sees the old claim and no-ops — while rebuilt /dev nodes open the new major. Kernel on the old slot, nodes on the new one, every build and every symbol check green; the only runtime evidence is a dead device.
  • The wrapping search. ed's /re/ address search wraps around the buffer, so an address computed as "the first TAB-indented }; after the anchor" can resolve inside a different switch table once the file drifts. Measured: a vfssw[] row landed inside fmodsw[] — exit status 0, no message.

Exit status is a weak oracle. Check $? on every ed invocation, but never let it be the only gate: behaviour across ? errors is not something to build on — the two eds in play (GNU host-side, SVR4 on the box) do not agree, wrappers habitually print "patched" unconditionally, and a remote-shell transport that swallows the on-box exit status turns every other guard silent ✅. The reliable mechanism is verification in the file itself, before and after the write.

The fail-closed pattern ✅:

  1. Pre-probe before any write (verify-then-apply). Probe every anchor the script depends on, row-scoped, not name-wide, and classify the file: target row free / already exactly ours (an idempotent re-run — proceed) / taken by another claimant / our name present at a different row / insert present but row absent / anchor missing. Refuse everything but the first two, with the diagnosis and the remedy spelled out.
  2. One ed invocation per edit, $? checked. Never pair an insert and an address-searched change in one body — that pairing is the half-patch generator.
  3. Post-verify the row, in the file. Read it back and confirm the expected content sits inside the intended table's span — a row can land in the wrong table with rc 0.
  4. Repair or refuse half states. Insert-present-row-absent gets repaired, or refused with the exact remedy — never absorbed into "already patched".
  5. Propagate refusal through every layer, so the calling builder actually stops instead of shipping the kernel anyway.
  6. Prove it with no box. Run the same script over fixtures cut from the real stock files and reproduce each failure mode first; a selftest that lifts the expected row out of the real patch script fails on drift instead of rotting quietly.

This is the scripted-edit companion to the registry's manual rule — match the row by its tag and refuse anything but the free filler (conventions) — and to the pre-POSIX shell warning in Step 2. One-line checklist entry: quirk 28.

See also

Sources

  • Stock sources, public ✅ — the stock SCSI and console sources in amigaux/amix-system (7cba961): usr/sys/amiga/alien/a3091.c, usr/sys/amiga/alien/a2091.c, usr/sys/amiga/alien/dd.c, usr/sys/amiga/alien/sdpart.c, usr/sys/amiga/alien/scsi.c, usr/sys/amiga/console/scrdev.c, usr/sys/amiga/console/c0.c, usr/sys/amiga/console/screen.c.
  • The public 68040/68060 port, asokero/amix-040-060-port @ upstream/main (2026-09-29): STATUS.md (the A3000 + accelerator rig with RAM and load base at 0x08000000; the hardware-verified "DMA coherence (A3091 / SDMAC)" row; rows 68040-260906-06 and 68040-260928-05), docs/REALHW-Z3-040-260906.md, docs/REALHW-ISSUE54-STAGED-260830-06.md (the sac=96D99F0 line) and docs/contracts/A3091-PHASE-MISMATCH-RESUME-AUDIT.md (the chip-RAM bounce is the A2091's) ✅ — https://github.com/asokero/amix-040-060-port.

  • The super-DMAC reach refutation (2026-10-02) ✅ — the 64119 row of the kernel table on this page (stock a3091, RDB buffer above 0x07000000, boots) and Amiberry's src/a2091.cpp (the A3000 super-DMAC initialised with the 24-bit DMA mask off; dma24bit applies only to the add-on cards), read in the Amiberry source. The stale-cache explanation is a hypothesis 🔴.

  • The 2026-09-29 stock-kernel RAM-layout measurement ✅ — on the emulated bench the same 64 MB A3000 motherboard bank boots a kernel that probes for the A3000 SCSI and stops the pinned stock kernel at s5mountroot VOP_OPEN error 6 (no card 0), symbolised against the stock autocon() load-address guess (quirks §57).
  • amix-kerntools first-party records (2026-07-16, 2026-07-21): relsim scope correction, bootchain-verify gate, measured 85% relink corruption, tdivs blast-radius map, __udivdi3 metal closure (2026-07-21).
  • Ditto, Writing Amix Device Drivers, 1990 European Amiga Developer's Conference (project PDF; see bibliography) — /usr/sys object libraries, kernel.c switch tables, the "Adding a device driver" procedure, rdbunix image name, keep-old-/unix rule.
  • asokero/va2000-amix README/install script — the 6-file patch set, mknod /dev/va2000 c 68 0, rm -f amiga/config/unix.o master.d/exp unix, make install → relocunix, cp relocunix /stand, make bootpart KERNEL=relocunix, the extern-before-io_init[] and pre-POSIX /bin/sh gotchas.
  • amix_21_root.adf analysis via tools/inspect-adf.sh — install-time make bootpart KERNEL=relocunix, BOOTSIZE=2 MB boot partition, shutdown -i6.
  • Master research brief §3 (boot process / kernel install flow), §4 (kernel architecture), §5 (driver model / "Adding a driver"), §6 (VA2000 patch set), §13 (open questions: rdbunix/relocunix rename).
  • The A4091-on-Amix project — NOTES.md §17–§18 (the D245 boot-breaker: relocator Alert() mechanism, the emulator-only kernel-image corruption (configuration-dependent; see the D245 section), the build-until-stable gate; reproduced locally ✅) and the handoff brief §5/§10, plus src//tools/.
  • tools/build-clean-kernel.sh — the relink-until-sum -r-recurs clean-gate that dodges the D245 corruption.
  • tools/checkunix.c — native big-endian .symtab integrity detector (0x52454C41 | AT_DeadEnd == D245; build with cc -O -o checkunix checkunix.c).
  • tools/relsim.py — host-side reimplementation of the boot relocator rel(); the full offline D245 oracle (symtab and relocation records).
  • a4091.device open-source project: https://github.com/A4091/a4091-software (A4091 ROM + SCRIPTS assembler), referenced by the A4091-on-Amix work.
  • amix-a4091 kernel patches src/kernel-patches/scsi.c.patch (userspace /dev/scsi GSIO bounce iobuf → 64 KB) and src/kernel-patches/a3091.c.patch (commit e70c1d7) — the A3000 super-DMAC chip-mem DMA bounce for buffers ≥ 0x1000000 (the threshold is a2091's, not a measured a3091 reach limit — see the refutation above), ported from the a2091.c sibling; evidence = source + Amiberry-savestate differential (amix-a4091 CD-ROM-effort handoff, 2026-07-10), including the savestate pair showing a valid UNIX_Root RDSK/PART in the booting kernel vs all-zeros in the panicking one with queue[0].f populated in both.
  • Scope correction (2026-07-26/27) ✅ — the root-cause write-up and its fix (first-party record, 2026-07-26/27). The bounce is carried by every kernel that roots through the A3000 onboard SCSI (measured necessary on the emulated A3000), not by a4091 or cdfs kernels specifically: a3091.c is the A3000 onboard driver, is in the stock amiga/alien Makefile and links into every kernel. The panic is not size-triggered — a patched kernel with a 652-byte larger loaded image boots, and both booting cdfs kernels place sdpart.c's static RDB buffer at a higher address than the panicking one; the four-kernel section-size/block-offset table, the two-image 2×2 (identical panic text and backtrace addresses in both panicking cells; whole-/usr/sys byte-identical except amiga/alien/) and the vfs_mountroot disassembly (empty rootfstype ⇒ iterate vfssw, print the last errno ⇒ errno 30 is not diagnostic; error 6 = ENXIO from ddopen→sdpartition→getrdb) are all first-party measurements. Real hardware rooting through a Z3660 is indifferent to the patch.
  • amix-cdfs ad09035 — byte-loop memset replacing the SVR4 kernel bzero that under-clears a

    ~1 KB region (tail left as garbage); it bit cdfs twice from one ~1.1 KB struct clear (has_child_link, is_relocated).

  • amix-cdfs a5c6915 — caps the in-kernel SCSI (struct sdcom + sdqueue()) media transfer to one 2048-byte sector; a multi-sector in-kernel DMA returns kernel text / wedges the box, while the userspace /dev/scsi GSIO path (64 KB iobuf) is unaffected (Amix kernel-RE + gotchas handoff, CD-ROM effort, 2026-07-10).
  • The stock /usr/sys/Makefile install rule (install : all → cp … /stand/NEWrelocunix, mv /stand/unix /stand/OLDunix, ln NEWrelocunix unix, make in /stand), read from a stock 2.1c tree ✅; the sum -r /stand/unix versus relocunix check on a real A4000 + Z3660 (first-party record, 2026-07-12) ✅. The same record also confirms SVR4 grep has no \| alternation (already noted above under build-script gotchas).
  • The cdevsw free-row gate and the half-patch audit fixes (first-party record, 2026-07-30/31) and the amix-z3660net patch-script hardening (603c719) ✅ — the ed miss-then-write half-patch and the bare-tag wrong-row overwrite measured host-side with GNU ed over the real stock kernel.c layout; the wrapping-search wrong-table landing and the discarded-exit-status mode measured on the filesys.c arm; the renumber-skew shape found live in pre-renumber integrated trees; and the fail-closed pre-probe / one-edit-per-run / post-verify-the-row / repair-half-states pattern, each failure mode first reproduced and then pinned by the repos' no-box selftests.