schfs: records at arbitrary offsets are packed, and the compiler is told
A leaf's value area holds variable-length records end to end, so a record begins wherever the one before it finished -- a multiple of 8 only by luck. The code reads them by casting a pointer into the block, which tells the compiler they are aligned when they are not. That works on x86, which is why it has never shown as a bug, and it is undefined in C. A compiler entitled to assume 8-byte alignment may emit an instruction that requires it, and the failure would arrive at a different optimisation level or on a different architecture looking like filesystem corruption rather than like codegen. UndefinedBehaviorSanitizer reported 31 of them across obj.c, objw.c, attr.c and space.c. Seven types now carry SCHFS_PACKED: dirent_rec, extent, extref, index_rec, attr_rec, snap and inode. Two of those -- attr_rec and inode -- were never reported, because schfs_objtest does not reach an unaligned instance of either. They were latent rather than absent, and casting into a leaf is the same lie whether or not a test has caught it yet. NO ON-DISK LAYOUT CHANGED, and that was measured before the annotation was applied rather than assumed: every one of these already carries explicit `pad` fields and therefore has no implicit padding, so sizes and member offsets are identical with and without it. Seven _Static_asserts now pin the sizes, because a record that silently grows misreads every existing volume from that point on and looks like corruption rather than like a struct that changed. schfs_node_hdr, schfs_item, schfs_kptr and schfs_uberblock are deliberately left alone. The node header is exactly 32 bytes, so the items and key pointers after it are already 8-aligned, and annotating them would cost byte-wise access on the tree's hottest paths to fix a problem they do not have. Verified by reading an image written before the change: schfs_fsck --check passes on structure, checksums, key ordering, data blocks and derived free space, and the kernel mounts its own root unchanged. Also here: mkschfs passed a null pointer to qsort for an empty directory. Every implementation ignores it when the count is zero; the standard does not say they have to. make schfs-check now runs with no sanitizer output at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
This commit is contained in:
parent
25530e7eed
commit
7c8cf51941
5 changed files with 123 additions and 41 deletions
|
|
@ -13,6 +13,36 @@
|
|||
|
||||
#include <stdint.h>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Records that live at an arbitrary offset inside a block.
|
||||
//
|
||||
// A leaf's value area holds variable-length records packed end to end, so a
|
||||
// record begins wherever the one before it ended -- which is a multiple of 8
|
||||
// only by luck. The code reads them by casting a pointer into the block, and
|
||||
// that is a lie to the compiler unless it is told: the type says "8-byte
|
||||
// aligned" and the pointer is not.
|
||||
//
|
||||
// It works on x86, which is why it never showed as a bug, and it is undefined
|
||||
// in C. A compiler entitled to assume alignment may emit an instruction that
|
||||
// requires it, and the failure would arrive at a different optimisation level
|
||||
// or on a different architecture looking like filesystem corruption rather than
|
||||
// like codegen. UndefinedBehaviorSanitizer reported 31 of these across obj.c,
|
||||
// objw.c, attr.c and space.c.
|
||||
//
|
||||
// This annotation changes NO on-disk layout. Every struct it is applied to
|
||||
// already carries explicit `pad` fields and therefore has no implicit padding;
|
||||
// sizes and member offsets are identical with and without it, which was
|
||||
// measured rather than assumed before it was added. What it changes is the
|
||||
// code generated to read them: byte-wise where the compiler cannot prove
|
||||
// alignment, which is exactly the truth here.
|
||||
//
|
||||
// It is deliberately NOT applied to schfs_node_hdr, schfs_item, schfs_kptr or
|
||||
// schfs_uberblock. Those sit at fixed offsets in a block -- the header is 32
|
||||
// bytes, so everything after it is 8-aligned -- and annotating them would cost
|
||||
// byte-wise access on the hottest paths in the tree to fix a problem they do
|
||||
// not have.
|
||||
#define SCHFS_PACKED __attribute__((packed))
|
||||
|
||||
#define SCHFS_MAGIC 0x53636846u // "SchF"
|
||||
#define SCHFS_NODE_MAGIC 0x536e6f64u // "Snod"
|
||||
|
||||
|
|
@ -208,7 +238,7 @@ struct schfs_snap {
|
|||
uint32_t pad0;
|
||||
uint64_t created; // generation the snapshot was taken at
|
||||
char name[SCHFS_SNAP_NAME_MAX];
|
||||
};
|
||||
} SCHFS_PACKED;
|
||||
|
||||
struct schfs_uberblock {
|
||||
uint32_t magic; // SCHFS_MAGIC
|
||||
|
|
@ -283,7 +313,7 @@ struct schfs_inode {
|
|||
uint32_t flags; // SCHFS_I_* — none defined yet
|
||||
uint32_t gen; // transaction that last changed this inode;
|
||||
// what an incremental send/snapshot diff needs
|
||||
};
|
||||
} SCHFS_PACKED;
|
||||
|
||||
// An extent maps a run of file blocks onto a run of disk blocks. refs is what
|
||||
// makes snapshots and reflink copies nearly free: sharing a range is a refcount
|
||||
|
|
@ -313,14 +343,14 @@ struct schfs_extent {
|
|||
// without consulting the tree at all.
|
||||
uint32_t refs;
|
||||
uint32_t pad;
|
||||
};
|
||||
} SCHFS_PACKED;
|
||||
|
||||
// How many files point at a run of blocks. Present only for runs with more than
|
||||
// one reference; see SCHFS_EXTREF_OBJECTID.
|
||||
struct schfs_extref {
|
||||
uint64_t nblocks;
|
||||
uint64_t refs;
|
||||
};
|
||||
} SCHFS_PACKED;
|
||||
|
||||
// Directory entry payload. The key's offset field holds the name hash, so a
|
||||
// lookup is one tree descent rather than a directory scan.
|
||||
|
|
@ -337,7 +367,7 @@ struct schfs_dirent_rec {
|
|||
uint16_t namelen;
|
||||
uint32_t pad;
|
||||
/* name bytes follow, namelen of them, not NUL-terminated */
|
||||
};
|
||||
} SCHFS_PACKED;
|
||||
|
||||
#define SCHFS_NAME_MAX 255
|
||||
|
||||
|
|
@ -364,7 +394,7 @@ struct schfs_attr_rec {
|
|||
uint16_t flags;
|
||||
uint32_t pad;
|
||||
/* name bytes, then value bytes; the pair padded to an 8-byte boundary */
|
||||
};
|
||||
} SCHFS_PACKED;
|
||||
|
||||
// What a value *is*, so that a query can compare two of them meaningfully. A
|
||||
// string and an integer that happen to share bytes are not the same value, and
|
||||
|
|
@ -392,7 +422,7 @@ struct schfs_index_rec {
|
|||
uint32_t valuelen;
|
||||
uint32_t pad;
|
||||
/* value bytes follow, so a query can reject a hash collision */
|
||||
};
|
||||
} SCHFS_PACKED;
|
||||
|
||||
#define SCHFS_INDEX_LEN(valuelen) \
|
||||
((unsigned)((sizeof(struct schfs_index_rec) + (valuelen) + 7) & ~7u))
|
||||
|
|
@ -437,3 +467,14 @@ SCHFS_ASSERT_SIZE(schfs_attr_rec, 16);
|
|||
SCHFS_ASSERT_SIZE(schfs_index_rec, 16);
|
||||
|
||||
#endif /* SCHFS_H */
|
||||
|
||||
// The annotation above must not have moved anything. These are on-disk sizes:
|
||||
// if one changes, every existing volume is misread from that record onward, and
|
||||
// the failure looks like corruption rather than like a struct that grew.
|
||||
_Static_assert(sizeof(struct schfs_dirent_rec) == 16, "dirent record size changed");
|
||||
_Static_assert(sizeof(struct schfs_extent) == 32, "extent record size changed");
|
||||
_Static_assert(sizeof(struct schfs_extref) == 16, "extref record size changed");
|
||||
_Static_assert(sizeof(struct schfs_index_rec) == 16, "index record size changed");
|
||||
_Static_assert(sizeof(struct schfs_attr_rec) == 16, "attr record size changed");
|
||||
_Static_assert(sizeof(struct schfs_snap) == 96, "snapshot record size changed");
|
||||
_Static_assert(sizeof(struct schfs_inode) == 80, "inode record size changed");
|
||||
|
|
|
|||
Loading…
Reference in a new issue