CUBELinux.0.5: a torn log entry is overwritten, not buried
The write path's last unproven claim was the one everything else rests on: replay is prefix-trusting. Building the gate for it found that the claim was false as implemented — in *both* implementations, and in the same way. The log walkers advanced their offset past an entry's frame and only then checked the checksum. A corrupted entry therefore never moved the *replay* point (it was discarded, so the store looked right) but it did move the *append* point. The consequences, in order of how bad they are: - the kernel appended **after** the tear, burying the corruption inside a log that then looked well-formed — the exact opposite of the documented rule; - the userspace log truncated to a prefix that still contained the corrupt frame, so every later append started past it and the corruption stayed in the log forever; - and because each append then read back the same wrong prefix, every append wrote to the same offset and overwrote its predecessor — four mutations in, one on disk. The fix is one line of ordering in three places: compute the frame's end, check the length and the checksum, and only then move the offset. An entry that did not validate may not move the point that says where the log ends. Gate (kernel/verify-torn-tail.sh), on a store with an overwrite, an insertion and a deletion whose last entry has been torn by zeroing its tail in place: whole : 272 bytes, 3 records (the delete applied — the contrast) reference : 349 bytes, 4 records (userspace, torn bytes: the delete discarded) kernel : 349 bytes, 4 records appended : 667 bytes, 8 records (the kernel appended over the tear) expected : 667 bytes, 8 records (userspace: torn tail dropped, then the same four) Building the gate also corrected the gate itself: tearing a log by *truncating the device* is not a torn log, it is a smaller device — the capacity shrinks, writes past the end vanish into the page cache with no error anywhere, and the test measures an artifact. A torn write leaves the device the same size and corrupts bytes in place. All seven gates pass on 0.5.
This commit is contained in:
@@ -9,7 +9,7 @@ NAME = CUBELinux
|
||||
# release. The base version stays in VERSION/PATCHLEVEL/SUBLEVEL above (visible in
|
||||
# `make kernelversion`), while `uname -r` and /lib/modules report the CUBELinux
|
||||
# release, with any -dirty or SCM suffix still appended by setlocalversion.
|
||||
CUBELINUX_VERSION = CUBELinux.0.4
|
||||
CUBELINUX_VERSION = CUBELinux.0.5
|
||||
|
||||
# *DOCUMENTATION*
|
||||
# To see a list of typical targets execute "make help"
|
||||
|
||||
@@ -634,17 +634,17 @@ fn apply_log(log: &[u8], merged: &mut Merged) -> Result<Option<u64>, &'static st
|
||||
let key = &log[off + 37..off + 61];
|
||||
word.copy_from_slice(&log[off + 61..off + 65]);
|
||||
let len = u32::from_le_bytes(word) as usize;
|
||||
off += ENTRY_FIXED;
|
||||
if off + len > log.len() {
|
||||
let frame_end = start + ENTRY_FIXED + len;
|
||||
if frame_end > log.len() {
|
||||
break;
|
||||
}
|
||||
// The checksum covers space, key, length and value, so a corrupted entry is
|
||||
// stopped at rather than applied.
|
||||
if crc32(&log[start + 5..off + len]) != crc {
|
||||
if crc32(&log[start + 5..frame_end]) != crc {
|
||||
break;
|
||||
}
|
||||
let value = &log[off..off + len];
|
||||
off += len;
|
||||
let value = &log[start + ENTRY_FIXED..frame_end];
|
||||
off = frame_end;
|
||||
merged
|
||||
.add(space, key, value, op == 2)
|
||||
.map_err(|_| "out-of-memory")?;
|
||||
@@ -812,14 +812,17 @@ fn log_valid_len(log: &[u8]) -> Result<usize, &'static str> {
|
||||
let crc = u32::from_le_bytes(word);
|
||||
word.copy_from_slice(&log[off + 61..off + 65]);
|
||||
let len = u32::from_le_bytes(word) as usize;
|
||||
off += ENTRY_FIXED;
|
||||
if off + len > log.len() {
|
||||
// Only a validated entry moves the append point. Advancing first and checking after
|
||||
// counts a torn entry as part of the prefix, so the next append lands *after* the
|
||||
// corruption and buries it — the opposite of the rule that a torn tail is overwritten.
|
||||
let frame_end = start + ENTRY_FIXED + len;
|
||||
if frame_end > log.len() {
|
||||
break;
|
||||
}
|
||||
if crc32(&log[start + 5..off + len]) != crc {
|
||||
if crc32(&log[start + 5..frame_end]) != crc {
|
||||
break;
|
||||
}
|
||||
off += len;
|
||||
off = frame_end;
|
||||
}
|
||||
Ok(off)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user