# repo22 real multi-device manual test report

Date: 2026-08-08 19:37 UTC
Baseline: `96e22cc713cc57180ce3ecb2b98852f090e3868b` plus this multi-device batch
Guest: FreeBSD 15.0-RELEASE-p8 amd64, QEMU TCG
Host tools: erofs-utils 1.8.6

## Result

**PASS**. Real external providers, explicit slot mapping, flatdev, unified
addresses, 48-bit fields, failure rollback, forced GEOM orphaning, and the
single-device regressions all passed. There were no new dmesg lines, traps, or
panics. Final EROFS mount, md-provider, and loaded-module counts were zero.

## Build and module

Commands:

```sh
git diff --check
./build.sh
nm -u build/erofs.ko | awk '$NF == "bcmp" {bad=1} END {exit bad}'
sha256sum build/erofs.ko
```

Actual:

- `git diff --check`: PASS.
- Cross-build against `/work/dev-freebsd-releng`: PASS.
- Unresolved `bcmp`: none, PASS.
- Final `build/erofs.ko` SHA256:
  `aa6708256246b303f4be2a8d516bf503f8a232df1ad3f466f5685573f221c180`.
- Final `kldload` and `kldunload`: PASS.

## Fixture commands

The legal baseline was generated by erofs-utils 1.8.6, not by synthesizing an
EROFS image from scratch:

```sh
WORK=/work/build/repo22-multidev-fixtures-20260808
mkdir -p "$WORK/src/cross"
python3 - <<'PY'
from pathlib import Path
import hashlib
root = Path('/work/build/repo22-multidev-fixtures-20260808/src')
def content(label, blocks, tail=0):
    out = bytearray()
    for block in range(blocks):
        seed = f'{label}:block:{block}'.encode()
        chunk = bytearray()
        counter = 0
        while len(chunk) < 4096:
            chunk += hashlib.sha256(
                seed + counter.to_bytes(4, 'little')).digest()
            counter += 1
        out += chunk[:4096]
    if tail:
        out += hashlib.sha256(f'{label}:tail'.encode()).digest()[:tail]
    return bytes(out)
root.joinpath('cross/striped.bin').write_bytes(content('striped', 24, 173))
root.joinpath('cross/second.bin').write_bytes(content('second', 17, 29))
root.joinpath('control.txt').write_text(
    'repo22 multidev deterministic control\n', encoding='ascii')
PY
truncate -s 0 "$WORK/base.blob"
mkfs.erofs -T0 --all-root --chunksize=4096 \
    --blobdev="$WORK/base.blob" "$WORK/base.primary" "$WORK/src"
```

erofs-utils 1.8.6 accepts one `--blobdev`; direct generation of two or more
external slots is therefore **MKFS-UNAVAILABLE**. Multi-slot fixtures were made
by a structured, assertion-driven patch of the legal baseline:

1. `dump.erofs --path=PATH -e base.primary` supplied each NID and file size.
2. The patch asserted compact chunk layout, zero xattr size, index format, old
   device ID 1, and every old block address.
3. A complete physical chunk, rounded to 4096 bytes for the final partial
   chunk, was copied into a deterministic round-robin blob.
4. Each index was rewritten as little-endian
   `(startblk_hi, device_id, startblk_lo)`.
5. One zeroed primary block was appended for the enlarged device table;
   `blocks_lo`, `extra_devices`, and `devt_slotoff` were updated. This avoids
   overwriting inode metadata merely to add slots.
6. Each 128-byte slot was written at offsets 64/68/72/74 with `blocks_lo`,
   `uniaddr_lo`, `blocks_hi`, and `uniaddr_hi`.
7. The EROFS CRC32C was recomputed over bytes 1024 through 4095 with polynomial
   `0x82f63b78` and initial value `0xffffffff`.

The asserted patch manifest was:

```text
/control.txt nid=44 size=38 chunk=4096 indexes=1 base=1440
/cross/second.bin nid=50 size=69661 chunk=4096 indexes=18 base=1632
/cross/striped.bin nid=56 size=98336 chunk=32768 indexes=4 base=1824
```

