Skip to content

Building & Installing a Kernel

Amix has a monolithic SVR4 kernel with no loadable modules ✅ — 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-POSIXno $(...) 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 installrelocunix ✅.) 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 intermittent ld 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, and the cause was twice mis-attributed: first to the SCSI driver's completion poll loop, then to the bootstrap relocator misbehaving. Both were wrong. 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)

🔴 Superseded attribution. This page previously said the corruption was introduced "when ld writes the linked kernel to disk". That was wrong, and so was every variant of it: 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 ≡ 0x19f0x1a4 (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; on the standing 16 MB bench config 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). The first golden gated this way validated 5/5 on the 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) — confirming the historical ~70% figure and 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, it needs the a3091.c chip-mem DMA bounce patch — whatever else is in it. ✅ This is not an A4091 requirement and not a cdfs requirement, though it was documented as both for months: 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 ✅.

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 ✅:

Rig Roots through Needs the patch?
Any emulated bench box (c6d0s1, card 0) A3000 onboard SCSI Yes — regardless of which add-on HBA the kernel carries
A4091 bench profiles A3000 onboard SCSI (A4091 sorts to card 1) Yes
Real A4000 + Z3660, root on the piscsi mailbox the Z3660 No — a3091 is linked but never carries the root read

That last row is why the defect survived so long: the metal-proven kernels were never exposed to it, so a build path that silently skipped the patch produced kernels that were fine on real hardware and unbootable on every bench box ✅.

Build-harness note. A patch keyed to a driver repo is only applied when that repo is part of the build. Keying this one to amix-a4091 meant build-kernel.sh <some-other-driver> silently omitted it — a boot-critical patch skipped by an unrelated repo-selection rule. 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; when that address is one the DMAC cannot reach, 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.

Distinct from the error-5 panic. This is VOP_OPEN error 6 (ENXIO — a super-DMAC high-address DMA failure). 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.

🔴 Open: the exact reach limit of the super-DMAC is uncharacterized. The ≥ 0x1000000 (16 MB) threshold is a2091's proven-safe cutoff, not a measured a3091 boundary — and the failing buffer sat around VA 0x078F0000, well above 16 MB, so a plain 24-bit-address story is incomplete. Treat the bounce as a safe over-approximation pending real-hardware measurement.

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). It ships today in the a4091 bench kernels.

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 this board 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). This is the same DMA-limitation family as the a3091 super-DMAC high-address bounce above.

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 HBA/DMA limit is uncharacterized, so the one-sector cap can't yet be raised.

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).

make install does not update /stand/unix — copy it yourself ✅

A successful cd /usr/sys; make install prints installed. Old kernel kept as /stand/OLDunix but was observed to leave /stand/unix still holding the previous kernel ✅ — confirmed by sum -r: /usr/sys/relocunix was the freshly-built image (checksum 44416) while /stand/unix was still the old one (38553). Because the Amiga bootstrap boots the boot-partition image written from /stand/unix, rebooting at that point silently re-boots the old kernel and any "verified on the new kernel" result is worthless. Do the copy explicitly and rebuild the bootpart, then re-check:

cp /usr/sys/relocunix /stand/unix
cd /stand; make            # (or: cd /usr/sys; make bootpart KERNEL=relocunix)
sum -r /stand/unix         # must now match sum -r /usr/sys/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 🟡

Warm reboot (shutdown -i6) was intermittently a no-op in one bench session — the box stayed multiuser on the old kernel — while cold boot (Amiberry down/up, which reloads the boot partition) was reliable 🟡. 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), and the fail-closed pattern at the end is what this family's own build tooling now enforces. 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. This exact shape was produced by this family's own tooling when z3660eth moved 48 → 51, and is now mechanically refused (a "name found at a different row" probe outranks every other verdict).
  • 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 the family's builders converged on ✅:

  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

  • amix-kerntools briefs boot2-d245-trap (2026-07-16), e2-relink-corruption-measured + tdivs-cross-assembler-miscompile (2026-07-21): relsim scope correction, bootchain-verify gate, measured 85% relink corruption, tdivs blast-radius map, __udivdi3 metal closure (kernel 10842→00961, validate-metal ALL GREEN 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 installrelocunix, 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 intermittent ~70% ld-write corruption, 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 high-address chip-mem DMA bounce for buffers ≥ 0x1000000, 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 amix-kerntools root cause (docs/a3091-bounce-boot-patch.md @ 05d0d78, fix f459afa). The bounce is required by every kernel that roots through the A3000 onboard SCSI, 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 ddopensdpartitiongetrdb) are all first-party measurements. Real hardware rooting through a Z3660 is indifferent to the patch, which is why metal never caught it; the 2026-07-11 kernel 52550 that "stopped at vfs_mountroot" and was attributed to code staleness is the same defect.
  • 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).
  • Bench caveat (2026-07-10): warm reboot (shutdown -i6) was intermittently a no-op (box stayed on the old kernel); cold boot reloads the boot partition reliably — always identity-check which kernel actually booted.
  • The amix-kerntools bench forensics @ 8a76775cd /usr/sys; make install prints "installed" yet leaves /stand/unix holding the previous kernel (proven by sum -r: relocunix 44416 vs /stand/unix 38553); copy relocunix to /stand/unix explicitly and rebuild the bootpart, then re-check sum -r /stand/unix. Real A4000 + Z3660, 2026-07-12 ✅. Same brief also confirms SVR4 grep has no \| alternation (already noted above under build-script gotchas).
  • The amix-kerntools cdevsw free-row gate + 2026-07-31 half-patch audit fix packets (2b23808, 1c6a57e, a81d7c9, f0c517c, f90756b, f025444, dac78a0) and the amix-z3660net patch-script hardening (603c719), 2026-07-30/31 ✅ — 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.