Refuse a directory whose last entries cannot be named as a parent

A parent is an entry index PLUS ONE in two bytes, so entry 65535 has no
parent number: adding one wraps to zero, and zero is the root. Eight
entries to a block, so 8192 directory blocks reaches it and SplitDisk
formatted that happily.

It does not fail by refusing, which is why it was worth chasing rather than
reasoning about. Reproduced on a disk built for it: mkdir /deep/child, with
/deep at entry 65535, printed 'Made "/deep/child" as entry 0' and put child
in the ROOT. Listing /deep then showed nothing, because the search is for a
parent of 65536 and the entry carries zero - so the same mkdir succeeded
again, and again, and five entries called /child piled up in the root.
Duplicate names in one directory are the one thing rename refuses outright,
on the grounds that a search answers with whichever it meets first and the
rest can never be reached; this manufactured them one per attempt.

8191 blocks is the most, giving 65528 entries. Refused when formatting and
again when reading, in both implementations, because a disk claiming more
was made by something that never checked. On the machine only the high byte
of the count has to be looked at: anything from 0x20 up is too many.

Three checks, all of which fail with their guard removed. The machine's
disk claims the size rather than having it, so the test image is 64 blocks
that lie rather than sixteen megabytes that do not - mounting is refused at
the geometry, which is read out of block 0.
This commit is contained in:
Anachronaut
2026-08-25 23:47:02 -04:00
parent 634650cab9
commit ce0f18f4ef
9 changed files with 129 additions and 1 deletions
+19 -1
View File
@@ -78,10 +78,20 @@ static int readSuperblock(FILE *image, Superblock *super) {
block[SBFS_SUPER_VERSION], SBFS_VERSION_FLAT, SBFS_VERSION_TREE);
return 1;
}
// A directory big enough that its last entries cannot be named as a parent. See
// sbfs.h: those entries do not refuse what is put in them, they quietly put it in the
// root instead. Refused on the way in, so that nothing below ever has to wonder.
uint16_t directoryBlocks = readWord(block + SBFS_SUPER_DIRBLOCKS);
if (directoryBlocks > SBFS_MAX_DIRECTORY_BLOCKS) {
fprintf(stderr, "Error: That disk claims %u directory blocks, and %u is the most"
" that leaves every entry able to be named as a parent.\n",
directoryBlocks, SBFS_MAX_DIRECTORY_BLOCKS);
return 1;
}
super->version = block[SBFS_SUPER_VERSION];
super->diskBlocks = readWord(block + SBFS_SUPER_DISK);
super->directoryStart = readWord(block + SBFS_SUPER_DIRSTART);
super->directoryBlocks = readWord(block + SBFS_SUPER_DIRBLOCKS);
super->directoryBlocks = directoryBlocks;
super->freeBlocks = readWord(block + SBFS_SUPER_FREE);
return 0;
}
@@ -397,6 +407,14 @@ static uint16_t countFree(const Directory *directory, const Superblock *super) {
// ---- Commands ----
static int commandFormat(const char *path, uint16_t blocks, uint16_t directoryBlocks) {
if (directoryBlocks > SBFS_MAX_DIRECTORY_BLOCKS) {
fprintf(stderr, "Error: %u directory blocks is %u entries, and entry 65535 has no"
" parent number - adding one wraps to zero, which is the root."
" %u blocks is the most, giving %u entries.\n",
directoryBlocks, (unsigned)directoryBlocks * SBFS_ENTRIES_PER_BLOCK,
SBFS_MAX_DIRECTORY_BLOCKS, SBFS_MAX_ENTRIES);
return 1;
}
if (blocks <= 1u + directoryBlocks) {
fprintf(stderr, "Error: A disk of %u blocks has no room for a superblock and a"
" directory of %u.\n", blocks, directoryBlocks);
+21
View File
@@ -119,6 +119,27 @@
#define SBFS_PARENT_OF(index) ((uint16_t)((index) + 1))
#define SBFS_PARENT_INDEX(parent) ((int)(parent) - 1)
// ---- How big a directory may be ----
//
// The parent is an index plus one in sixteen bits, so index 65535 has no representation:
// adding one wraps to zero, and zero is the root. An entry that cannot be named as a
// parent is a directory that cannot hold anything, and it does not fail by refusing.
//
// WHAT IT DOES INSTEAD IS WORSE THAN FAILING. Creating something inside it writes a
// parent of zero, so the thing lands in the root while the tool reports the path it was
// asked for. Looking in that directory afterwards finds nothing, because the search is
// for a parent of 65536 and the entry says zero - so the same create succeeds again, and
// again, piling up entries of one name in the root. Duplicate names in one directory are
// the one thing rename refuses outright, on the grounds that a search answers with
// whichever it meets first and the rest can never be reached; this manufactured them.
//
// Eight entries to a block, and 65535 entries is the most that leaves every index one
// short of the wrap. 8191 blocks gives 65528 of them, which is the last whole block that
// fits. Checked when formatting and again when reading, because a disk claiming more may
// have been made by something that never checked at all.
#define SBFS_MAX_DIRECTORY_BLOCKS 8191
#define SBFS_MAX_ENTRIES (SBFS_MAX_DIRECTORY_BLOCKS * SBFS_ENTRIES_PER_BLOCK)
// Paths are separated by this, and a leading one means "from the root". A name may not
// contain it, which is what makes a path unambiguous without any quoting.
#define SBFS_SEPARATOR '/'