Pre-kernel qualification:

```sh
fsck.erofs --device=two.blob1 --device=two.blob2 \
    --extract=two.extract two.primary
fsck.erofs --device=three.blob1 --device=three.blob2 \
    --device=three.blob3 --extract=three.extract three.primary
fsck.erofs --device=four.blob1 --device=four.blob2 \
    --device=four.blob3 --device=four.blob4 \
    --extract=four.extract four.primary
```

All extracted source SHA256 values matched. The 48-bit feature and patched
`device_id=0 + uniaddr` extraction are not understood by erofs-utils 1.8.6;
those layouts were qualified by the FreeBSD kernel SHA256 tests below and the
Linux 7.1 `erofs_map_dev` semantics.

FreeBSD attachment and mount commands used one md provider per image/blob:

```sh
mdconfig -a -t vnode -f primary -u 90
mdconfig -a -t vnode -f blob1 -u 91
mdconfig -a -t vnode -f blob2 -u 92
mount -t erofs -o ro -o device.2=/dev/md92 \
    -o device.1=/dev/md91 /dev/md90 /mnt/repo22-multidev
```

The reversed option order was intentional. Flatdev used only:

```sh
mdconfig -a -t vnode -f two.flat -u 90
mount -t erofs -o ro /dev/md90 /mnt/repo22-multidev
```

## Fixture SHA256

| Fixture | SHA256 |
|---|---|
| one-blob primary | `fa2f35f59f63cc4a56d230ba960a6c1d27748506fccce081447cab0dc0d540fd` |
| one-blob slot 1 | `3aeb19c519636b61dc3ebd47c6286098deaa0990ff618ce3facbff4fd65907cd` |
| two-slot primary | `46d76d5cc311f97f263ea4c20a2510338f5b11da9d71e0c73f69ae3751afd696` |
| two-slot blob 1 | `53853d033adf154a956192d9813726888c3f9575ecd13fa45a26aea5c4f07eaf` |
| two-slot blob 2 | `9ee3e3ad420024ff1945d92469d3d337817977f28c5b745c4ca7b1664f4d6584` |
| two-slot flatdev | `4b561db1eb0048989256f3c51504b4611881069bf283fbfe2051847e8162301e` |
| three-slot primary | `4f2dc4ee6385f31139151e1b04279a8fb90fcd2cbba928ee6949df62f1bb9585` |
| three-slot blobs 1/2/3 | `12c5d73cb12e1bb5fd1a9d9715ec88d2132dbaa8df62e2b5aad3cfb454116a3a`, `3d1574254a97e572fca323f153bb2a46ebdfb060a0765298700768e39a7f91d8`, `ba42a2a3724771975c27a43ac8d64a2568515fb4e8ad14b29cc09a7a865ffba0` |
| four-slot primary | `d3366dfb5f4c98db2db669ead3209a0abbba5df989d9aea7e1217aff46f0154b` |
| four-slot blobs 1/2/3/4 | `ad24e257baf97f855f756cc917878a9616858860ddbfab4ffe3cc362e9233662`, `eb55bd05f873dd8732bcbe35a0818ef232a9eff6fedec618f5f949253d7cd2d1`, `59f9a3aed34cc8598e6a96381b55a18f6a0e6674e3e26921834948ad333cb3c5`, `d5080743a85e47752cd33e86e26690b8978d314e685b27d449d5394c44e54244` |
| device ID 0 + uniaddr | `e4cf71f76f2a2e690e905e4135c21ce29d647be5dd2b6ce567ac19f82d1875d2` |
| 48-bit high uniaddr | `00d49beea1237125886f13912bf062153b992582c1cf3a2494b44679285b192e` |
| overlapping-slot negative | `6ecfc77b688ac1931f3e904f48fa43a81c05a6047c21ccfc4636e9e07c17d5cf` |
| mapped-ID-3 negative | `dd6ddc6943b7edd2009a48999d03a2d5b69a9ce7d820357527dbcdb52adb2afc` |

