Found by wiring the affs kernel module — present here all along — into the lifecycle check, which put ADF in front of an outside reader for the first time.
Silent corruption past 36 864 bytes
An Amiga FFS file header names at most 72 data blocks; everything past that is named by a chain of file-extension blocks. The writer never emitted one:
for (var i = 0; i < dataBlockCount && i < HashTableCount; i++)
WriteUInt32BE(disk, hdrOff + 308 - i * 4, (uint)dataBlocks[i]);
The blocks past the 72nd were still allocated and still filled with the file's bytes — nothing pointed at them. So a file of any size came back at exactly the right length, without an error anywhere, correct to byte 36 863 and wrong after it:
size 1024: all files identical
size 4096: all files identical
size 20000: all files identical
size 49152: WRONG, first difference at 36864
The writer also set high_seq to the total block count rather than the number of pointers in that block.
Two fields in the wrong place
The last four words of a block are hash_chain (496), parent (500), extension (504), sec_type (508). We had:
|
ours |
correct |
| writer, parent |
504 |
500 |
| reader, extension |
496 |
504 |
| extent map, extension |
496 |
504 |
So every file header this project wrote put its parent directory into the extension pointer — claiming a file-extension block that was really a directory. And the reader took the hash chain for the extension chain, so a file sharing a hash bucket with another would have followed its neighbour as a list of data blocks. Neither showed, because the two mistakes cancelled: the reader never looked at 504, and 496 was zero for the last entry in every bucket.
The reader's own comment recorded the assumption: ExtBlockOffset = 496; // same field used for extension blocks.
Defragmentation
AdfBlockMover patched only the 72 pointers in the header, so once extension blocks exist, every block of a file past the first 36 KB kept naming where it had been. It now follows the chain, repointing the chain links and the data pointers inside each block.
Verification
The kernel's own Amiga driver, on files that cross the 72-block boundary:
$ sudo mount -t affs -o loop,ro,setuid=…,setgid=…,mode=644 v.adf m
$ md5sum m/*
e7f1a12e5108ef4706c011e19325a87d m/KEEP00.BIN <- matches the source
e1d9c510a5f3869bb66f6a48380686fb m/KEEP02.BIN <- matches
dafd9ae0f99ee6bd5b2ffbecc842043e m/KEEP04.BIN <- matches
f61a32382bd235ea85bd6ec50ed22c54 m/KEEP06.BIN <- matches
affs does not know uid=/gid= — it hands everything to root as 0700 — so the check now passes setuid=/setgid=/mode= instead, which it does know.
Found by wiring the
affskernel module — present here all along — into the lifecycle check, which put ADF in front of an outside reader for the first time.Silent corruption past 36 864 bytes
An Amiga FFS file header names at most 72 data blocks; everything past that is named by a chain of file-extension blocks. The writer never emitted one:
The blocks past the 72nd were still allocated and still filled with the file's bytes — nothing pointed at them. So a file of any size came back at exactly the right length, without an error anywhere, correct to byte 36 863 and wrong after it:
The writer also set
high_seqto the total block count rather than the number of pointers in that block.Two fields in the wrong place
The last four words of a block are
hash_chain(496),parent(500),extension(504),sec_type(508). We had:So every file header this project wrote put its parent directory into the extension pointer — claiming a file-extension block that was really a directory. And the reader took the hash chain for the extension chain, so a file sharing a hash bucket with another would have followed its neighbour as a list of data blocks. Neither showed, because the two mistakes cancelled: the reader never looked at 504, and 496 was zero for the last entry in every bucket.
The reader's own comment recorded the assumption:
ExtBlockOffset = 496; // same field used for extension blocks.Defragmentation
AdfBlockMoverpatched only the 72 pointers in the header, so once extension blocks exist, every block of a file past the first 36 KB kept naming where it had been. It now follows the chain, repointing the chain links and the data pointers inside each block.Verification
The kernel's own Amiga driver, on files that cross the 72-block boundary:
affsdoes not knowuid=/gid=— it hands everything to root as 0700 — so the check now passessetuid=/setgid=/mode=instead, which it does know.