Source SHA256 values for the cross-device fixture:

```text
6a38f93ca44de4affcef23ad5979505225f21e5b6912b5c886866170c007b16c  ./control.txt
45b7df16a5833013042694c2e8171d782102bf20abbc6c1ec2789518d5a65125  ./cross/second.bin
7ab83ca35cc4bb361bf60b5619d3f55b717fb8e2a7a81a2f069dce859efe8de9  ./cross/striped.bin
```

## Test matrix

| Test | Expected | Actual | Status |
|---|---|---|---|
| TC093 plain single device | full SHA256 match | both files matched | PASS |
| TC093 chunk single device | device ID 0 reads primary | both files matched | PASS |
| compression regression | LZ4 data unchanged | both files matched | PASS |
| TC098 fragments | packed-inode file matches | `65b9a59e...e7cd` | PASS |
| metabox regression | cold metabox-backed files match | `f1` and `f10` matched known hashes | PASS |
| TC006/TC099 primary + one blob | all external chunks use slot 1 | three files matched | PASS |
| TC094 two external slots | reversed option order, cross-slot SHA256 | all three files matched | PASS |
| TC100 four providers total | slots 1-3, order 3/1/2 | all three files matched | PASS |
| additional four external slots | file crosses four blobs | SHA256 matched | PASS |
| statfs combined blocks | sum primary and external blocks | 184 KiB reported for 46 blocks | PASS |
| TC101 flatdev | non-zero IDs add `uniaddr` on primary | SHA256 matched | PASS |
| TC101 device ID 0 + uniaddr | unified range selects external provider | SHA256 matched | PASS |
| 48-bit root/index/uniaddr | high fields are not truncated | source SHA256 matched | PASS |

## Error and rollback matrix

| Case | Expected | Actual | Status |
|---|---|---|---|
| split primary, no external options | `ENXIO` flatdev media bound | `Device not configured` | PASS |
| missing slot 2 | `ENXIO` | `external devices don't match ... Device not configured` | PASS |
| short slot 2 | `ENXIO` | `Device not configured` | PASS |
| swapped short/long providers | fail before root | `Device not configured` | PASS |
| same provider in two slots | `EINVAL` | `Invalid argument` | PASS |
| overlapping unified ranges | `EINTEGRITY` | `Integrity check failed` | PASS |
| explicit writable request | `EROFS` | `Read-only file system` | PASS |
| mapped device ID 3 with two slots | read-time `ENODEV` | `Operation not supported by device` | PASS |
| second concurrent reuse mount | `EBUSY` | `Device busy`; first mount remained readable | PASS |
| normal detach while mounted | `EBUSY` | `mdconfig ... Device busy` | PASS |
| forced orphan then cold slot-2 read | `ENXIO` | `Device not configured` | PASS |
| unmount after forced orphan | clean close of detached consumer | succeeded | PASS |

Every mount failure was followed immediately by successful md detach. Opened
external devices were therefore rolled back, including failures after one or
more earlier slots had opened.

## dmesg and cleanup

The final matrix captured `dmesg` before and after all tests:

```text
before_lines=2
after_lines=2
no-new-lines
```

The two pre-existing lines were unchanged historical messages:

```text
interface erofs.1 already present in the KLD 'erofs-repo22-xattrfix.ko'!
md0: truncating fractional last sector by 14 bytes
```

Final state:

```text
kldstat -n erofs: empty
mdconfig -l: empty
mount -p | grep erofs: empty
```

## Tool limitations

- **MKFS-UNAVAILABLE**: erofs-utils 1.8.6 cannot directly emit more than one
  external blob option in this environment. Multi-slot images were derived
  from a legal mkfs image with deterministic, asserted structural patches.
- **MKFS-UNAVAILABLE**: erofs-utils 1.8.6 rejects the experimental 48-bit
  incompat feature and does not correctly extract the patched device-ID-0
  unified-address control. Both were verified by complete FreeBSD kernel
  SHA256 reads.
- **KERNEL-FAIL**: none.
