diff --git a/Cargo.lock b/Cargo.lock index 8559887ff71..8b791e31ed6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -191,6 +191,8 @@ name = "bcachefs" version = "0.1.0" dependencies = [ "flate2", + "toyos-blockdev", + "toyos-crypto", ] [[package]] @@ -3730,6 +3732,9 @@ dependencies = [ [[package]] name = "toyos-net-wire" version = "0.1.0" +dependencies = [ + "toyos-crypto", +] [[package]] name = "toyos-osrelease" diff --git a/NOTICE b/NOTICE index 9ea46447e6e..414c4393252 100644 --- a/NOTICE +++ b/NOTICE @@ -272,11 +272,22 @@ lib/bcachefs/tests/fixtures/ — volumes upstream's tools wrote 144,978 bytes gzip -9, of a 33,554,432-byte raw volume sha256 7be2c99db0e68c784ea59ef394454a084fa57868b14027528ab6b8b21031840e raw 021c816a064fc2fca2402c8b2a87a9e6124e0b2f4a32779ef14654e908bcacff + + lib/bcachefs/tests/fixtures/encrypted.img.gz + 716,785 bytes gzip -9n, of a 33,554,432-byte raw volume + sha256 2a32529edf3e30de75d02cdc4ad0b2060552555b012e9647dc24702c0ee7f82d + raw a640704b34cadfc6544f096a36c74bd60b047a079fb20fd96c622f9bcd81a72e + + lib/bcachefs/tests/fixtures/encrypted-wide-macs.img.gz + 774,007 bytes gzip -9n, of a 33,554,432-byte raw volume + sha256 fd137a545190906ded0269eeec3e32835ffe3585d3e4c2cebe84d234a96b1b1f + raw 52638d7b3c31abb202e6b12c5ccb5a58fe95bee412bada3d7fc3756705cdcace + SPDX-License-Identifier: MIT OR Apache-2.0 -`lib/bcachefs/src/upstream` implements upstream's on-disk format, and the only -honest judge of a format reader is bytes the format's own implementation wrote. -This fixture is those bytes. It was produced by `bcachefs-tools` v1.39.4 running +`lib/bcachefs/src/engine` reads upstream's on-disk format, and the only honest +judge of a format reader is bytes the format's own implementation wrote. This +fixture is those bytes. It was produced by `bcachefs-tools` v1.39.4 running in a QEMU guest booted from the Arch Linux 2026.09.01 install medium, as a development instrument: neither the tools nor that medium is a dependency of this repository, nothing fetches either, and the suite reads only what is @@ -295,7 +306,7 @@ The commands, in the guest, against a 32 MiB raw disk on `/dev/vdc`: bcachefs fsck -y /dev/vdc `--block_size=4096` is explicit because `bcachefs format` picks a smaller block -for a small device, and 4096 is the only block size this crate reads. 32 MiB is +for a small device, and ToyOS's block devices have 4096-byte blocks. 32 MiB is the smallest volume that formats, fills and passes fsck at that block size: 16 and 24 MiB both fail during format, and 8 and 12 MiB fail even at the default block size. `bcachefs show-super` on the result: @@ -331,6 +342,31 @@ layout is a published on-disk format, not a copyrightable work of the program, and the file contents are ours. The tool is not distributed with this repository in any form. +`encrypted.img.gz` and `encrypted-wide-macs.img.gz` are encrypted volumes +upstream's tools wrote, which the engine unlocks and reads: ChaCha20/Poly1305 +with 80-bit data MACs, and with the 128-bit ones that `--wide_macs` sets. They were produced by `bcachefs-tools` v1.39.4 (commit +d997ad76e906), built from its tag, in a QEMU guest booted from the Ubuntu +26.04.1 arm64 cloud image, as a development instrument on the same terms as +above. The commands, against a raw image file, the second time with +`--wide_macs` and the label `toyosencw`: + + truncate -s 32M enc.img + printf '%s\n' 'toyos-test-passphrase' > pass + bcachefs format --block_size=4096 --encrypted --passphrase_file pass --fs_label=toyosenc enc.img + bcachefs unlock -k session -f pass enc.img # in a fresh session keyring + bcachefs fusemount enc.img /mnt/t + printf hello-toyos > /mnt/t/a.txt; seq 1 40000 > /mnt/t/seq.txt; sync + fusermount3 -u /mnt/t + bcachefs fsck -n -K enc.img # exit 0 + gzip -9n < enc.img > enc.img.gz + +The passphrase is a published test value. The files are an eleven-byte string, +the output of `seq` and an empty `lost+found`; the UUIDs, hash seeds and keys +inside are generated values; the member's device name is the image file's +basename, and no model or serial is recorded. Timestamps are 2026-10-22, the +guest's clock. The same reasoning as for `crc32c.img.gz` makes the bytes this +project's own. + lib/fat32/tests/fixtures/ — volumes macOS's FAT tools wrote ------------------------------------------------------------ diff --git a/build/licence.rs b/build/licence.rs index 465ee826784..7ebdef0ff38 100644 --- a/build/licence.rs +++ b/build/licence.rs @@ -417,15 +417,27 @@ pub const COMMITTED_FILES: &[(&str, &str, &str, Terms)] = &[ "ours: OVMF's answer on a q35 guest, read off that boot's own loader log", Terms::Spdx("MIT OR Apache-2.0"), ), - // A bcachefs volume upstream's own tools wrote, gzipped. The bytes inside - // it are this repository's test material; `NOTICE` carries the raw digest, - // the commands, and the fsck that called it clean. + // bcachefs volumes upstream's own tools wrote, gzipped. The bytes inside + // them are this repository's test material; `NOTICE` carries each raw + // digest, the commands, and the fsck that called it clean. ( "lib/bcachefs/tests/fixtures/crc32c.img.gz", "7be2c99db0e68c784ea59ef394454a084fa57868b14027528ab6b8b21031840e", "NOTICE", Terms::Spdx("MIT OR Apache-2.0"), ), + ( + "lib/bcachefs/tests/fixtures/encrypted.img.gz", + "2a32529edf3e30de75d02cdc4ad0b2060552555b012e9647dc24702c0ee7f82d", + "NOTICE", + Terms::Spdx("MIT OR Apache-2.0"), + ), + ( + "lib/bcachefs/tests/fixtures/encrypted-wide-macs.img.gz", + "fd137a545190906ded0269eeec3e32835ffe3585d3e4c2cebe84d234a96b1b1f", + "NOTICE", + Terms::Spdx("MIT OR Apache-2.0"), + ), // FAT32 volumes macOS's `newfs_msdos` formatted and its `msdosfs` // populated, as runs of nonzero sectors. Each boot sector carries // `newfs_msdos`'s boot stub; `NOTICE` carries its notice, each raw digest diff --git a/issues/bcachefs-crate-is-not-bcachefs.md b/issues/bcachefs-crate-is-not-bcachefs.md index bfa39185346..8ec8f86e94c 100644 --- a/issues/bcachefs-crate-is-not-bcachefs.md +++ b/issues/bcachefs-crate-is-not-bcachefs.md @@ -32,10 +32,15 @@ default filesystem.** The name stops being wrong by the crate growing into it. This entry therefore stops being a question and becomes work: what is owed is the implementation, and the gap between the two formats recorded above is the measure -of it. The upstream research this tree used to carry went with the documents, so -whoever picks this up reads the format out of upstream's own source and -specification again — that reading is part of the work now, not a head start on -it. +of it. The format is implemented clean-room (root `CLAUDE.md`): an analyst reads +upstream and writes the specification, and a different agent implements from it. + +**Where it stands.** `lib/bcachefs/src/engine` reads clean single-device 1.39 +volumes, crc32c and ChaCha20/Poly1305, and formats a minimal complete volume that +upstream's `bcachefs fsck -n -K` passes. It replaced a read side written from +upstream's headers. Its tests read volumes upstream's tools wrote +(`lib/bcachefs/tests/fixtures/`). The rest of the crate is still the interim +format, which ROOT, the loader and DATA use until the engine replaces it. The track itself is not planned here. diff --git a/issues/deviceerror-classify-trusts-a-lying-transfererror.md b/issues/deviceerror-classify-trusts-a-lying-transfererror.md index 587f2389608..6e0d20f4332 100644 --- a/issues/deviceerror-classify-trusts-a-lying-transfererror.md +++ b/issues/deviceerror-classify-trusts-a-lying-transfererror.md @@ -56,8 +56,7 @@ and by the same rule would refuse `kernel/src/file_backing.rs`'s `impl` for the kernel's `BlockError`, the fileserver's for its `Attempted` (`system/fileserver/src/cache.rs`), the loader's `Outside` (`loader/src/microcode.rs`) and the crate's own test devices -(`lib/bcachefs/tests/crash.rs`, `lib/bcachefs/tests/integration.rs`, -`lib/bcachefs/tests/upstream_fixture.rs`). +(`lib/bcachefs/tests/crash.rs`, `lib/bcachefs/tests/integration.rs`). Implementing `TransferError` from outside is what the trait is *for*: a foreign block device is the only authority on whether its own transfer was attempted. Making `classify` crate-private has the identical problem, because diff --git a/issues/general-crates-are-published-and-toyos-consumes-them.md b/issues/general-crates-are-published-and-toyos-consumes-them.md index f6a1c4e25a6..38a3ee9522c 100644 --- a/issues/general-crates-are-published-and-toyos-consumes-them.md +++ b/issues/general-crates-are-published-and-toyos-consumes-them.md @@ -14,7 +14,7 @@ opened: 2026-10-11 **Candidates** (scan of the restructured tree, 2026-10-11), best first: 1. `toyos-fat32-check`: FAT32 checker written from Microsoft's fatgen103. No ToyOS dependencies, `no_std`. 2. `toyos-fat32`: FAT32 read/write hardened against hostile volumes. It needs `toyos-wallclock` folded in, and its tests made self-contained. -3. The bcachefs `upstream` reader, split out of `lib/bcachefs`. +3. The bcachefs engine (`lib/bcachefs/src/engine`), split out of `lib/bcachefs` once the interim format leaves it. 4. `toyos-microcode` and `toyos-usbhid`. 5. Later: the TCP stack (once `wire`'s grab-bag is factored out), the SSH server (once its track and defect issues close), and the AML interpreter (once it is decoupled from `toyos-acpi`). diff --git a/lib/bcachefs/Cargo.toml b/lib/bcachefs/Cargo.toml index 3c5177cae10..bd26285f07d 100644 --- a/lib/bcachefs/Cargo.toml +++ b/lib/bcachefs/Cargo.toml @@ -1,10 +1,14 @@ [package] name = "bcachefs" -description = "The /home filesystem: a read side of the real bcachefs on-disk format." +description = "bcachefs: the on-disk format's reader, mkfs and check, and the interim ToyOS format they replace." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" +[dependencies] +toyos-blockdev = { path = "../blockdev" } +toyos-crypto = { path = "../crypto" } + [features] default = ["std"] std = [] diff --git a/lib/bcachefs/src/crc32c.rs b/lib/bcachefs/src/crc32c.rs index 90accb89543..c80e4dea65a 100644 --- a/lib/bcachefs/src/crc32c.rs +++ b/lib/bcachefs/src/crc32c.rs @@ -26,12 +26,21 @@ const CRC32C_TABLE: [u32; 256] = { /// Compute CRC-32c over a byte slice. pub fn crc32c(data: &[u8]) -> u32 { - let mut crc = 0xFFFF_FFFFu32; + update(0xFFFF_FFFF, data) ^ 0xFFFF_FFFF +} + +/// The same register with no initial or final inversion: bcachefs's data +/// checksum type 5, whose value over zeros is 0. +pub fn crc32c_zero(data: &[u8]) -> u32 { + update(0, data) +} + +fn update(mut crc: u32, data: &[u8]) -> u32 { for &byte in data { let idx = ((crc ^ byte as u32) & 0xFF) as usize; crc = (crc >> 8) ^ CRC32C_TABLE[idx]; } - crc ^ 0xFFFF_FFFF + crc } #[cfg(test)] @@ -46,6 +55,12 @@ mod tests { assert_eq!(crc32c(b"123456789"), 0xE3069283); } + #[test] + fn the_uninverted_register_has_its_own_check_value() { + assert_eq!(crc32c_zero(b"123456789"), 0x58E3_FA20); + assert_eq!(crc32c_zero(&[0; 4096]), 0); + } + #[test] fn zeroes() { // CRC of all zeroes should be deterministic diff --git a/lib/bcachefs/src/engine/bkey.rs b/lib/bcachefs/src/engine/bkey.rs new file mode 100644 index 00000000000..22f1e843206 --- /dev/null +++ b/lib/bcachefs/src/engine/bkey.rs @@ -0,0 +1,250 @@ +//! Positions, key headers and the node's key format. + +use super::{le32, le64, put32, put64, Error, Result}; + +/// A key's position, compared as the integer (inode, offset, snapshot). +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Bpos { + pub inode: u64, + pub offset: u64, + pub snapshot: u32, +} + +impl Bpos { + pub const MIN: Bpos = Bpos { inode: 0, offset: 0, snapshot: 0 }; + pub const MAX: Bpos = Bpos { inode: u64::MAX, offset: u64::MAX, snapshot: u32::MAX }; + + pub const fn new(inode: u64, offset: u64, snapshot: u32) -> Self { + Bpos { inode, offset, snapshot } + } + + /// The next position, carrying from snapshot into offset into inode. + /// `None` after [`Bpos::MAX`]. + pub fn successor(self) -> Option { + match (self.snapshot.checked_add(1), self.offset.checked_add(1), self.inode.checked_add(1)) { + (Some(snapshot), _, _) => Some(Bpos { snapshot, ..self }), + (None, Some(offset), _) => Some(Bpos { offset, snapshot: 0, ..self }), + (None, None, Some(inode)) => Some(Bpos { inode, offset: 0, snapshot: 0 }), + (None, None, None) => None, + } + } + + pub(crate) fn decode(b: &[u8]) -> Bpos { + Bpos { snapshot: le32(b, 0), offset: le64(b, 4), inode: le64(b, 12) } + } + + pub(crate) fn encode(self, b: &mut [u8]) { + put32(b, 0, self.snapshot); + put64(b, 4, self.offset); + put64(b, 12, self.inode); + } +} + +/// A key's 96-bit version. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +pub struct Version { + pub hi: u32, + pub lo: u64, +} + +/// A key's header, unpacked: its type, version, extent size and position. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Key { + pub kind: u8, + pub version: Version, + /// An extent's length in sectors, 0 for every other key. + pub size: u32, + pub pos: Bpos, +} + +/// u64s of an unpacked key header. +pub(crate) const KEY_U64S: usize = 5; +/// Format byte of an unpacked key. +const UNPACKED: u8 = 1; + +impl Key { + pub(crate) fn new(kind: u8, pos: Bpos) -> Self { + Key { kind, version: Version::default(), size: 0, pos } + } + + /// The unpacked key followed by `value`, zero-padded to whole u64s. + pub(crate) fn encode(&self, value: &[u8]) -> alloc::vec::Vec { + let words = KEY_U64S + value.len().div_ceil(8); + let mut b = alloc::vec![0u8; words * 8]; + b[0] = u8::try_from(words).expect("a value no key can carry"); + b[1] = UNPACKED; + b[2] = self.kind; + put64(&mut b, 4, self.version.lo); + put32(&mut b, 12, self.version.hi); + put32(&mut b, 16, self.size); + self.pos.encode(&mut b[20..40]); + b[40..40 + value.len()].copy_from_slice(value); + b + } +} + +/// Which field of a packed key, in the order a format lists them. +const FIELDS: usize = 6; +/// Each field's unpacked width in bits: inode, offset, snapshot, size, +/// version high, version low. +const WIDTH: [u32; FIELDS] = [64, 64, 32, 32, 32, 64]; + +/// A node's key format: how wide each packed field is and what it is +/// offset from. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct Format { + key_u64s: u8, + bits: [u8; FIELDS], + offset: [u64; FIELDS], +} + +impl Format { + /// Every key in format 1, the unpacked one, which is valid in any node. + pub(crate) const UNPACKED: Format = Format { key_u64s: 5, bits: [64, 64, 32, 32, 32, 64], offset: [0; FIELDS] }; + + pub(crate) fn decode(b: &[u8]) -> Result { + if b[1] as usize != FIELDS { + return Err(Error::Corrupt("key format without six fields")); + } + let bits: [u8; FIELDS] = b[2..8].try_into().unwrap(); + let offset: [u64; FIELDS] = core::array::from_fn(|i| le64(b, 8 + 8 * i)); + let mut total = 24; + for i in 0..FIELDS { + let (bits, width) = (u32::from(bits[i]), WIDTH[i]); + let max = if width == 64 { u64::MAX } else { (1 << width) - 1 }; + let span = if bits >= 64 { u64::MAX } else { (1 << bits) - 1 }; + if bits > width || (bits == width && offset[i] != 0) || offset[i].checked_add(span).is_none_or(|top| top > max) { + return Err(Error::Corrupt("key format field out of range")); + } + total += bits; + } + if u32::from(b[0]) != total.div_ceil(64) { + return Err(Error::Corrupt("key format size disagrees with its fields")); + } + Ok(Format { key_u64s: b[0], bits, offset }) + } + + pub(crate) fn encode(&self, b: &mut [u8]) { + b[0] = self.key_u64s; + b[1] = FIELDS as u8; + b[2..8].copy_from_slice(&self.bits); + for (i, offset) in self.offset.iter().enumerate() { + put64(b, 8 + 8 * i, *offset); + } + } + + /// The key whose bytes begin `b`, and its value. `b` runs to the end of + /// its bset. + pub(crate) fn key<'a>(&self, b: &'a [u8]) -> Result<(Key, &'a [u8])> { + if b.len() < 8 { + return Err(Error::Corrupt("key past the end of its bset")); + } + let len = usize::from(b[0]) * 8; + let kind = b[2]; + let (key, header) = match b[1] & 0x7f { + 0 => (self.unpack(b, len)?, usize::from(self.key_u64s) * 8), + UNPACKED => (unpacked(b, len)?, KEY_U64S * 8), + _ => return Err(Error::Corrupt("key in an unknown format")), + }; + Ok((Key { kind, ..key }, &b[header..len])) + } + + fn unpack(&self, b: &[u8], len: usize) -> Result { + let words = usize::from(self.key_u64s); + if len < words * 8 || len > b.len() || words == 0 { + return Err(Error::Corrupt("packed key past the end of its bset")); + } + let word = |i: usize| le64(b, 8 * i); + // The key is one little-endian integer of `words` words; fields run + // from its top bit down. + let mut top = words as u32 * 64; + let value: [u64; FIELDS] = core::array::from_fn(|i| { + let bits = u32::from(self.bits[i]); + top -= bits; + take(&word, top, bits) + self.offset[i] + }); + Ok(Key { + kind: 0, + pos: Bpos { inode: value[0], offset: value[1], snapshot: value[2] as u32 }, + size: value[3] as u32, + version: Version { hi: value[4] as u32, lo: value[5] }, + }) + } +} + +/// Bits `[lo, lo + len)` of the little-endian integer `word(0), word(1), …`. +fn take(word: &impl Fn(usize) -> u64, lo: u32, len: u32) -> u64 { + if len == 0 { + return 0; + } + let (i, shift) = ((lo / 64) as usize, lo % 64); + let mut v = word(i) >> shift; + if shift + len > 64 { + v |= word(i + 1) << (64 - shift); + } + if len < 64 { v & ((1 << len) - 1) } else { v } +} + +fn unpacked(b: &[u8], len: usize) -> Result { + if len < KEY_U64S * 8 || len > b.len() { + return Err(Error::Corrupt("unpacked key past the end of its bset")); + } + Ok(Key { + kind: 0, + version: Version { lo: le64(b, 4), hi: le32(b, 12) }, + size: le32(b, 16), + pos: Bpos::decode(&b[20..40]), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn successor_carries_from_snapshot_through_offset_into_inode() { + assert_eq!(Bpos::new(1, 2, 3).successor(), Some(Bpos::new(1, 2, 4))); + assert_eq!(Bpos::new(1, 2, u32::MAX).successor(), Some(Bpos::new(1, 3, 0))); + assert_eq!(Bpos::new(1, u64::MAX, u32::MAX).successor(), Some(Bpos::new(2, 0, 0))); + assert_eq!(Bpos::MAX.successor(), None); + } + + #[test] + fn an_unpacked_key_reads_back() { + let key = Key { kind: 6, version: Version { hi: 7, lo: 1 << 40 }, size: 32, pos: Bpos::new(0x8000_0003, 64, u32::MAX) }; + let bytes = key.encode(&[0xab; 9]); + assert_eq!(bytes.len(), 56); + let (read, value) = Format::UNPACKED.key(&bytes).unwrap(); + assert_eq!(read, key); + assert_eq!(value, &[[0xab; 9].as_slice(), &[0; 7]].concat()[..]); + } + + /// An upstream alloc leaf's format: one word, offset 16 bits + /// from 82 and snapshot 8 bits. The fields fill the word from its top. + #[test] + fn a_packed_key_reads_its_fields_from_the_top_bit_down() { + let mut format = [0u8; 56]; + format[0] = 1; + format[1] = 6; + format[2..8].copy_from_slice(&[0, 16, 8, 0, 0, 0]); + format[16..24].copy_from_slice(&82u64.to_le_bytes()); + let format = Format::decode(&format).unwrap(); + let word: u64 = 1 | (27 << 16) | (5u64 << 40) | ((1991 - 82) << 48); + // u64s 1, format 0 (packed), type 27. + let bytes = word.to_le_bytes(); + let (key, value) = format.key(&bytes).unwrap(); + assert_eq!((key.kind, key.pos, value.len()), (27, Bpos::new(0, 1991, 5), 0)); + } + + #[test] + fn a_format_whose_size_disagrees_with_its_fields_is_refused() { + let mut format = [0u8; 56]; + Format::UNPACKED.encode(&mut format); + assert_eq!(Format::decode(&format), Ok(Format::UNPACKED)); + format[0] = 4; + assert!(Format::decode(&format).is_err()); + Format::UNPACKED.encode(&mut format); + format[8] = 1; // inode offset at full width + assert!(Format::decode(&format).is_err()); + } +} diff --git a/lib/bcachefs/src/engine/crypt.rs b/lib/bcachefs/src/engine/crypt.rs new file mode 100644 index 00000000000..d9dac9b2526 --- /dev/null +++ b/lib/bcachefs/src/engine/crypt.rs @@ -0,0 +1,171 @@ +//! bcachefs's encryption: raw ChaCha20 at a chosen block counter, and +//! Poly1305 keyed from a separate nonce domain over the ciphertext alone. +//! This is not RFC 8439's AEAD. + +use alloc::vec; +use toyos_crypto::{chacha, poly1305, scrypt}; + +use super::superblock::{Crypt, Kdf}; +use super::{Error, Result}; + +/// The magic a correct unwrap yields, "bch**key". +const KEY_MAGIC: [u8; 8] = *b"bch**key"; +/// scrypt's salt, which the format fixes. +const SALT: &[u8] = b"bcache\0"; + +/// The smallest scrypt parameters the format admits: N = 2, +/// r = p = 1. For a high-entropy passphrase scrypt adds nothing. +pub const MIN_KDF: Kdf = Kdf { log_n: 1, log_r: 0, log_p: 0 }; + +/// Each structure's domain, XORed into its nonce's last word. +#[derive(Clone, Copy)] +pub(crate) enum Domain { + Extent = 0x1000_0000, + Node = 0x2000_0000, +} +/// Selects the MAC key's nonce. +const MAC: u32 = 0x8000_0000; + +/// A nonce: `d0` is ChaCha20's first block, `d1`–`d3` its 96-bit nonce. +#[derive(Clone, Copy)] +pub(crate) struct Nonce([u32; 4]); + +impl Nonce { + /// The record at byte `offset` of node `seq`, whose bset is + /// stamped `journal_seq`. + pub(crate) fn node(offset: u32, seq: u64, journal_seq: u64) -> Nonce { + Nonce([offset, seq as u32, (seq >> 32) as u32, journal_seq as u32 ^ Domain::Node as u32]) + } + + /// An uncompressed extent of version `(hi, lo)` whose crc + /// nonce field is `crc_nonce`. + pub(crate) fn extent(hi: u32, lo: u64, crc_nonce: u32) -> Nonce { + Nonce([8 * crc_nonce, lo as u32, (lo >> 32) as u32, hi ^ Domain::Extent as u32]) + } + + /// This nonce `blocks` ChaCha20 blocks on. + pub(crate) fn plus(self, blocks: u32) -> Nonce { + let [d0, d1, d2, d3] = self.0; + Nonce([d0 + blocks, d1, d2, d3]) + } + + fn parts(self) -> (u32, [u8; 12]) { + let [d0, d1, d2, d3] = self.0; + let mut n = [0u8; 12]; + n[..4].copy_from_slice(&d1.to_le_bytes()); + n[4..8].copy_from_slice(&d2.to_le_bytes()); + n[8..].copy_from_slice(&d3.to_le_bytes()); + (d0, n) + } +} + +/// A volume's master key, decrypted. It never reaches the medium unwrapped. +pub(crate) struct MasterKey([u8; 32]); + +impl Drop for MasterKey { + fn drop(&mut self) { + erase(&mut self.0); + } +} + +/// Overwrites a secret before it goes out of scope. `black_box` makes the +/// compiler treat the zeros as read, so the stores are not elided. +fn erase(secret: &mut [u8; N]) { + *secret = [0; N]; + core::hint::black_box(secret); +} + +impl MasterKey { + pub(crate) fn new(key: [u8; 32]) -> Self { + MasterKey(key) + } + + /// `data` XORed with the keystream from `nonce`'s first block. + pub(crate) fn xor(&self, nonce: Nonce, data: &mut [u8]) -> Result<()> { + let (counter, n) = nonce.parts(); + chacha::xor(&self.0, &n, counter, data).map_err(|_| Error::Corrupt("keystream past its counter")) + } + + fn mac_key(&self, nonce: Nonce) -> Result<[u8; 32]> { + let [d0, d1, d2, d3] = nonce.0; + let mut key = [0u8; 32]; + self.xor(Nonce([d0, d1, d2, d3 ^ MAC]), &mut key)?; + Ok(key) + } + + /// The 128-bit MAC of `data` under `nonce`. + pub(crate) fn mac(&self, nonce: Nonce, data: &[u8]) -> Result<[u8; 16]> { + let mut key = self.mac_key(nonce)?; + let tag = poly1305::tag(&key, data); + erase(&mut key); + Ok(tag) + } + + /// `Ok` when `tag`, 10 or 16 bytes, is `data`'s MAC under `nonce`. + pub(crate) fn verify(&self, nonce: Nonce, data: &[u8], tag: &[u8], what: &'static str) -> Result<()> { + let mut key = self.mac_key(nonce)?; + let verdict = poly1305::verify(&key, data, tag).map_err(|_| Error::Checksum(what)); + erase(&mut key); + verdict + } + + /// The key of a volume formatted without a passphrase, which + /// `crypt` holds in clear behind the magic. + pub(crate) fn stored(crypt: &Crypt) -> Option { + (crypt.wrapped[..8] == KEY_MAGIC).then(|| MasterKey(crypt.wrapped[8..].try_into().unwrap())) + } + + /// `crypt`'s key unwrapped under `passphrase`, its scrypt within + /// `limits`. + pub(crate) fn unwrap(crypt: &Crypt, uuid: &[u8; 16], passphrase: &[u8], limits: scrypt::Limits) -> Result { + let mut key = passphrase_key(passphrase, crypt.kdf, limits)?; + let mut bytes = crypt.wrapped; + let unwrapped = wrap(&key, uuid, &mut bytes); + erase(&mut key); + unwrapped?; + let master = (bytes[..8] == KEY_MAGIC).then(|| MasterKey(bytes[8..].try_into().unwrap())); + erase(&mut bytes); + master.ok_or(Error::Passphrase) + } + + /// `crypt`'s 40 wrapped bytes: the magic and this key under + /// `passphrase`'s key. + pub(crate) fn wrapped(&self, uuid: &[u8; 16], passphrase: &[u8], kdf: Kdf) -> Result<[u8; 40]> { + let mut key = passphrase_key(passphrase, kdf, limits(kdf)?)?; + let mut out = [0u8; 40]; + out[..8].copy_from_slice(&KEY_MAGIC); + out[8..].copy_from_slice(&self.0); + let wrapped = wrap(&key, uuid, &mut out); + erase(&mut key); + wrapped.map(|()| out) + } +} + +/// The limits that admit exactly `kdf`: a caller formatting a volume chose +/// its cost. A cost past `usize` saturates, and scrypt refuses it. +fn limits(kdf: Kdf) -> Result { + let (n, r, p) = kdf.values()?; + let memory = 128 * u128::from(r) * (u128::from(n) + 2); + let work = u128::from(n) * u128::from(r) * u128::from(p); + Ok(scrypt::Limits { memory: memory.min(usize::MAX as u128) as usize, work: work.min(u128::from(u64::MAX)) as u64 }) +} + +/// scrypt of the passphrase with the format's salt: the key that wraps +/// the master key. +fn passphrase_key(passphrase: &[u8], kdf: Kdf, limits: scrypt::Limits) -> Result<[u8; 32]> { + let (_, r, p) = kdf.values()?; + let log_n = u8::try_from(kdf.log_n).ok().filter(|&n| n < 32).ok_or(Error::Unsupported("scrypt N past 2^31"))?; + let params = scrypt::Params::new(log_n, r, p, limits).map_err(|_| Error::Unsupported("scrypt parameters past RFC 7914's or the caller's limits"))?; + let mut workspace = vec![0u32; params.workspace_len()]; + let mut key = [0u8; 32]; + scrypt::derive(passphrase, SALT, params, &mut workspace, &mut key).map_err(|_| Error::Unsupported("scrypt parameters"))?; + Ok(key) +} + +/// The XOR of the 40 key bytes with `key`'s keystream at the fixed +/// nonce the internal UUID gives. +fn wrap(key: &[u8; 32], uuid: &[u8; 16], bytes: &mut [u8; 40]) -> Result<()> { + let mut nonce = [0u8; 12]; + nonce[4..].copy_from_slice(&uuid[..8]); + chacha::xor(key, &nonce, 0, bytes).map_err(|_| Error::Corrupt("keystream past its counter")) +} diff --git a/lib/bcachefs/src/engine/fsck.rs b/lib/bcachefs/src/engine/fsck.rs new file mode 100644 index 00000000000..fa6d41448e8 --- /dev/null +++ b/lib/bcachefs/src/engine/fsck.rs @@ -0,0 +1,197 @@ +//! ToyOS's check of a volume ToyOS wrote: every invariant the writer +//! maintains, grown with each stage that adds one. It is built from the +//! same facts as the writer, so it judges ToyOS's consistency with itself; +//! upstream's fsck is the independent judge. + +use alloc::vec::Vec; + +use toyos_blockdev::{BlockDev, BLOCK}; + +use super::bkey::Bpos; +use super::mkfs::{self, COMPAT_CLEAN, COMPAT_INITIALIZED, DOWNGRADE, EXTENT_TYPE_U64S, FEATURES}; +use super::read::{read_bytes, Volume}; +use super::superblock::{field_type, member_flag, Superblock, HEADER}; +use super::value::{self, data_type, inode_field, InodeValue, NodePtr, Subvolume, SUBVOLUME_LIVE}; +use super::{kind, Btree, Result, SECTOR, VERSION}; + +/// One way a volume departs from what ToyOS writes. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Finding { + /// A superblock rule ToyOS writes by, by name. + Superblock(&'static str), + /// A superblock copy that differs from the chosen one in more than its + /// own offset and checksum, by its sector. + Copy(u64), + /// The root subvolume, snapshot, tree, directory or inode cursor + /// missing or not as mkfs writes them, by name. + Root(&'static str), + /// A node outside the member's btree bitmap. + Bitmap { btree: Btree, sector: u64 }, + /// A bset stamped with a journal sequence past `clean`'s. + BsetAboveClean { btree: Btree, sector: u64, seq: u64 }, + /// An inode stamped with a journal sequence past `clean`'s. + InodeAboveClean { ino: u64, seq: u64 }, + /// A key of a type its btree, at that level, does not hold. The reader + /// passes the non-strict types; ToyOS writes none out of place. + KeyType { btree: Btree, level: u8, kind: u8, pos: Bpos }, +} + +/// Everything wrong with `volume`; empty when nothing is. +pub async fn fsck(volume: &Volume) -> Result> { + let sb = volume.superblock(); + let mut found = superblock(sb); + found.extend(copies(volume.dev(), sb).await?); + found.extend(root_world(volume).await?); + found.extend(nodes(volume).await?); + Ok(found) +} + +/// What every ToyOS-written superblock carries. +fn superblock(sb: &Superblock) -> Vec { + let h = &sb.header; + let clean = sb.clean_flag(); + let ext = sb.field(field_type::EXT).unwrap_or(&[]); + let flags = sb.member.flags; + let expected_features = FEATURES | if sb.blacklist.is_empty() { 0 } else { 1 << 5 }; + let expected_compat = COMPAT_INITIALIZED | if clean { COMPAT_CLEAN } else { 0 }; + let has_replicas = |t: u8| sb.replicas.iter().any(|r| r.data_type == t && r.devs == [0] && r.required == 1); + let padded = |kind: u32, want: &[u8]| { + sb.field(kind).is_some_and(|b| b.len() >= want.len() && b[..want.len()] == *want && b[want.len()..].iter().all(|&x| x == 0)) + }; + let rules: [(bool, &'static str); 26] = [ + (h.version == VERSION && h.version_min == VERSION, "version and minimum version 1.39"), + (sb.flag(5, 32, 16) == 1 << 10 | 36, "version incompat 1.36"), + (sb.flag(5, 48, 16) == u64::from(VERSION), "version incompat allowed 1.39"), + (sb.flag(5, 0, 16) == u64::from(VERSION), "version upgrade complete 1.39"), + (ext.len() >= 80 && ext[..80].iter().all(|&b| b == 0), "no recovery pass required and no error silenced"), + (sb.flag(0, 0, 1) == 1, "initialized"), + (sb.flag(0, 60, 2) == 0, "no errors or topology errors recorded"), + (sb.flag(6, 0, 4) == 0, "inode shard bits 0"), + (sb.flag(6, 40, 8) == 16, "extent backpointer shift 16"), + (sb.flag(1, 14, 6) == 9, "encoded extent maximum 256 KiB"), + (sb.flag(0, 40, 4) == 1 && sb.flag(0, 44, 4) == 1, "crc32c metadata and data checksum options"), + (sb.flag(1, 10, 4) <= 1 && (sb.flag(1, 10, 4) == 0 || sb.flag(1, 9, 1) == 1), "encrypted only as ChaCha20/Poly1305 with wide_macs"), + (h.features == [expected_features, 0], "format's feature set"), + (h.compat == [expected_compat, 0], "compat bits 2-5, and 0-1 exactly when clean"), + (has_replicas(data_type::BTREE), "btree replicas entry"), + (has_replicas(data_type::JOURNAL) != clean, "journal replicas entry exactly while dirty"), + (flags & member_flag::FREESPACE_INITIALIZED != 0 && flags & member_flag::INIT_STATE == 0, "member freespace initialized"), + (flags & member_flag::DISCARD == 0, "member discard off"), + (flags & member_flag::ROTATIONAL == 0 && flags & member_flag::ROTATIONAL_KNOWN != 0, "member not rotational, and known"), + (sb.member.identity_zero, "member device name, model and serial zero"), + (padded(field_type::DOWNGRADE, &DOWNGRADE), "downgrade field of 1.39 above 1.36"), + (padded(field_type::EXTENT_TYPE_U64S, &EXTENT_TYPE_U64S), "extent entry sizes of 1.39"), + (sb.field(field_type::COUNTERS).is_none(), "no counters field"), + (sb.field(field_type::ERRORS_V2).is_none(), "no errors_v2 field"), + (sb.field(field_type::RECOVERY_PASSES).is_none(), "no recovery_passes field"), + (!clean || sb.clean.as_ref().is_some_and(|c| c.key_version.is_some()), "clean carries the key version"), + ]; + rules.iter().filter(|(ok, _)| !ok).map(|&(_, rule)| Finding::Superblock(rule)).collect() +} + +/// Every copy is the chosen one's bytes but for its own offset and +/// checksum. +async fn copies(dev: &impl BlockDev, sb: &Superblock) -> Result> { + let len = HEADER + 8 * sb.fields.iter().map(|f| 1 + f.body.len().div_ceil(8)).sum::(); + let reference = Superblock::encode(&sb.header, &sb.fields, 0); + let mut found = Vec::new(); + let device = dev.blocks() * BLOCK as u64; + for &offset in &sb.header.layout.offsets { + let fits = offset.checked_mul(SECTOR as u64).and_then(|at| at.checked_add(len as u64)).is_some_and(|end| end <= device); + if !fits { + found.push(Finding::Copy(offset)); + continue; + } + let mut copy = read_bytes(dev, offset * SECTOR as u64, len).await?; + copy[..16].fill(0); + copy[104..112].fill(0); + let mut want = reference.clone(); + want[..16].fill(0); + if copy != want { + found.push(Finding::Copy(offset)); + } + } + Ok(found) +} + +/// The root world, as mkfs writes it. +async fn root_world(v: &Volume) -> Result> { + let mut found = Vec::new(); + let mut check = |ok: bool, what| { + if !ok { + found.push(Finding::Root(what)); + } + }; + let snapshot = mkfs::ROOT_SNAPSHOT; + let subvolume = v.get(Btree::Subvolumes, Bpos::new(0, u64::from(mkfs::ROOT_SUBVOLUME), 0)).await?; + let subvolume = subvolume.filter(|(k, _)| k.kind == kind::SUBVOLUME).map(|(_, v)| Subvolume::decode(&v)).transpose()?; + check( + subvolume.is_some_and(|s| s.snapshot == snapshot && s.inode == mkfs::ROOT_INODE && s.state == SUBVOLUME_LIVE && s.fs_path_parent == 0), + "root subvolume", + ); + let tree = v.get(Btree::Snapshots, Bpos::new(0, u64::from(snapshot), 0)).await?; + check( + tree.is_some_and(|(k, val)| k.kind == kind::SNAPSHOT && val == value::snapshot(mkfs::ROOT_SUBVOLUME, mkfs::SNAPSHOT_TREE)), + "root snapshot", + ); + let snapshot_tree = v.get(Btree::SnapshotTrees, Bpos::new(0, u64::from(mkfs::SNAPSHOT_TREE), 0)).await?; + check( + snapshot_tree.is_some_and(|(k, val)| k.kind == kind::SNAPSHOT_TREE && val == value::snapshot_tree(mkfs::ROOT_SUBVOLUME, snapshot)), + "snapshot tree", + ); + let root = v.get(Btree::Inodes, Bpos::new(0, mkfs::ROOT_INODE, snapshot)).await?; + let root = root.filter(|(k, _)| k.kind == kind::INODE_V3).map(|(_, val)| InodeValue::decode(&val)).transpose()?; + check( + root.is_some_and(|r| { + r.mode() == 0o40755 + && r.size == 0 + && r.field(inode_field::SUBVOL) == u64::from(mkfs::ROOT_SUBVOLUME) + && r.field(inode_field::DIR) == 0 + && r.field(inode_field::DIR_OFFSET) == 0 + && r.field(inode_field::DEPTH) == 0 + }), + "root directory", + ); + let cursor = v.get(Btree::LoggedOps, Bpos::new(1, 1, 0)).await?; + check(cursor.is_some_and(|(k, _)| k.kind == kind::INODE_ALLOC_CURSOR), "inode cursor"); + Ok(found) +} + +/// Every node: inside the bitmap, every key of a type its btree holds, and +/// no bset or inode stamped past +/// `clean`'s journal sequence: upstream's fsck reports an inode above it, +/// and upstream's read-write mount of such a volume loses it. +async fn nodes(v: &Volume) -> Result> { + let sb = v.superblock(); + let clean = sb.clean.as_ref().map_or(0, |c| c.journal_seq); + let node_sectors = u64::from(sb.node_sectors()); + let mut found = Vec::new(); + for root in v.roots() { + let mut pending = alloc::vec![(root.level, root.ptr, root.key.pos)]; + while let Some((level, ptr, max)) = pending.pop() { + let sector = ptr.ptr.sector; + if !sb.member.bitmap_covers(sector, sector + node_sectors) { + found.push(Finding::Bitmap { btree: root.btree, sector }); + } + let node = v.node(root.btree, level, &ptr, max).await?; + for &seq in node.bset_seqs.iter().filter(|&&s| s > clean) { + found.push(Finding::BsetAboveClean { btree: root.btree, sector, seq }); + } + for (key, value) in &node.keys { + if !root.btree.allows(level, key.kind) { + found.push(Finding::KeyType { btree: root.btree, level, kind: key.kind, pos: key.pos }); + continue; + } + if level > 0 { + pending.push((level - 1, NodePtr::decode(value)?, key.pos)); + } else if root.btree == Btree::Inodes && key.kind == kind::INODE_V3 { + let seq = InodeValue::decode(value)?.journal_seq; + if seq > clean { + found.push(Finding::InodeAboveClean { ino: key.pos.offset, seq }); + } + } + } + } + } + Ok(found) +} diff --git a/lib/bcachefs/src/engine/mkfs.rs b/lib/bcachefs/src/engine/mkfs.rs new file mode 100644 index 00000000000..3df41e64d27 --- /dev/null +++ b/lib/bcachefs/src/engine/mkfs.rs @@ -0,0 +1,721 @@ +//! A minimal complete volume: superblock copies, an empty +//! journal and `clean`, the root subvolume, snapshot, snapshot tree and +//! root directory, the inode cursor, and the allocator and accounting state +//! of exactly those nodes. Nothing it writes names a journal sequence, so +//! `clean`'s is 0. + +use alloc::vec::Vec; + +use toyos_blockdev::{BlockDev, BLOCK}; +use toyos_crypto::Random; + +use super::bkey::{Bpos, Key}; +use super::crypt::MasterKey; +use super::node::{self, Build}; +use super::superblock::{entry_type, field_type, member_flag, Crypt, Field, Header, Kdf, Layout, Member, Superblock, KEY_VERSION, LAYOUT, MAX_NODE_SECTORS, PRIMARY}; +use super::value::{self, class, data_type, inode_field, Alloc, InodeValue, NodePtr, Ptr}; +use super::{kind, put16, put64, Btree, Error, Result, SECTOR, VERSION}; + +/// What a volume is formatted with. +pub struct Options<'a> { + /// At most 31 bytes. + pub label: &'a [u8], + /// Nanoseconds since the Unix epoch: the volume's time base and every + /// time it records. + pub time: u64, + /// ChaCha20/Poly1305 with 128-bit MACs for data and metadata, its key + /// wrapped under a passphrase; `None` for crc32c. + pub encryption: Option>, +} + +/// How the master key is wrapped. +pub struct Encryption<'a> { + /// The exact bytes upstream's tools take as the passphrase: no NUL, and + /// no trailing newline, which they strip. + pub passphrase: &'a [u8], + pub kdf: Kdf, +} + +/// The root directory's inode number, the root subvolume's id and its +/// snapshot. +pub(crate) const ROOT_INODE: u64 = 4096; +pub(crate) const ROOT_SUBVOLUME: u32 = 1; +pub(crate) const ROOT_SNAPSHOT: u32 = u32::MAX; +pub(crate) const SNAPSHOT_TREE: u32 = 1; +/// The first inode number the cursor hands out. +pub(crate) const FIRST_INODE: u64 = 1 << 31; + +/// Each copy's reserve, 1 MiB as `format` lays it out. +const SB_MAX_BITS: u8 = 11; +const SB_SECTORS: u64 = 1 << SB_MAX_BITS; +/// Block size: the device's (`toyos_blockdev::BLOCK`), in sectors. +const BLOCK_SECTORS: u64 = (BLOCK / SECTOR) as u64; +/// Bucket sizes from 16 KiB to 2 MiB, at least 2 048 buckets where the +/// device allows. +const MIN_BUCKET: u64 = 32; +const MAX_BUCKET: u64 = 4096; +const WANT_BUCKETS: u64 = 2048; +const MIN_BUCKETS: u64 = 512; + +/// Every btree a v1 volume writes, each given a root here: a node count +/// that only grows with its keys keeps the layout's fixed point monotone. +const BTREES: [Btree; 15] = [ + Btree::Extents, + Btree::Inodes, + Btree::Dirents, + Btree::Alloc, + Btree::Subvolumes, + Btree::Snapshots, + Btree::Lru, + Btree::Freespace, + Btree::NeedDiscard, + Btree::Backpointers, + Btree::BucketGens, + Btree::SnapshotTrees, + Btree::DeletedInodes, + Btree::LoggedOps, + Btree::Accounting, +]; + +/// Writes the volume to `dev`, every identifier and key drawn from `random`, +/// then flushes. +pub async fn mkfs(dev: &impl BlockDev, options: &Options<'_>, random: &mut impl Random) -> Result<()> { + if options.label.len() > 31 { + return Err(Error::Invalid("label longer than 31 bytes")); + } + // Upstream's tools cut a passphrase at NUL and strip one trailing + // newline, so neither could unlock what such bytes wrapped. + if options.encryption.as_ref().is_some_and(|e| e.passphrase.contains(&0) || e.passphrase.ends_with(b"\n")) { + return Err(Error::Invalid("passphrase holding NUL or ending in a newline")); + } + let geometry = Geometry::new(dev.blocks() * BLOCK_SECTORS)?; + let mut draw = Draw(random); + let internal_uuid = draw.uuid()?; + let user_uuid = draw.uuid()?; + let member_uuid = draw.uuid()?; + let hash_seed = draw.u64()?; + let key = match &options.encryption { + Some(_) => Some(MasterKey::new(draw.bytes()?)), + None => None, + }; + + // The least node count that holds the keys of its own nodes, from below. + let mut nodes = BTREES.len() as u64; + let plan = loop { + let plan = Plan::new(&geometry, nodes, hash_seed)?; + if plan.nodes() == nodes { + break plan; + } + assert!(plan.nodes() > nodes, "the layout's node count shrank as its nodes grew"); + nodes = plan.nodes(); + }; + let seqs = (0..nodes).map(|_| draw.nonzero()).collect::>>()?; + let tree = plan.lay_out(&geometry, &seqs)?; + + let magic = node::magic(&internal_uuid); + for n in &tree.nodes { + let build = Build { btree: n.btree, level: n.level, seq: n.seq, min: n.min, max: n.max, journal_seq: 0, keys: &n.keys }; + let bytes = node::encode(magic, BLOCK, key.as_ref(), &build)?; + write(dev, n.sector, &bytes).await?; + } + + let member = Member { + uuid: member_uuid, + nbuckets: geometry.nbuckets, + first_bucket: 0, + bucket_sectors: geometry.bucket as u16, + bitmap_shift: tree.bitmap_shift, + last_mount: options.time / 1_000_000_000, + flags: member_flag::DATA_ALLOWED | member_flag::DURABILITY_1 | member_flag::FREESPACE_INITIALIZED | member_flag::ROTATIONAL_KNOWN, + seq: 1, + bitmap: tree.bitmap, + journal_bucket: 0, + journal_offset: 0, + identity_zero: true, + }; + let crypt = match (&options.encryption, &key) { + (Some(e), Some(key)) => Some(Crypt { kdf: e.kdf, wrapped: key.wrapped(&internal_uuid, e.passphrase, e.kdf)? }), + _ => None, + }; + let mut label = [0u8; 32]; + label[..options.label.len()].copy_from_slice(options.label); + let header = Header { + version: VERSION, + version_min: VERSION, + internal_uuid, + user_uuid, + label, + offset: 0, + seq: 1, + block_sectors: BLOCK_SECTORS as u16, + dev_idx: 0, + nr_devices: 1, + time_base: options.time, + time_precision: 1, + flags: flags(geometry.node, crypt.is_some()), + write_time: options.time / 1_000_000_000, + features: [FEATURES, 0], + compat: [COMPAT_INITIALIZED | COMPAT_CLEAN, 0], + layout: geometry.layout.clone(), + }; + let fields = fields(&geometry, &member, crypt.as_ref(), &tree.roots); + for &offset in &geometry.layout.offsets { + write(dev, offset, &Superblock::encode(&header, &fields, offset)).await?; + } + // Sectors 0–6 are zeros here; the layout copy is sector 7. + let mut first = alloc::vec![0u8; BLOCK]; + let at = LAYOUT as usize * SECTOR; + first[at..at + Layout::BYTES].copy_from_slice(&geometry.layout.encode()); + write(dev, 0, &first).await?; + dev.flush().await?; + Ok(()) +} + +/// The feature bits `format` sets, all of a clean volume's. +pub(crate) const FEATURES: u64 = 0x000f_ba80; +/// Compat bits 2–5, set at initialization; 0–1 at a clean shutdown. +pub(crate) const COMPAT_INITIALIZED: u64 = 0x3c; +pub(crate) const COMPAT_CLEAN: u64 = 0x3; + +/// Flag words 0–6 as `format` sets them, clean and +/// initialized, at node size `node` sectors. +fn flags(node: u64, encrypted: bool) -> [u64; 7] { + let word0 = 1 // initialized + | 1 << 1 // clean + | 1 << 2 // superblock checksum crc32c + | 1 << 8 // error action fix_safe + | node << 12 // btree node size, sectors + | 8 << 28 // copygc reserve, percent + | 1 << 40 // metadata checksum crc32c + | 1 << 44 // data checksum crc32c + | 1 << 48 // metadata replicas wanted + | 1 << 52 // data replicas wanted + | 1 << 56 // POSIX ACLs + | 1 << 63; // promote whole extents + let encryption = if encrypted { 1 << 9 | 1 << 10 } else { 0 }; // wide_macs, ChaCha20/Poly1305 + let word1 = 2 // string hash siphash + | encryption + | 9 << 14 // encoded extent max, 2^9 sectors + | 1 << 20 // metadata replicas required + | 1 << 24; // data replicas required + let word3 = 1000 << 30; // journal flush delay, ms + let word4 = 100 | 1 << 32; // journal reclaim delay, ms; transaction names + let incompat = 1 << 10 | 36; // 1.36: no user replicas in the superblock + let word5 = u64::from(VERSION) | 30 << 16 | incompat << 32 | u64::from(VERSION) << 48; + let word6 = 30 << 4 | 3 << 14 | 16 << 40; // write error timeout, checksum retries, backpointer shift + [word0, word1, 0, word3, word4, word5, word6] +} + +/// `ext`'s extended flags as `format` writes them. +const EXT_FLAGS: u64 = 0x0900_2850_1000; +/// The downgrade field of 1.39 with incompat 1.36: the entries for 1.38 and +/// 1.39 that upstream regenerates on every superblock write. +pub(crate) const DOWNGRADE: [u8; 52] = [ + 0x26, 0x04, 0x00, 0x04, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x03, 0x00, 0x7c, 0x00, 0x72, 0x00, 0x7b, 0x00, + 0x27, 0x04, 0x20, 0x88, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x03, 0x00, 0x83, 0x00, 0x69, 0x00, 0x10, 0x01, +]; +/// Each extent entry type's size in u64s, pointer through reconcile_bp. +pub(crate) const EXTENT_TYPE_U64S: [u8; 9] = [1, 1, 2, 3, 1, 1, 1, 1, 1]; + +fn fields(geometry: &Geometry, member: &Member, crypt: Option<&Crypt>, roots: &[(Btree, u8, Vec)]) -> Vec { + let mut ext = alloc::vec![0u8; 14 * 8]; + put64(&mut ext, 11 * 8, EXT_FLAGS); + let mut members = alloc::vec![0u8; 8]; + put16(&mut members, 0, Member::BYTES as u16); + members.extend_from_slice(&member.encode()); + let mut journal = alloc::vec![0u8; 16]; + put64(&mut journal, 0, geometry.journal.0); + put64(&mut journal, 8, geometry.journal.1); + + // `clean`: sequence 0, the key version, both clocks at 0, every root. + let mut clean = alloc::vec![0u8; 16]; + record(&mut clean, KEY_VERSION, 0, entry_type::USAGE, &0u64.to_le_bytes()); + for rw in 0..2u8 { + let mut clock = [0u8; 16]; + clock[0] = rw; + record(&mut clean, 0, 0, entry_type::CLOCK, &clock); + } + for (btree, level, key) in roots { + record(&mut clean, *btree as u8, *level, entry_type::BTREE_ROOT, key); + } + + let mut out = alloc::vec![ + Field { kind: field_type::EXT, body: ext }, + Field { kind: field_type::MEMBERS_V2, body: members }, + ]; + if let Some(crypt) = crypt { + out.push(Field { kind: field_type::CRYPT, body: crypt.encode() }); + } + out.extend([ + Field { kind: field_type::CLEAN, body: clean }, + Field { kind: field_type::EXTENT_TYPE_U64S, body: EXTENT_TYPE_U64S.to_vec() }, + Field { kind: field_type::DOWNGRADE, body: DOWNGRADE.to_vec() }, + Field { kind: field_type::JOURNAL_V2, body: journal }, + // The btree's replicas entry, on device 0. + Field { kind: field_type::REPLICAS_V0, body: alloc::vec![data_type::BTREE, 1, 0] }, + ]); + out +} + +/// Appends a `jset_entry` record. +fn record(out: &mut Vec, btree: u8, level: u8, kind_: u8, payload: &[u8]) { + let mut header = [0u8; 8]; + put16(&mut header, 0, (payload.len() / 8) as u16); + header[2] = btree; + header[3] = level; + header[4] = kind_; + out.extend_from_slice(&header); + out.extend_from_slice(payload); +} + +async fn write(dev: &impl BlockDev, sector: u64, bytes: &[u8]) -> Result<()> { + let at = sector * SECTOR as u64; + assert!(at.is_multiple_of(BLOCK as u64), "a write off a block boundary"); + let mut blocks = alloc::vec![[0u8; BLOCK]; bytes.len().div_ceil(BLOCK)]; + blocks.as_flattened_mut()[..bytes.len()].copy_from_slice(bytes); + dev.write(at / BLOCK as u64, &blocks).await?; + Ok(()) +} + +struct Draw<'r, R>(&'r mut R); + +impl Draw<'_, R> { + fn bytes(&mut self) -> Result<[u8; N]> { + let mut b = [0u8; N]; + self.0.fill(&mut b).map_err(|_| Error::Entropy)?; + Ok(b) + } + + fn u64(&mut self) -> Result { + Ok(u64::from_le_bytes(self.bytes()?)) + } + + fn nonzero(&mut self) -> Result { + loop { + let v = self.u64()?; + if v != 0 { + return Ok(v); + } + } + } + + fn uuid(&mut self) -> Result<[u8; 16]> { + loop { + let v = self.bytes()?; + if v != [0; 16] { + return Ok(v); + } + } + } +} + +/// Where everything goes on a device of `sectors`. +struct Geometry { + /// Sectors per bucket. + bucket: u64, + nbuckets: u64, + /// Sectors per btree node. + node: u64, + layout: Layout, + /// The journal's first bucket and bucket count. + journal: (u64, u64), + /// Superblock sectors in each bucket that holds any. + sb: Vec<(u64, u64)>, +} + +impl Geometry { + fn new(sectors: u64) -> Result { + let wanted = (sectors / WANT_BUCKETS).max(1); + let bucket = (1u64 << wanted.ilog2()).clamp(MIN_BUCKET, MAX_BUCKET); + let nbuckets = sectors / bucket; + if nbuckets < MIN_BUCKETS || sectors < 4 * SB_SECTORS { + return Err(Error::Invalid("device smaller than 512 buckets")); + } + let second = (PRIMARY + SB_SECTORS).next_multiple_of(BLOCK_SECTORS); + let end = (sectors - SB_SECTORS) / bucket * bucket; + if end < second + SB_SECTORS { + return Err(Error::Invalid("device too small for three superblock copies")); + } + let layout = Layout { max_bits: SB_MAX_BITS, offsets: alloc::vec![PRIMARY, second, end] }; + let mut sb: Vec<(u64, u64)> = Vec::new(); + let regions = core::iter::once((0, PRIMARY)).chain(layout.offsets.iter().map(|&o| (o, o + SB_SECTORS))); + for (start, stop) in regions { + for b in start / bucket..stop.div_ceil(bucket).min(nbuckets) { + let marked = stop.min((b + 1) * bucket) - start.max(b * bucket); + match sb.iter_mut().find(|(bb, _)| *bb == b) { + Some((_, sectors)) => *sectors += marked, + None => sb.push((b, marked)), + } + } + } + sb.sort_unstable(); + let front = (second + SB_SECTORS).div_ceil(bucket); + let journal = (front, (nbuckets / 128).max(8)); + Ok(Geometry { bucket, nbuckets, node: bucket.min(MAX_NODE_SECTORS), layout, journal, sb }) + } + + /// The first bucket btree nodes go in. + fn btree_start(&self) -> u64 { + self.journal.0 + self.journal.1 + } + + fn nodes_per_bucket(&self) -> u64 { + self.bucket / self.node + } + + /// The bucket and sector of node slot `i`. + fn slot(&self, i: u64) -> (u64, u64) { + let bucket = self.btree_start() + i / self.nodes_per_bucket(); + (bucket, bucket * self.bucket + i % self.nodes_per_bucket() * self.node) + } + + /// Node counts of the buckets `n` nodes fill. + fn btree_buckets(&self, n: u64) -> impl Iterator + '_ { + let per = self.nodes_per_bucket(); + (0..n.div_ceil(per)).map(move |i| (self.btree_start() + i, per.min(n - i * per))) + } +} + +/// Every btree's keys for `nodes` node slots, before the node values that +/// depend on where each node goes. +struct Plan { + /// Per btree of [`BTREES`]: each key's position and encoded bytes. + keys: Vec)>>, + /// Each btree's levels: per level, each node's key range in the level + /// below (or in `keys`, for leaves). + shapes: Vec>>>, + node_bytes: usize, +} + +/// A first bset fills at most two thirds of its node, as upstream splits. +fn capacity(node_bytes: usize) -> usize { + node_bytes * 2 / 3 +} + +/// An interior key: a `btree_ptr_v2` with one device pointer. +const POINTER_KEY: usize = 40 + NodePtr::BYTES; + +impl Plan { + fn new(g: &Geometry, nodes: u64, hash_seed: u64) -> Result { + let (last, _) = g.slot(nodes - 1); + let end_copy = (g.layout.offsets[2] / g.bucket).min(g.nbuckets); + if last >= end_copy { + return Err(Error::Invalid("device too small for its metadata")); + } + let node_bytes = (g.node as usize) * SECTOR; + let mut keys: Vec)>> = BTREES.iter().map(|_| Vec::new()).collect(); + let mut put = |btree: Btree, key: Key, value: &[u8]| { + let i = BTREES.iter().position(|&b| b == btree).expect("a btree mkfs writes"); + keys[i].push((key.pos, key.encode(value))); + }; + + let root = root_inode(hash_seed); + put(Btree::Inodes, Key::new(kind::INODE_V3, Bpos::new(0, ROOT_INODE, ROOT_SNAPSHOT)), &root); + put(Btree::Subvolumes, Key::new(kind::SUBVOLUME, Bpos::new(0, u64::from(ROOT_SUBVOLUME), 0)), &value::subvolume(ROOT_SNAPSHOT, ROOT_INODE)); + put(Btree::Snapshots, Key::new(kind::SNAPSHOT, Bpos::new(0, u64::from(ROOT_SNAPSHOT), 0)), &value::snapshot(ROOT_SUBVOLUME, SNAPSHOT_TREE)); + put(Btree::SnapshotTrees, Key::new(kind::SNAPSHOT_TREE, Bpos::new(0, u64::from(SNAPSHOT_TREE), 0)), &value::snapshot_tree(ROOT_SUBVOLUME, ROOT_SNAPSHOT)); + put(Btree::LoggedOps, Key::new(kind::INODE_ALLOC_CURSOR, Bpos::new(1, 1, 0)), &value::inode_cursor(0, FIRST_INODE)); + + // Alloc keys for used buckets only; the rest are free holes. + let journal = g.journal.0..g.journal.0 + g.journal.1; + let btree: Vec<(u64, u64)> = g.btree_buckets(nodes).collect(); + let mut used: Vec<(u64, Alloc)> = g.sb.iter().map(|&(b, s)| (b, Alloc { data_type: data_type::SB, dirty_sectors: s as u32 })).collect(); + used.extend(journal.map(|b| (b, Alloc { data_type: data_type::JOURNAL, dirty_sectors: g.bucket as u32 }))); + used.extend(btree.iter().map(|&(b, n)| (b, Alloc { data_type: data_type::BTREE, dirty_sectors: (n * g.node) as u32 }))); + used.sort_unstable_by_key(|(b, _)| *b); + for (b, alloc) in &used { + put(Btree::Alloc, Key::new(kind::ALLOC_V4, Bpos::new(0, *b, 0)), &alloc.encode()); + } + let mut free = 0; + let mut ranges = Vec::new(); + for &(b, _) in &used { + if b > free { + ranges.push((free, b)); + } + free = b + 1; + } + if free < g.nbuckets { + ranges.push((free, g.nbuckets)); + } + for &(start, end) in &ranges { + let key = Key { size: (end - start) as u32, ..Key::new(kind::SET, Bpos::new(0, end, 0)) }; + put(Btree::Freespace, key, &[]); + } + // A fragmentation entry for each partly filled btree bucket. + for &(b, n) in &btree { + let dirty = n * g.node; + if dirty < g.bucket { + let time = dirty * (1 << 31) / g.bucket; + put(Btree::Lru, Key::new(kind::SET, Bpos::new(8192 << 48 | time, b, 0)), &[]); + } + } + // Backpointers' positions; their values follow the layout. + for i in 0..nodes { + let (_, sector) = g.slot(i); + put(Btree::Backpointers, Key::new(kind::BACKPOINTER, Bpos::new(0, sector << 16, 0)), &[0; 32]); + } + // Accounting's positions and sizes; its values follow the layout. + for (record, counters) in accounting_classes() { + put(Btree::Accounting, Key::new(kind::ACCOUNTING, value::accounting_pos(&record)), &alloc::vec![0u8; 8 * counters]); + } + for list in &mut keys { + list.sort_unstable_by_key(|(pos, _)| *pos); + } + let shapes = keys.iter().map(|k| shape(k.iter().map(|(_, b)| b.len()), node_bytes)).collect::>>()?; + Ok(Plan { keys, shapes, node_bytes }) + } + + fn nodes(&self) -> u64 { + self.shapes.iter().flatten().map(|level| level.len() as u64).sum() + } +} + +/// The accounting classes a fresh volume carries, as class records +/// and counter counts, in a fixed order whose values [`Tree::accounting`] +/// fills. +fn accounting_classes() -> Vec<(Vec, usize)> { + let mut out = alloc::vec![ + (alloc::vec![class::NR_INODES], 1), + (alloc::vec![class::REPLICAS, data_type::BTREE, 1, 1, 0], 1), + ]; + for t in [data_type::FREE, data_type::SB, data_type::JOURNAL, data_type::BTREE] { + out.push((alloc::vec![class::DEV_DATA_TYPE, 0, t], 3)); + } + for btree in BTREES { + let mut r = alloc::vec![class::BTREE]; + r.extend_from_slice(&(btree as u32).to_le_bytes()); + out.push((r, 3)); + } + let mut snapshot = alloc::vec![class::SNAPSHOT]; + snapshot.extend_from_slice(&ROOT_SNAPSHOT.to_le_bytes()); + snapshot.extend_from_slice(&(Btree::Inodes as u32).to_le_bytes()); + out.push((snapshot, 3)); + out +} + +/// The root directory: mode 040755, owned by root, siphash names under +/// `hash_seed`, the root subvolume's root. Its times are the time base, so +/// zero. +fn root_inode(hash_seed: u64) -> Vec { + let mut inode = InodeValue { hash_seed, flags: 0o40755 << 36 | 3 << 20, ..Default::default() }; + inode.set(inode_field::SUBVOL, u64::from(ROOT_SUBVOLUME)); + inode.encode() +} + +/// A btree's nodes for keys of `sizes`: leaves filled in order to +/// [`capacity`], then interior levels of pointers until one root. +fn shape(sizes: impl Iterator, node_bytes: usize) -> Result>>> { + let cap = capacity(node_bytes) - node::record_bytes(core::iter::empty()); + let mut leaves = alloc::vec![0..0]; + let mut fill = 0; + for (i, size) in sizes.enumerate() { + let last = leaves.last_mut().unwrap(); + if fill + size > cap && last.start != last.end { + leaves.push(i..i + 1); + fill = size; + } else { + last.end = i + 1; + fill += size; + } + } + let fanout = cap / POINTER_KEY; + let mut levels = alloc::vec![leaves]; + while levels.last().unwrap().len() > 1 { + let below = levels.last().unwrap().len(); + levels.push((0..below).step_by(fanout).map(|i| i..(i + fanout).min(below)).collect()); + if levels.len() > 4 { + return Err(Error::Invalid("a btree deeper than four levels")); + } + } + Ok(levels) +} + +/// One node as written. +struct Laid { + btree: Btree, + level: u8, + seq: u64, + sector: u64, + min: Bpos, + max: Bpos, + keys: Vec>, +} + +/// The nodes, `clean`'s root records, and the member's btree bitmap. +struct Tree { + nodes: Vec, + roots: Vec<(Btree, u8, Vec)>, + bitmap_shift: u8, + bitmap: u64, +} + +impl Plan { + /// Assigns slots in btree order, leaves first, and fills the values + /// that depend on them: backpointers, accounting, and every pointer. + fn lay_out(mut self, g: &Geometry, seqs: &[u64]) -> Result { + // Each node's place and range, before its keys are final. + struct Place { + btree: usize, + level: usize, + range: core::ops::Range, + slot: u64, + min: Bpos, + max: Bpos, + } + let mut places: Vec = Vec::new(); + let mut slot = 0; + let mut first_of: Vec> = Vec::new(); + for (t, levels) in self.shapes.iter().enumerate() { + let mut firsts = Vec::new(); + for (level, nodes) in levels.iter().enumerate() { + firsts.push(places.len()); + let count = nodes.len(); + for (j, range) in nodes.iter().enumerate() { + let (min, max) = if level == 0 { + let keys = &self.keys[t]; + let min = if j == 0 { Bpos::MIN } else { keys[range.start - 1].0.successor().expect("a key below MAX") }; + let max = if j + 1 == count { Bpos::MAX } else { keys[range.end - 1].0 }; + (min, max) + } else { + let below = firsts[level - 1]; + (places[below + range.start].min, places[below + range.end - 1].max) + }; + places.push(Place { btree: t, level, range: range.clone(), slot, min, max }); + slot += 1; + } + } + first_of.push(firsts); + } + + // Backpointers and accounting, now that every node has its place. + let bp = BTREES.iter().position(|&b| b == Btree::Backpointers).unwrap(); + for p in &places { + let (_, sector) = g.slot(p.slot); + let i = self.keys[bp].binary_search_by_key(&Bpos::new(0, sector << 16, 0), |(pos, _)| *pos).expect("a backpointer per node"); + let value = value::backpointer(BTREES[p.btree] as u8, p.level as u8, g.node as u32, p.max); + self.keys[bp][i].1[40..72].copy_from_slice(&value); + } + self.fill_accounting(g, &places.iter().map(|p| (p.btree, p.level)).collect::>()); + + // Nodes from the leaves up, each level's pointers from the one + // below. + let mut written: Vec> = (0..places.len()).map(|_| None).collect(); + let mut nodes = Vec::new(); + for (idx, p) in places.iter().enumerate() { + let keys: Vec> = if p.level == 0 { + self.keys[p.btree][p.range.clone()].iter().map(|(_, b)| b.clone()).collect() + } else { + let below = first_of[p.btree][p.level - 1]; + p.range + .clone() + .map(|c| { + let (ptr, max) = written[below + c].expect("children laid out first"); + Key::new(kind::BTREE_PTR_V2, max).encode(&ptr.encode()) + }) + .collect() + }; + let (_, sector) = g.slot(p.slot); + let bytes = node::record_bytes(keys.iter().map(Vec::len)).next_multiple_of(BLOCK); + assert!(bytes <= self.node_bytes, "a node past its size"); + let ptr = NodePtr { + seq: seqs[p.slot as usize], + written: (bytes / SECTOR) as u16, + min: p.min, + ptr: Ptr { sector, dev: 0, gen: 0, cached: false, unwritten: false }, + }; + written[idx] = Some((ptr, p.max)); + nodes.push(Laid { btree: BTREES[p.btree], level: p.level as u8, seq: ptr.seq, sector, min: p.min, max: p.max, keys }); + } + let roots = (0..BTREES.len()) + .map(|t| { + let top = self.shapes[t].len() - 1; + let (ptr, max) = written[first_of[t][top]].unwrap(); + (BTREES[t], top as u8, Key::new(kind::BTREE_PTR_V2, max).encode(&ptr.encode())) + }) + .collect(); + + // The bitmap's shift covers the last node; each node's regions are set. + let end = nodes.iter().map(|n| n.sector + g.node).max().unwrap_or(0); + let bitmap_shift = (64 - (end.max(64) - 1).leading_zeros()).saturating_sub(6) as u8; + let mut bitmap = 0u64; + for n in &nodes { + for i in n.sector >> bitmap_shift..(n.sector + g.node).div_ceil(1 << bitmap_shift) { + bitmap |= 1 << i; + } + } + Ok(Tree { nodes, roots, bitmap_shift, bitmap }) + } + + /// Each class's counters, in [`accounting_classes`]' order. + fn fill_accounting(&mut self, g: &Geometry, nodes: &[(usize, usize)]) { + let n = nodes.len() as u64; + let node = g.node; + let btree: Vec<(u64, u64)> = g.btree_buckets(n).collect(); + let sb_sectors: u64 = g.sb.iter().map(|&(_, s)| s).sum(); + let sb_frag: u64 = g.sb.iter().map(|&(_, s)| g.bucket - s).sum(); + let btree_frag: u64 = btree.iter().map(|&(_, k)| g.bucket - k * node).sum(); + let used = g.sb.len() as u64 + g.journal.1 + btree.len() as u64; + let inode_bytes = { + let inodes = BTREES.iter().position(|&b| b == Btree::Inodes).unwrap(); + self.keys[inodes][0].1.len() as u64 + }; + let mut values: Vec> = alloc::vec![ + alloc::vec![1], + alloc::vec![n * node], + alloc::vec![g.nbuckets - used, 0, 0], + alloc::vec![g.sb.len() as u64, sb_sectors, sb_frag], + alloc::vec![g.journal.1, g.journal.1 * g.bucket, 0], + alloc::vec![btree.len() as u64, n * node, btree_frag], + ]; + for t in 0..BTREES.len() { + let count = nodes.iter().filter(|&&(b, _)| b == t).count() as u64; + let interior = nodes.iter().filter(|&&(b, l)| b == t && l > 0).count() as u64; + values.push(alloc::vec![count * node, count, interior]); + } + values.push(alloc::vec![1, inode_bytes, 0]); + + let acct = BTREES.iter().position(|&b| b == Btree::Accounting).unwrap(); + for ((record, _), v) in accounting_classes().iter().zip(values) { + let pos = value::accounting_pos(record); + let i = self.keys[acct].binary_search_by_key(&pos, |(p, _)| *p).expect("a key per class"); + self.keys[acct][i].1[40..].copy_from_slice(&value::counters(&v)); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_small_device_lays_out_as_format_does() { + // Upstream's own 32 MiB layout: 65 536 sectors in 32-sector buckets, copies at 8, + // 2 056 and 63 488; buckets 0–127 hold 32 superblock sectors, 128 + // holds 8, 1 984–2 047 hold 32. + let g = Geometry::new(65536).unwrap(); + assert_eq!((g.bucket, g.nbuckets, g.node), (32, 2048, 32)); + assert_eq!(g.layout.offsets, [8, 2056, 63488]); + assert_eq!(g.sb.len(), 129 + 64); + assert_eq!(g.sb[128], (128, 8)); + assert!(g.sb.iter().filter(|&&(b, _)| b != 128).all(|&(_, s)| s == 32)); + assert_eq!(g.journal, (129, 16)); + } + + #[test] + fn a_large_device_takes_two_mib_buckets_and_256_kib_nodes() { + // 171 GiB, as `format` lays out a partition that size. + let g = Geometry::new(171 << 21).unwrap(); + assert_eq!((g.bucket, g.node), (4096, 512)); + assert_eq!(g.journal.1, g.nbuckets / 128); + } + + #[test] + fn leaves_fill_to_two_thirds_and_one_root_spans_them() { + let levels = shape(core::iter::repeat_n(104, 300), 16384).unwrap(); + assert_eq!(levels.len(), 2); + assert_eq!(levels[0].len(), 3); + assert_eq!(levels[1], vec![0..3]); + let empty = shape(core::iter::empty(), 16384).unwrap(); + assert_eq!(empty, vec![vec![0..0]], "an empty btree still has its root"); + } +} diff --git a/lib/bcachefs/src/engine/mod.rs b/lib/bcachefs/src/engine/mod.rs new file mode 100644 index 00000000000..26ca234b000 --- /dev/null +++ b/lib/bcachefs/src/engine/mod.rs @@ -0,0 +1,254 @@ +//! bcachefs as bcachefs-tools v1.39.4 defines metadata version 1.39 (1063), +//! on one device: a reader of clean volumes, [`mkfs`] of a minimal complete +//! volume, and ToyOS's check of what it writes ([`fsck`]). +//! +//! **Everything read came off a disk this crate does not own.** Bytes become +//! typed values in one place each, the superblock in [`superblock`], a node +//! and its keys in `node`, a value in `value`; what those return is not +//! checked again. A structure outside what this reader decodes is refused by +//! name ([`Error::Unsupported`]), never guessed at. + +mod bkey; +mod crypt; +mod fsck; +mod mkfs; +mod node; +mod read; +mod superblock; +mod value; + +pub use bkey::{Bpos, Key, Version}; +pub use crypt::MIN_KDF; +pub use fsck::{fsck, Finding}; +pub use mkfs::{mkfs, Encryption, Options}; +pub use read::{DirEntry, Inode, Volume}; +pub use superblock::{Kdf, Superblock}; +pub use value::DirentType; + +/// Why an operation on a volume did not happen. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Error { + Device(toyos_blockdev::Error), + /// A checksum or MAC disagreed with the bytes it covers. + Checksum(&'static str), + /// Bytes that are not the format, and what was wrong with them. + Corrupt(&'static str), + /// Valid bcachefs this crate does not handle, by name. + Unsupported(&'static str), + /// An encrypted volume opened without its passphrase. + Locked, + /// The passphrase does not unwrap the volume's key. + Passphrase, + NotFound, + NotDirectory, + /// An argument the caller should not have made, by name. + Invalid(&'static str), + /// The random source failed. + Entropy, +} + +impl From for Error { + fn from(e: toyos_blockdev::Error) -> Self { + Error::Device(e) + } +} + +pub type Result = core::result::Result; + +/// Bytes per sector, whatever the device's block. +pub const SECTOR: usize = 512; + +/// The metadata version this crate reads and writes: 1.39. +pub const VERSION: u16 = 1 << 10 | 39; + +pub(crate) fn le16(b: &[u8], at: usize) -> u16 { + u16::from_le_bytes([b[at], b[at + 1]]) +} + +pub(crate) fn le32(b: &[u8], at: usize) -> u32 { + u32::from_le_bytes(b[at..at + 4].try_into().unwrap()) +} + +pub(crate) fn le64(b: &[u8], at: usize) -> u64 { + u64::from_le_bytes(b[at..at + 8].try_into().unwrap()) +} + +pub(crate) fn put16(b: &mut [u8], at: usize, v: u16) { + b[at..at + 2].copy_from_slice(&v.to_le_bytes()); +} + +pub(crate) fn put32(b: &mut [u8], at: usize, v: u32) { + b[at..at + 4].copy_from_slice(&v.to_le_bytes()); +} + +pub(crate) fn put64(b: &mut [u8], at: usize, v: u64) { + b[at..at + 8].copy_from_slice(&v.to_le_bytes()); +} + +/// Bits `[lo, lo + len)` of `word`. +pub(crate) fn field(word: u64, lo: u32, len: u32) -> u64 { + (word >> lo) & if len == 64 { !0 } else { (1 << len) - 1 } +} + +/// The btrees this crate names, numbered as the format numbers them. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[repr(u8)] +pub enum Btree { + Extents = 0, + Inodes = 1, + Dirents = 2, + Xattrs = 3, + Alloc = 4, + Quotas = 5, + Stripes = 6, + Reflink = 7, + Subvolumes = 8, + Snapshots = 9, + Lru = 10, + Freespace = 11, + NeedDiscard = 12, + Backpointers = 13, + BucketGens = 14, + SnapshotTrees = 15, + DeletedInodes = 16, + LoggedOps = 17, + ReconcileWork = 18, + SubvolumeChildren = 19, + Accounting = 20, + ReconcileHipri = 21, + ReconcilePending = 22, + ReconcileScan = 23, + ReconcileWorkPhys = 24, + ReconcileHipriPhys = 25, + BucketToStripe = 26, + StripeBackpointers = 27, + Damage = 28, +} + +impl Btree { + pub(crate) const ALL: [Btree; 29] = [ + Btree::Extents, + Btree::Inodes, + Btree::Dirents, + Btree::Xattrs, + Btree::Alloc, + Btree::Quotas, + Btree::Stripes, + Btree::Reflink, + Btree::Subvolumes, + Btree::Snapshots, + Btree::Lru, + Btree::Freespace, + Btree::NeedDiscard, + Btree::Backpointers, + Btree::BucketGens, + Btree::SnapshotTrees, + Btree::DeletedInodes, + Btree::LoggedOps, + Btree::ReconcileWork, + Btree::SubvolumeChildren, + Btree::Accounting, + Btree::ReconcileHipri, + Btree::ReconcilePending, + Btree::ReconcileScan, + Btree::ReconcileWorkPhys, + Btree::ReconcileHipriPhys, + Btree::BucketToStripe, + Btree::StripeBackpointers, + Btree::Damage, + ]; + + pub(crate) fn from_id(id: u32) -> Option { + Btree::ALL.get(id as usize).copied() + } + + /// Keys are extents, positioned at their end. + pub(crate) fn extents(self) -> bool { + matches!(self, Btree::Extents | Btree::Reflink | Btree::Freespace) + } + + /// The key types a leaf of this btree may hold besides `deleted`. + fn leaf_types(self) -> &'static [u8] { + match self { + Btree::Extents => &[1, 2, 3, 6, 7, 15, 17, 36], + Btree::Inodes => &[1, 8, 9, 23, 29], + Btree::Dirents => &[1, 4, 10], + Btree::Xattrs => &[1, 3, 4, 11], + Btree::Alloc => &[12, 20, 24, 27], + Btree::Quotas => &[13], + Btree::Stripes => &[14], + Btree::Reflink => &[2, 16, 19], + Btree::Subvolumes => &[21], + Btree::Snapshots => &[22], + Btree::Backpointers | Btree::StripeBackpointers => &[28], + Btree::BucketGens => &[30], + Btree::SnapshotTrees => &[31], + Btree::LoggedOps => &[32, 33, 35, 37], + Btree::ReconcileWork => &[3, 25], + Btree::Accounting => &[34], + Btree::ReconcileScan => &[3, 28], + Btree::Damage => &[1, 38], + Btree::Lru + | Btree::Freespace + | Btree::NeedDiscard + | Btree::DeletedInodes + | Btree::SubvolumeChildren + | Btree::ReconcileHipri + | Btree::ReconcilePending + | Btree::ReconcileWorkPhys + | Btree::ReconcileHipriPhys + | Btree::BucketToStripe => &[25], + } + } + + /// Whether a key of `kind` belongs in a node of this btree at `level`: + /// an interior node holds only `deleted` and btree pointers. + pub(crate) fn allows(self, level: u8, kind: u8) -> bool { + match level { + 0 => kind == kind::DELETED || self.leaf_types().contains(&kind), + _ => matches!(kind, kind::DELETED | kind::BTREE_PTR | kind::BTREE_PTR_V2), + } + } +} + +/// Key types, numbered as the format numbers them: the order of +/// *bcachefs: Principles of Operation* §11.5's list. +pub(crate) mod kind { + /// The first key type v1.39.4 does not define. + pub const UNKNOWN: u8 = 39; + + /// Whether a read of a leaf checks this type against its btree. The + /// others (deleted, whiteout, error, cookie, set) upstream refuses in the + /// wrong btree only when it commits them. + pub fn strict(kind: u8) -> bool { + !matches!(kind, DELETED | WHITEOUT | ERROR | COOKIE | SET) + } + + pub const DELETED: u8 = 0; + pub const WHITEOUT: u8 = 1; + pub const ERROR: u8 = 2; + pub const COOKIE: u8 = 3; + pub const HASH_WHITEOUT: u8 = 4; + pub const BTREE_PTR: u8 = 5; + pub const EXTENT: u8 = 6; + pub const RESERVATION: u8 = 7; + pub const INODE: u8 = 8; + pub const DIRENT: u8 = 10; + pub const REFLINK_P: u8 = 15; + pub const INLINE_DATA: u8 = 17; + pub const BTREE_PTR_V2: u8 = 18; + pub const SUBVOLUME: u8 = 21; + pub const SNAPSHOT: u8 = 22; + pub const INODE_V2: u8 = 23; + pub const SET: u8 = 25; + pub const ALLOC_V4: u8 = 27; + pub const BACKPOINTER: u8 = 28; + pub const INODE_V3: u8 = 29; + pub const SNAPSHOT_TREE: u8 = 31; + pub const ACCOUNTING: u8 = 34; + pub const INODE_ALLOC_CURSOR: u8 = 35; + pub const EXTENT_WHITEOUT: u8 = 36; +} + +#[cfg(test)] +mod tests; diff --git a/lib/bcachefs/src/engine/node.rs b/lib/bcachefs/src/engine/node.rs new file mode 100644 index 00000000000..44253b51d16 --- /dev/null +++ b/lib/bcachefs/src/engine/node.rs @@ -0,0 +1,287 @@ +//! A btree node: a first record with the node header and a bset, then +//! later records each holding a bset, every one checksummed or MACed. + +use alloc::collections::BTreeMap; +use alloc::vec::Vec; + +use super::bkey::{Bpos, Format, Key}; +use super::crypt::{MasterKey, Nonce}; +use super::superblock::Superblock; +use super::{field, kind, le16, le32, le64, put16, put32, put64, Btree, Error, Result, SECTOR, VERSION}; +use crate::crc32c::crc32c; + +/// The node magic's XOR with the internal UUID's first eight bytes. +const NODE_MAGIC: u64 = 0x9013_5c78_b99e_07f5; +/// Bytes before the first bset's keys: header, format and bset header. +const FIRST_KEYS: usize = 160; +/// Bytes before a later bset's keys: checksum and bset header. +const LATER_KEYS: usize = 40; +/// Node flags bit 8, set on every node. +const NEW_EXTENT_OVERWRITE: u64 = 1 << 8; + +/// Checksum types metadata uses. +pub(crate) mod csum { + pub const NONE: u8 = 0; + pub const CRC32C: u8 = 1; + pub const CHACHA_POLY_128: u8 = 4; +} + +/// Every node's magic on the volume of `internal_uuid`. +pub(crate) fn magic(internal_uuid: &[u8; 16]) -> u64 { + u64::from_le_bytes(internal_uuid[..8].try_into().unwrap()) ^ NODE_MAGIC +} + +/// What a node is expected to be: where its parent pointer says it is, and +/// what it says it holds. +pub(crate) struct Expect { + pub btree: Btree, + pub level: u8, + pub seq: u64, + pub min: Bpos, + pub max: Bpos, +} + +/// A node, its bsets merged. +pub(crate) struct Node { + pub level: u8, + /// Live keys in position order, each with its value. + pub keys: Vec<(Key, Vec)>, + /// Each bset's journal sequence, in the order written. + pub bset_seqs: Vec, +} + +/// One record's place in the node and its parts. +struct Record { + /// Byte offset in the node. + at: usize, + /// Byte offset of the bset header. + bset: usize, + /// Byte offset of the keys. + keys: usize, + /// Byte offset past the keys. + end: usize, +} + +/// The node `bytes`, its parent pointer's written sectors of it, checked +/// against `expect`; `key` for an encrypted volume. +pub(crate) fn decode(sb: &Superblock, key: Option<&MasterKey>, expect: &Expect, mut bytes: Vec) -> Result { + let block = sb.block_bytes(); + if bytes.len() < FIRST_KEYS || le64(&bytes, 16) != magic(&sb.header.internal_uuid) { + return Err(Error::Corrupt("btree node without its magic")); + } + let mut bsets: Vec)>> = Vec::new(); + let mut bset_seqs = Vec::new(); + let mut format = Format::UNPACKED; + let mut at = 0; + while at < bytes.len() { + let first = at == 0; + let bset = if first { 136 } else { at + 16 }; + if bset + 24 > bytes.len() { + return Err(Error::Corrupt("bset header past the node's written sectors")); + } + if le64(&bytes, bset) != expect.seq { + if first { + return Err(Error::Corrupt("btree node of another sequence than its pointer's")); + } + break; + } + let keys = if first { FIRST_KEYS } else { at + LATER_KEYS }; + let end = keys + 8 * usize::from(le16(&bytes, bset + 22)); + let next = end.next_multiple_of(block); + if next > bytes.len() { + return Err(Error::Corrupt("bset past the node's written sectors")); + } + let record = Record { at, bset, keys, end }; + let journal_seq = le64(&bytes, bset + 8); + let flags = le32(&bytes, bset + 16); + check_bset(sb, flags, le16(&bytes, bset + 20), at)?; + let csum_type = field(u64::from(flags), 0, 4) as u8; + // An encrypted volume MACs all its metadata. + if sb.encrypted() && csum_type != csum::CHACHA_POLY_128 { + return Err(Error::Corrupt("unauthenticated btree node on an encrypted volume")); + } + open(key, expect.seq, journal_seq, csum_type, &record, &mut bytes)?; + if first { + format = parse_header(&bytes, expect)?; + } else if end == keys { + return Err(Error::Corrupt("later bset with no keys")); + } + if sb.blacklisted(journal_seq) { + if first { + return Err(Error::Corrupt("first bset's journal sequence blacklisted")); + } + } else { + bsets.push(parse_keys(&format, &bytes[keys..end], expect)?); + bset_seqs.push(journal_seq); + } + at = next; + } + Ok(Node { level: expect.level, keys: merge(bsets), bset_seqs }) +} + +fn check_bset(sb: &Superblock, flags: u32, version: u16, at: usize) -> Result<()> { + if flags & 1 << 4 != 0 { + return Err(Error::Unsupported("big-endian bset")); + } + if flags & 1 << 5 != 0 { + return Err(Error::Corrupt("bset with separate whiteouts")); + } + let offset = field(u64::from(flags), 16, 16) as usize; + if offset != 0 && offset * SECTOR != at { + return Err(Error::Corrupt("bset at another sector than it records")); + } + if version >> 10 != 1 || version > VERSION || version < sb.header.version_min { + return Err(Error::Unsupported("bset version outside the superblock's")); + } + Ok(()) +} + +/// Verifies a record's checksum or MAC, then decrypts it in place. +fn open(key: Option<&MasterKey>, seq: u64, journal_seq: u64, csum_type: u8, r: &Record, bytes: &mut [u8]) -> Result<()> { + match csum_type { + csum::NONE => Ok(()), + csum::CRC32C if crc32c(&bytes[r.at + 16..r.end]) == le32(bytes, r.at) => Ok(()), + csum::CRC32C => Err(Error::Checksum("btree node")), + csum::CHACHA_POLY_128 => { + let key = key.ok_or(Error::Corrupt("MACed btree node on an unencrypted volume"))?; + let nonce = Nonce::node(r.at as u32, seq, journal_seq); + let tag: [u8; 16] = bytes[r.at..r.at + 16].try_into().unwrap(); + key.verify(nonce, &bytes[r.at + 16..r.end], &tag, "btree node")?; + if r.at == 0 { + // Node flags through the format from block 0, the + // keys from block 2. + key.xor(nonce, &mut bytes[24..r.bset])?; + key.xor(nonce.plus(2), &mut bytes[r.keys..r.end]) + } else { + key.xor(nonce, &mut bytes[r.keys..r.end]) + } + } + _ => Err(Error::Unsupported("btree node checksum type")), + } +} + +/// The first record's node header, against what its pointer expects. +fn parse_header(b: &[u8], expect: &Expect) -> Result { + let flags = le64(b, 24); + let btree = field(flags, 0, 4) | field(flags, 9, 16) << 4; + if btree != expect.btree as u64 || field(flags, 4, 4) != u64::from(expect.level) { + return Err(Error::Corrupt("btree node of another btree or level than its pointer's")); + } + if expect.btree.extents() && flags & NEW_EXTENT_OVERWRITE == 0 { + return Err(Error::Unsupported("extents node without new_extent_overwrite")); + } + if Bpos::decode(&b[32..52]) != expect.min || Bpos::decode(&b[52..72]) != expect.max { + return Err(Error::Corrupt("btree node range disagrees with its pointer")); + } + Format::decode(&b[80..136]) +} + +/// A bset's keys: sorted, inside the node's range. +fn parse_keys(format: &Format, mut b: &[u8], expect: &Expect) -> Result)>> { + let mut keys: Vec<(Key, Vec)> = Vec::new(); + while !b.is_empty() { + let len = usize::from(b[0]) * 8; + if len == 0 { + return Err(Error::Corrupt("key of no size")); + } + let (key, value) = format.key(b)?; + if key.pos < expect.min || key.pos > expect.max { + return Err(Error::Corrupt("key outside its node's range")); + } + check_kind(expect.btree, expect.level, &key)?; + if let Some((last, _)) = keys.last() { + let ordered = last.pos < key.pos || (last.pos == key.pos && last.kind == kind::DELETED && key.kind != kind::DELETED); + if !ordered { + return Err(Error::Corrupt("keys out of order")); + } + } + keys.push((key, value.to_vec())); + b = &b[len..]; + } + Ok(keys) +} + +/// A key's type against its node: in an interior node only pointers, of +/// size 0; in a leaf a type its btree holds, which a read checks only for +/// the strict types. +fn check_kind(btree: Btree, level: u8, key: &Key) -> Result<()> { + if key.kind >= kind::UNKNOWN { + return Err(Error::Unsupported("key type v1.39.4 does not define")); + } + if level > 0 && (!btree.allows(level, key.kind) || key.size != 0) { + return Err(Error::Corrupt("interior key that is no size-0 pointer")); + } + if level == 0 && kind::strict(key.kind) && !btree.allows(0, key.kind) { + return Err(Error::Corrupt("key of a type its btree does not hold")); + } + Ok(()) +} + +/// Bsets merged by position, a later one winning, deleted keys dropped. +fn merge(bsets: Vec)>>) -> Vec<(Key, Vec)> { + let mut merged = BTreeMap::new(); + for bset in bsets { + for (key, value) in bset { + merged.insert(key.pos, (key, value)); + } + } + merged.into_values().filter(|(k, _)| k.kind != kind::DELETED).collect() +} + +/// A node's first and only record, written fresh. +pub(crate) struct Build<'a> { + pub btree: Btree, + pub level: u8, + pub seq: u64, + pub min: Bpos, + pub max: Bpos, + /// The bset's journal sequence. + pub journal_seq: u64, + /// Encoded unpacked keys, in position order. + pub keys: &'a [Vec], +} + +/// The first record's bytes: [`FIRST_KEYS`] plus the keys. +pub(crate) fn record_bytes(keys: impl Iterator) -> usize { + FIRST_KEYS + keys.sum::() +} + +/// `build`'s record, padded to whole blocks, checksummed for an +/// unencrypted volume or encrypted and MACed under `key`. +pub(crate) fn encode(sb_magic: u64, block: usize, key: Option<&MasterKey>, build: &Build) -> Result> { + let end = record_bytes(build.keys.iter().map(Vec::len)); + let mut b = alloc::vec![0u8; end.next_multiple_of(block)]; + put64(&mut b, 16, sb_magic); + let id = build.btree as u64; + put64(&mut b, 24, id & 0xf | u64::from(build.level) << 4 | NEW_EXTENT_OVERWRITE | (id >> 4) << 9); + build.min.encode(&mut b[32..52]); + build.max.encode(&mut b[52..72]); + Format::UNPACKED.encode(&mut b[80..136]); + put64(&mut b, 136, build.seq); + put64(&mut b, 144, build.journal_seq); + let csum_type = if key.is_some() { csum::CHACHA_POLY_128 } else { csum::CRC32C }; + put32(&mut b, 152, u32::from(csum_type)); + put16(&mut b, 156, VERSION); + put16(&mut b, 158, ((end - FIRST_KEYS) / 8) as u16); + let mut at = FIRST_KEYS; + for k in build.keys { + assert!(build.btree.allows(build.level, k[2]), "a key of type {} in a {:?} node at level {}", k[2], build.btree, build.level); + assert!(build.level == 0 || le32(k, 16) == 0, "an interior key with a size"); + b[at..at + k.len()].copy_from_slice(k); + at += k.len(); + } + match key { + None => { + let crc = crc32c(&b[16..end]); + put32(&mut b, 0, crc); + } + Some(key) => { + let nonce = Nonce::node(0, build.seq, build.journal_seq); + key.xor(nonce, &mut b[24..136])?; + key.xor(nonce.plus(2), &mut b[FIRST_KEYS..end])?; + let tag = key.mac(nonce, &b[16..end])?; + b[..16].copy_from_slice(&tag); + } + } + Ok(b) +} diff --git a/lib/bcachefs/src/engine/read.rs b/lib/bcachefs/src/engine/read.rs new file mode 100644 index 00000000000..9e655bb2e3f --- /dev/null +++ b/lib/bcachefs/src/engine/read.rs @@ -0,0 +1,526 @@ +//! A clean volume, read: its superblock chosen among the copies, its +//! btrees from `clean`'s roots, and files, directories and symlinks over +//! them. + +use alloc::vec::Vec; + +use toyos_blockdev::{BlockDev, BLOCK}; +use toyos_crypto::{scrypt, siphash}; + +use super::bkey::{Bpos, Key}; +use super::crypt::{MasterKey, Nonce}; +use super::node::{self, Expect, Node}; +use super::superblock::{Layout, Root, Superblock, HEADER, LAYOUT, MAX_BYTES, PRIMARY}; +use super::value::{inode_field, inode_flag, Dirent, DirentType, Extent, InodeValue, NodePtr, Subvolume}; +use super::{kind, le32, Btree, Error, Result, SECTOR}; +use crate::crc32c::{crc32c, crc32c_zero}; + +/// A clean volume open for reading. +pub struct Volume { + dev: D, + sb: Superblock, + key: Option, + /// The root subvolume's snapshot, the only one read. + snapshot: u32, + root: u64, +} + +/// A file's attributes. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Inode { + pub ino: u64, + pub mode: u16, + pub size: u64, + pub nlink: u32, + pub uid: u32, + pub gid: u32, + pub rdev: u32, + pub generation: u32, + /// Access, change, modification and creation, in nanoseconds since the + /// Unix epoch. + pub atime: i64, + pub ctime: i64, + pub mtime: i64, + pub otime: i64, + /// The journal sequence the inode was last written in. + pub journal_seq: u64, + value: InodeValue, +} + +/// One name in a directory. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DirEntry { + pub name: Vec, + pub ino: u64, + pub kind: DirentType, + /// Where a listing resumes after this entry. + pub next: u64, +} + +/// The longest symlink this reader reads, its NUL included: a path's +/// length on the systems that make them. +const MAX_SYMLINK: u64 = 4096; + +const S_IFMT: u16 = 0o170000; +const S_IFDIR: u16 = 0o040000; + +impl Inode { + pub fn is_dir(&self) -> bool { + self.mode & S_IFMT == S_IFDIR + } +} + +impl Volume { + /// An unencrypted volume, or one formatted without a passphrase. + pub async fn open(dev: D) -> Result { + let sb = read_superblock(&dev).await?; + let key = match &sb.crypt { + Some(crypt) => Some(MasterKey::stored(crypt).ok_or(Error::Locked)?), + None => None, + }; + Self::with(dev, sb, key).await + } + + /// An encrypted volume, its key unwrapped under `passphrase` with no more + /// scrypt cost than `limits`, which the superblock asks and the caller + /// bounds. + pub async fn unlock(dev: D, passphrase: &[u8], limits: scrypt::Limits) -> Result { + let sb = read_superblock(&dev).await?; + let crypt = sb.crypt.ok_or(Error::Invalid("a passphrase for an unencrypted volume"))?; + let key = MasterKey::unwrap(&crypt, &sb.header.internal_uuid, passphrase, limits)?; + Self::with(dev, sb, Some(key)).await + } + + async fn with(dev: D, sb: Superblock, key: Option) -> Result { + if sb.encrypted() != sb.crypt.is_some() { + return Err(Error::Corrupt("encryption option and crypt field disagree")); + } + if !sb.clean_flag() || sb.clean.is_none() { + return Err(Error::Unsupported("a volume not cleanly unmounted: its journal needs replay")); + } + let mut volume = Volume { dev, sb, key, snapshot: 0, root: 0 }; + let (k, v) = volume.get(Btree::Subvolumes, Bpos::new(0, u64::from(super::mkfs::ROOT_SUBVOLUME), 0)).await?.ok_or(Error::Corrupt("no root subvolume"))?; + if k.kind != kind::SUBVOLUME { + return Err(Error::Corrupt("root subvolume of another type")); + } + let subvolume = Subvolume::decode(&v)?; + // A snapshotted subvolume's keys live in its ancestors' snapshots, + // which this reader does not walk. + if subvolume.snapshot != super::mkfs::ROOT_SNAPSHOT { + return Err(Error::Unsupported("a root subvolume that has been snapshotted")); + } + volume.snapshot = subvolume.snapshot; + volume.root = subvolume.inode; + Ok(volume) + } + + pub fn superblock(&self) -> &Superblock { + &self.sb + } + + pub(crate) fn dev(&self) -> &D { + &self.dev + } + + /// The root directory's inode number. + pub fn root(&self) -> u64 { + self.root + } + + pub(crate) fn roots(&self) -> &[Root] { + &self.sb.clean.as_ref().expect("opened clean").roots + } + + fn root_of(&self, btree: Btree) -> Option<&Root> { + self.roots().iter().find(|r| r.btree == btree) + } + + /// The node `ptr` points at, `level` of `btree`, spanning `[min, max]`. + pub(crate) async fn node(&self, btree: Btree, level: u8, ptr: &NodePtr, max: Bpos) -> Result { + let sectors = u32::from(ptr.written); + if sectors > self.sb.node_sectors() || ptr.ptr.dev != 0 { + return Err(Error::Corrupt("btree pointer past its node, or to another device")); + } + self.in_one_bucket(ptr.ptr.sector, u64::from(self.sb.node_sectors()))?; + let bytes = read_bytes(&self.dev, ptr.ptr.sector * SECTOR as u64, sectors as usize * SECTOR).await?; + let expect = Expect { btree, level, seq: ptr.seq, min: ptr.min, max }; + node::decode(&self.sb, self.key.as_ref(), &expect, bytes) + } + + /// A pointed range lies in one bucket of the device. + fn in_one_bucket(&self, sector: u64, sectors: u64) -> Result<()> { + let m = &self.sb.member; + let bucket = u64::from(m.bucket_sectors); + let first = sector / bucket; + if first < u64::from(m.first_bucket) || first >= m.nbuckets || sector % bucket + sectors > bucket { + return Err(Error::Corrupt("pointer outside one bucket of the device")); + } + Ok(()) + } + + /// Visits `btree`'s keys from `from` in order until `visit` answers + /// `false`. + pub(crate) async fn walk(&self, btree: Btree, from: Bpos, mut visit: impl FnMut(&Key, &[u8]) -> Result) -> Result<()> { + let Some(root) = self.root_of(btree) else { return Ok(()) }; + let mut stack: Vec<(Node, usize)> = Vec::new(); + let node = self.node(btree, root.level, &root.ptr, root.key.pos).await?; + let start = node.keys.partition_point(|(k, _)| k.pos < from); + stack.push((node, start)); + while let Some((node, i)) = stack.last_mut() { + let Some((key, value)) = node.keys.get(*i) else { + stack.pop(); + continue; + }; + *i += 1; + if node.level == 0 { + if !visit(key, value)? { + return Ok(()); + } + continue; + } + if key.kind != kind::BTREE_PTR_V2 { + return Err(Error::Unsupported("interior key other than btree_ptr_v2, such as a v1 btree_ptr")); + } + let (level, ptr, max) = (node.level - 1, NodePtr::decode(value)?, key.pos); + let child = self.node(btree, level, &ptr, max).await?; + let start = child.keys.partition_point(|(k, _)| k.pos < from); + stack.push((child, start)); + } + Ok(()) + } + + /// The key at `pos` in `btree`, if any. + pub(crate) async fn get(&self, btree: Btree, pos: Bpos) -> Result)>> { + let mut found = None; + self.walk(btree, pos, |k, v| { + if k.pos == pos { + found = Some((*k, v.to_vec())); + } + Ok(false) + }) + .await?; + Ok(found) + } + + /// The inode numbered `ino`. + pub async fn inode(&self, ino: u64) -> Result { + let (key, value) = self.get(Btree::Inodes, Bpos::new(0, ino, self.snapshot)).await?.ok_or(Error::NotFound)?; + match key.kind { + kind::INODE_V3 => {} + kind::INODE | kind::INODE_V2 => return Err(Error::Unsupported("inode in a format before inode_v3")), + _ => return Err(Error::NotFound), + } + let v = InodeValue::decode(&value)?; + let mode = v.mode(); + let stored = v.field(inode_field::NLINK) as u32; + let nlink = if v.flags & inode_flag::UNLINKED != 0 { + 0 + } else { + stored.checked_add(if mode & S_IFMT == S_IFDIR { 2 } else { 1 }).ok_or(Error::Corrupt("inode link count past 2^32"))? + }; + let time = |f| self.time(v.field(f)); + Ok(Inode { + ino, + mode, + size: v.size, + nlink, + uid: v.field(inode_field::UID) as u32, + gid: v.field(inode_field::GID) as u32, + rdev: v.field(inode_field::RDEV) as u32, + generation: v.field(inode_field::GENERATION) as u32, + atime: time(inode_field::ATIME), + ctime: time(inode_field::CTIME), + mtime: time(inode_field::MTIME), + otime: time(inode_field::OTIME), + journal_seq: v.journal_seq, + value: v, + }) + } + + /// A stored time, signed units since the time base, in + /// nanoseconds since the epoch. + fn time(&self, stored: u64) -> i64 { + let precision = i128::from(self.sb.header.time_precision); + let ns = (i128::from(stored as i64) + i128::from(self.sb.header.time_base) / precision) * precision; + ns.clamp(i128::from(i64::MIN), i128::from(i64::MAX)) as i64 + } + + async fn directory(&self, dir: u64) -> Result { + let inode = self.inode(dir).await?; + if inode.is_dir() { + Ok(inode) + } else { + Err(Error::NotDirectory) + } + } + + /// The entry `name` in directory `dir`: its name's hash slot, then the run + /// of entries and whiteouts after it. + pub async fn lookup(&self, dir: u64, name: &[u8]) -> Result> { + let inode = self.directory(dir).await?; + let start = Bpos::new(dir, hash(&inode.value, name)?, self.snapshot); + let mut expected = start; + let mut found = None; + let mut failed = None; + self.walk(Btree::Dirents, start, |k, v| { + if k.pos != expected { + return Ok(false); + } + if k.kind == kind::DIRENT { + match entry(k, v) { + Ok(e) if e.name == name => { + found = Some(e); + return Ok(false); + } + Ok(_) => {} + Err(e) => { + failed = Some(e); + return Ok(false); + } + } + } else if k.kind != kind::HASH_WHITEOUT { + return Ok(false); + } + match k.pos.offset.checked_add(1) { + Some(offset) => expected.offset = offset, + None => return Ok(false), + } + Ok(true) + }) + .await?; + failed.map_or(Ok(found), Err) + } + + /// Up to `max` entries of `dir` from `cursor` on, in hash order, and + /// whether more follow. Cursor 0 starts the listing; an entry's `next` + /// resumes after it. + pub async fn readdir(&self, dir: u64, cursor: u64, max: usize) -> Result<(Vec, bool)> { + self.directory(dir).await?; + let mut out = Vec::new(); + let mut more = false; + self.walk(Btree::Dirents, Bpos::new(dir, cursor, 0), |k, v| { + if k.pos.inode != dir { + return Ok(false); + } + if k.pos.snapshot != self.snapshot || k.kind != kind::DIRENT { + return Ok(true); + } + if out.len() == max { + more = true; + return Ok(false); + } + out.push(entry(k, v)?); + Ok(true) + }) + .await?; + Ok((out, more)) + } + + /// The target of the symlink `ino`. + pub async fn readlink(&self, ino: u64) -> Result> { + let inode = self.inode(ino).await?; + if inode.mode & S_IFMT != 0o120000 || inode.size == 0 { + return Err(Error::Invalid("readlink of a node that is no symlink")); + } + if inode.size > MAX_SYMLINK { + return Err(Error::Unsupported("symlink target past 4 KiB")); + } + let mut target = alloc::vec![0u8; inode.size as usize]; + self.read(ino, 0, &mut target).await?; + // The target is stored with one NUL after it. + if target.pop() != Some(0) { + return Err(Error::Corrupt("symlink target without its NUL")); + } + Ok(target) + } + + /// Bytes of file `ino` from `offset` into `out`, as many as the file + /// holds there. Holes read as zeros. + pub async fn read(&self, ino: u64, offset: u64, out: &mut [u8]) -> Result { + let inode = self.inode(ino).await?; + let len = inode.size.saturating_sub(offset).min(out.len() as u64) as usize; + let out = &mut out[..len]; + out.fill(0); + if len == 0 { + return Ok(0); + } + let (start, end) = (offset, offset + len as u64); + let mut extents = Vec::new(); + self.walk(Btree::Extents, Bpos::new(ino, start / SECTOR as u64 + 1, 0), |k, v| { + if k.pos.inode != ino { + return Ok(false); + } + let first = extent_start(k)?; + if first >= end { + return Ok(false); + } + if k.pos.snapshot == self.snapshot { + extents.push((*k, v.to_vec())); + } + Ok(true) + }) + .await?; + for (key, value) in extents { + let first = extent_start(&key)?; + let last = first + u64::from(key.size) * SECTOR as u64; + let (from, to) = (first.max(start), last.min(end)); + if from >= to { + continue; + } + let dst = &mut out[(from - start) as usize..(to - start) as usize]; + match key.kind { + kind::EXTENT => self.extent(&key, &value, from - first, dst).await?, + kind::INLINE_DATA => { + // The bytes from the key's first; past the value, + // zeros. + let skip = (from - first) as usize; + let have = value.get(skip..).unwrap_or(&[]); + let n = have.len().min(dst.len()); + dst[..n].copy_from_slice(&have[..n]); + } + kind::RESERVATION | kind::WHITEOUT | kind::EXTENT_WHITEOUT => {} + kind::REFLINK_P => return Err(Error::Unsupported("reflinked extent")), + kind::ERROR => return Err(Error::Corrupt("extent recorded as lost")), + _ => return Err(Error::Corrupt("extents key of no extent type")), + } + } + Ok(len) + } + + /// `dst.len()` bytes of the extent `key` from `skip` bytes into its + /// range, its checksum verified or its MAC verified and decrypted. + async fn extent(&self, key: &Key, value: &[u8], skip: u64, dst: &mut [u8]) -> Result<()> { + let extent = Extent::decode(value)?; + let crc = extent.crc.ok_or(Error::Unsupported("extent without a checksum entry"))?; + if extent.ptr.unwritten { + return Ok(()); + } + if crc.compression != 0 || crc.compressed != crc.uncompressed { + return Err(Error::Unsupported("compressed extent")); + } + if extent.ptr.dev != 0 || extent.ptr.cached { + return Err(Error::Unsupported("extent on another device, or cached")); + } + if u64::from(crc.offset) + u64::from(key.size) > u64::from(crc.uncompressed) { + return Err(Error::Corrupt("extent past its checksummed range")); + } + self.in_one_bucket(extent.ptr.sector, u64::from(crc.compressed))?; + // An encrypted volume MACs all its data. + if self.sb.encrypted() && !matches!(crc.csum_type, 3 | 4) { + return Err(Error::Corrupt("unauthenticated extent on an encrypted volume")); + } + let mut data = read_bytes(&self.dev, extent.ptr.sector * SECTOR as u64, crc.compressed as usize * SECTOR).await?; + match crc.csum_type { + 0 => {} + // Over the whole encoded extent, the value zero-extended to the + // entry's checksum bytes. + 1 | 5 => { + let value = if crc.csum_type == 1 { crc32c(&data) } else { crc32c_zero(&data) }; + let mut want = [0u8; 16]; + want[..4].copy_from_slice(&value.to_le_bytes()); + if want != crc.csum { + return Err(Error::Checksum("extent")); + } + } + // An 80-bit MAC in a crc64 entry, a 128-bit one in a + // crc128. + 3 | 4 if crc.csum_len == if crc.csum_type == 3 { 10 } else { 16 } => { + let master = self.key.as_ref().ok_or(Error::Locked)?; + let nonce = Nonce::extent(key.version.hi, key.version.lo, crc.nonce); + master.verify(nonce, &data, &crc.csum[..crc.csum_len], "extent")?; + master.xor(nonce, &mut data)?; + } + _ => return Err(Error::Unsupported("data checksum type")), + } + let at = crc.offset as usize * SECTOR + skip as usize; + dst.copy_from_slice(&data[at..at + dst.len()]); + Ok(()) + } +} + +/// The byte an extent key's range starts at: its position is its end. +fn extent_start(k: &Key) -> Result { + let first = k.pos.offset.checked_sub(u64::from(k.size)).ok_or(Error::Corrupt("extent larger than its end"))?; + k.pos.offset.checked_mul(SECTOR as u64).ok_or(Error::Corrupt("extent past 2^64 bytes"))?; + Ok(first * SECTOR as u64) +} + +/// A dirent key and value as an entry. +fn entry(k: &Key, v: &[u8]) -> Result { + let dirent = Dirent::decode(v)?; + if dirent.kind == DirentType::Subvolume { + return Err(Error::Unsupported("subvolume dirent")); + } + let next = k.pos.offset.checked_add(1).ok_or(Error::Corrupt("dirent at the last offset"))?; + Ok(DirEntry { name: dirent.name.to_vec(), ino: dirent.target, kind: dirent.kind, next }) +} + +/// The hash slot of `name` under directory `dir`'s seed: SipHash-2-4 keyed +/// with the seed and zero, shifted to 63 bits, never 0 or 1, which are `.` +/// and `..`. +pub(crate) fn hash(dir: &InodeValue, name: &[u8]) -> Result { + if dir.hash_type() != 3 { + return Err(Error::Unsupported("directory name hash other than siphash")); + } + let mut key = [0u8; 16]; + key[..8].copy_from_slice(&dir.hash_seed.to_le_bytes()); + let mut h = siphash::siphash24(&key, name) >> 1; + if dir.flags & inode_flag::DIRENT_31_BIT != 0 { + h &= (1 << 31) - 1; + } + Ok(h.max(2)) +} + +/// `len` bytes from byte `at`, read through the device's whole blocks. +pub(crate) async fn read_bytes(dev: &impl BlockDev, at: u64, len: usize) -> Result> { + let first = at / BLOCK as u64; + let skip = (at % BLOCK as u64) as usize; + let blocks = (skip + len).div_ceil(BLOCK); + let mut buf = alloc::vec![[0u8; BLOCK]; blocks]; + dev.read(first, &mut buf).await?; + Ok(buf.as_flattened()[skip..skip + len].to_vec()) +} + +/// The primary copy's layout, or the layout copy's when the primary +/// is unreadable; then of every copy it lists, the valid one of highest +/// sequence, the later on a tie. +pub(crate) async fn read_superblock(dev: &impl BlockDev) -> Result { + let primary = read_copy(dev, PRIMARY).await; + let layout = match &primary { + Ok(sb) => sb.header.layout.clone(), + Err(Error::Device(e)) => return Err(Error::Device(*e)), + Err(_) => Layout::decode(&read_bytes(dev, LAYOUT * SECTOR as u64, Layout::BYTES).await?)?, + }; + let mut best: Option = primary.as_ref().ok().cloned(); + let mut failure = primary.err(); + for &offset in &layout.offsets { + if offset == PRIMARY { + continue; + } + match read_copy(dev, offset).await { + Ok(sb) if best.as_ref().is_none_or(|b| sb.header.seq >= b.header.seq) => best = Some(sb), + Ok(_) => {} + Err(Error::Device(e)) => return Err(Error::Device(e)), + Err(e) => failure = failure.or(Some(e)), + } + } + best.ok_or(failure.unwrap_or(Error::Corrupt("no superblock copy"))) +} + +async fn read_copy(dev: &impl BlockDev, sector: u64) -> Result { + let device = dev.blocks() * BLOCK as u64; + let fits = |len: usize| sector.checked_mul(SECTOR as u64).and_then(|at| at.checked_add(len as u64)).is_some_and(|end| end <= device); + if !fits(BLOCK) { + return Err(Error::Corrupt("superblock copy past the device")); + } + let at = sector * SECTOR as u64; + let head = read_bytes(dev, at, BLOCK).await?; + let total = HEADER + 8 * le32(&head, 124) as usize; + if total <= head.len() { + return Superblock::decode(&head, sector); + } + if total > MAX_BYTES || !fits(total) { + return Err(Error::Unsupported("superblock past 64 KiB or past the device")); + } + Superblock::decode(&read_bytes(dev, at, total).await?, sector) +} diff --git a/lib/bcachefs/src/engine/superblock.rs b/lib/bcachefs/src/engine/superblock.rs new file mode 100644 index 00000000000..05cb1b96ac4 --- /dev/null +++ b/lib/bcachefs/src/engine/superblock.rs @@ -0,0 +1,578 @@ +//! The superblock: its layout copy, fixed header and variable fields, +//! validated once into typed values. + +use alloc::vec::Vec; + +use super::bkey::{Bpos, Format, Key}; +use super::value::NodePtr; +use super::{field, kind, le16, le32, le64, put16, put32, put64, Btree, Error, Result, VERSION}; +use crate::crc32c::crc32c; + +/// The bcachefs magic, a UUID in RFC 4122 byte order. +pub(crate) const MAGIC: [u8; 16] = [0xc6, 0x85, 0x73, 0xf6, 0x66, 0xce, 0x90, 0xa9, 0xd9, 0x6a, 0x60, 0xcf, 0x80, 0x3d, 0xf7, 0xef]; +/// Where the primary copy and the layout copy start, in sectors. +pub(crate) const PRIMARY: u64 = 8; +pub(crate) const LAYOUT: u64 = 7; +/// The fixed header's bytes. +pub(crate) const HEADER: usize = 752; +/// The most a copy this reader reads may hold. Upstream's default reserve is +/// 1 MiB and its fields a few KiB; this bounds what an image can make it +/// allocate. +pub(crate) const MAX_BYTES: usize = 64 << 10; + +/// The largest btree node read, 256 KiB: `format`'s largest, and +/// what bounds a node read's allocation. +pub(crate) const MAX_NODE_SECTORS: u64 = 512; + +/// Feature bits this reader knows: 0 to 23 of word 0. +const KNOWN_FEATURES: u64 = (1 << 24) - 1; + +/// Variable-field types. +pub(crate) mod field_type { + pub const CRYPT: u32 = 2; + pub const REPLICAS_V0: u32 = 3; + pub const CLEAN: u32 = 6; + pub const REPLICAS: u32 = 7; + pub const BLACKLIST: u32 = 8; + pub const JOURNAL_V2: u32 = 9; + pub const COUNTERS: u32 = 10; + pub const MEMBERS_V2: u32 = 11; + pub const EXT: u32 = 13; + pub const DOWNGRADE: u32 = 14; + pub const RECOVERY_PASSES: u32 = 15; + pub const EXTENT_TYPE_U64S: u32 = 16; + pub const ERRORS_V2: u32 = 17; +} + +/// `bch_sb_layout`: where every copy is and what each may occupy. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct Layout { + pub max_bits: u8, + pub offsets: Vec, +} + +impl Layout { + pub(crate) const BYTES: usize = 512; + + pub(crate) fn decode(b: &[u8]) -> Result { + let (max_bits, count) = (b[17], usize::from(b[18])); + if b[..16] != MAGIC || b[16] != 0 || max_bits > 16 || !(1..=61).contains(&count) { + return Err(Error::Corrupt("superblock layout")); + } + let offsets: Vec = (0..count).map(|i| le64(b, 24 + 8 * i)).collect(); + if offsets.windows(2).any(|w| w[0].checked_add(1 << max_bits).is_none_or(|end| w[1] < end)) { + return Err(Error::Corrupt("superblock copies overlap")); + } + Ok(Layout { max_bits, offsets }) + } + + pub(crate) fn encode(&self) -> [u8; Self::BYTES] { + let mut b = [0u8; Self::BYTES]; + b[..16].copy_from_slice(&MAGIC); + b[17] = self.max_bits; + b[18] = self.offsets.len() as u8; + for (i, offset) in self.offsets.iter().enumerate() { + put64(&mut b, 24 + 8 * i, *offset); + } + b + } +} + +/// The fixed header's fields a reader or writer sets. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct Header { + pub version: u16, + pub version_min: u16, + pub internal_uuid: [u8; 16], + pub user_uuid: [u8; 16], + pub label: [u8; 32], + /// The sector this copy is at. + pub offset: u64, + pub seq: u64, + pub block_sectors: u16, + pub dev_idx: u8, + pub nr_devices: u8, + pub time_base: u64, + pub time_precision: u32, + pub flags: [u64; 7], + pub write_time: u64, + pub features: [u64; 2], + pub compat: [u64; 2], + pub layout: Layout, +} + +/// One variable field, as stored: its type and its body. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct Field { + pub kind: u32, + pub body: Vec, +} + +/// The member entry of the one device. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct Member { + pub uuid: [u8; 16], + pub nbuckets: u64, + pub first_bucket: u16, + pub bucket_sectors: u16, + pub bitmap_shift: u8, + pub last_mount: u64, + pub flags: u64, + pub seq: u64, + pub bitmap: u64, + pub journal_bucket: u32, + pub journal_offset: u32, + /// Device name, model and serial, which ToyOS writes as zeros. + pub identity_zero: bool, +} + +impl Member { + pub(crate) const BYTES: usize = 328; + + pub(crate) fn decode(e: &[u8]) -> Member { + // A shorter entry reads as zero-extended. + let mut b = [0u8; Self::BYTES]; + let n = e.len().min(Self::BYTES); + b[..n].copy_from_slice(&e[..n]); + Member { + uuid: b[..16].try_into().unwrap(), + nbuckets: le64(&b, 16), + first_bucket: le16(&b, 24), + bucket_sectors: le16(&b, 26), + bitmap_shift: b[28], + last_mount: le64(&b, 32), + flags: le64(&b, 40), + seq: le64(&b, 120), + bitmap: le64(&b, 128), + journal_bucket: le32(&b, 136), + journal_offset: le32(&b, 140), + identity_zero: b[144..224].iter().chain(&b[232..296]).all(|&x| x == 0), + } + } + + pub(crate) fn encode(&self) -> [u8; Self::BYTES] { + let mut b = [0u8; Self::BYTES]; + b[..16].copy_from_slice(&self.uuid); + put64(&mut b, 16, self.nbuckets); + put16(&mut b, 24, self.first_bucket); + put16(&mut b, 26, self.bucket_sectors); + b[28] = self.bitmap_shift; + put64(&mut b, 32, self.last_mount); + put64(&mut b, 40, self.flags); + put64(&mut b, 120, self.seq); + put64(&mut b, 128, self.bitmap); + put32(&mut b, 136, self.journal_bucket); + put32(&mut b, 140, self.journal_offset); + b + } + + /// Whether the btree bitmap covers sectors `[start, end)`. + pub(crate) fn bitmap_covers(&self, start: u64, end: u64) -> bool { + let shift = u32::from(self.bitmap_shift); + if shift >= 58 || end > 64 << shift { + return false; + } + (start >> shift..end.div_ceil(1 << shift)).all(|i| self.bitmap >> i & 1 != 0) + } +} + +/// Member flags. +pub(crate) mod member_flag { + pub const DISCARD: u64 = 1 << 14; + pub const DATA_ALLOWED: u64 = 0x1c << 15; + /// Durability 1, stored plus one. + pub const DURABILITY_1: u64 = 2 << 28; + pub const FREESPACE_INITIALIZED: u64 = 1 << 30; + pub const ROTATIONAL: u64 = 1 << 32; + pub const ROTATIONAL_KNOWN: u64 = 1 << 33; + pub const INIT_STATE: u64 = 0xf << 34; +} + +/// A btree root from `clean`: where its root node is. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct Root { + pub btree: Btree, + pub level: u8, + /// The root's key: its position is the root node's max key. + pub key: Key, + pub ptr: NodePtr, +} + +/// `clean`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct Clean { + pub journal_seq: u64, + pub roots: Vec, + /// The key-version counter. + pub key_version: Option, +} + +/// scrypt's cost as `crypt` stores it, each a log2. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Kdf { + pub log_n: u16, + pub log_r: u16, + pub log_p: u16, +} + +impl Kdf { + /// N, r and p, when r and p fit scrypt's `u32`s. + pub(crate) fn values(self) -> Result<(u64, u32, u32)> { + let pow = |log: u16| if log < 32 { Ok(1u32 << log) } else { Err(Error::Unsupported("scrypt r or p past 2^31")) }; + if self.log_n == 0 || self.log_n >= 64 { + return Err(Error::Corrupt("scrypt N not a power of two from 2 to 2^63")); + } + Ok((1 << self.log_n, pow(self.log_r)?, pow(self.log_p)?)) + } +} + +/// `crypt`: the key-derivation cost and the wrapped key. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct Crypt { + pub kdf: Kdf, + /// The key magic and master key, wrapped. + pub wrapped: [u8; 40], +} + +impl Crypt { + pub(crate) fn encode(&self) -> Vec { + let mut b = alloc::vec![0u8; 56]; + put64(&mut b, 8, u64::from(self.kdf.log_n) | u64::from(self.kdf.log_r) << 16 | u64::from(self.kdf.log_p) << 32); + b[16..].copy_from_slice(&self.wrapped); + b + } +} + +/// A replicas entry: a data type and the devices it lives on. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct Replicas { + pub data_type: u8, + pub required: u8, + pub devs: Vec, +} + +/// A validated superblock copy. +#[derive(Clone, Debug)] +pub struct Superblock { + pub(crate) header: Header, + pub(crate) fields: Vec, + pub(crate) member: Member, + pub(crate) clean: Option, + pub(crate) crypt: Option, + pub(crate) blacklist: Vec<(u64, u64)>, + pub(crate) replicas: Vec, +} + +/// The `[lo, lo + len)` bits of flag word `word`. +pub(crate) fn flag(flags: &[u64; 7], word: usize, lo: u32, len: u32) -> u64 { + field(flags[word], lo, len) +} + +impl Superblock { + /// The copy in `b`, read at `sector`, validated. + pub(crate) fn decode(b: &[u8], sector: u64) -> Result { + let header = decode_header(b)?; + if header.offset != sector { + return Err(Error::Corrupt("superblock copy read away from its own offset")); + } + let end = HEADER + 8 * le32(b, 124) as usize; + let mut fields = Vec::new(); + let mut at = HEADER; + while at < end { + let words = le32(b, at) as usize; + let len = words.checked_mul(8).filter(|&len| len >= 8 && len <= end - at).ok_or(Error::Corrupt("superblock field size"))?; + fields.push(Field { kind: le32(b, at + 4), body: b[at + 8..at + len].to_vec() }); + at += len; + } + let find = |kind: u32| fields.iter().find(|f| f.kind == kind).map(|f| f.body.as_slice()); + let member = members(find(field_type::MEMBERS_V2).ok_or(Error::Unsupported("superblock without members_v2"))?, &header)?; + if let Some(j) = find(field_type::JOURNAL_V2) { + journal(j, &member)?; + } + let clean = find(field_type::CLEAN).map(clean).transpose()?; + let crypt = find(field_type::CRYPT).map(crypt).transpose()?; + let blacklist = find(field_type::BLACKLIST).map(pairs).unwrap_or_default(); + let replicas = match (find(field_type::REPLICAS_V0), find(field_type::REPLICAS)) { + (Some(v0), _) => replicas(v0, false)?, + (None, Some(v1)) => replicas(v1, true)?, + (None, None) => Vec::new(), + }; + Ok(Superblock { header, fields, member, clean, crypt, blacklist, replicas }) + } + + /// This copy's bytes with `offset` as its own: the copies differ only + /// there and in the checksum. + pub(crate) fn encode(header: &Header, fields: &[Field], offset: u64) -> Vec { + let words: usize = fields.iter().map(|f| 1 + f.body.len().div_ceil(8)).sum(); + let mut b = alloc::vec![0u8; HEADER + 8 * words]; + put16(&mut b, 16, header.version); + put16(&mut b, 18, header.version_min); + b[24..40].copy_from_slice(&MAGIC); + b[40..56].copy_from_slice(&header.internal_uuid); + b[56..72].copy_from_slice(&header.user_uuid); + b[72..104].copy_from_slice(&header.label); + put64(&mut b, 104, offset); + put64(&mut b, 112, header.seq); + put16(&mut b, 120, header.block_sectors); + b[122] = header.dev_idx; + b[123] = header.nr_devices; + put32(&mut b, 124, words as u32); + put64(&mut b, 128, header.time_base); + put32(&mut b, 140, header.time_precision); + for (i, w) in header.flags.iter().enumerate() { + put64(&mut b, 144 + 8 * i, *w); + } + put64(&mut b, 200, header.write_time); + put64(&mut b, 208, header.features[0]); + put64(&mut b, 216, header.features[1]); + put64(&mut b, 224, header.compat[0]); + put64(&mut b, 232, header.compat[1]); + b[240..HEADER].copy_from_slice(&header.layout.encode()); + let mut at = HEADER; + for f in fields { + let words = 1 + f.body.len().div_ceil(8); + put32(&mut b, at, words as u32); + put32(&mut b, at + 4, f.kind); + b[at + 8..at + 8 + f.body.len()].copy_from_slice(&f.body); + at += 8 * words; + } + let csum = crc32c(&b[16..]); + put32(&mut b, 0, csum); + b + } + + pub(crate) fn flag(&self, word: usize, lo: u32, len: u32) -> u64 { + flag(&self.header.flags, word, lo, len) + } + + pub(crate) fn clean_flag(&self) -> bool { + self.flag(0, 1, 1) != 0 + } + + pub(crate) fn block_bytes(&self) -> usize { + usize::from(self.header.block_sectors) * super::SECTOR + } + + /// Btree node size in sectors. + pub(crate) fn node_sectors(&self) -> u32 { + self.flag(0, 12, 16) as u32 + } + + /// Encryption type 1, ChaCha20/Poly1305. + pub(crate) fn encrypted(&self) -> bool { + self.flag(1, 10, 4) != 0 + } + + /// The variable field of `kind`, as stored. + pub(crate) fn field(&self, kind: u32) -> Option<&[u8]> { + self.fields.iter().find(|f| f.kind == kind).map(|f| f.body.as_slice()) + } + + pub(crate) fn blacklisted(&self, seq: u64) -> bool { + self.blacklist.iter().any(|&(start, end)| (start..end).contains(&seq)) + } + + /// The internal UUID, which seeds the node magic and the key wrap. + pub fn internal_uuid(&self) -> [u8; 16] { + self.header.internal_uuid + } + + /// The user-visible UUID. + pub fn user_uuid(&self) -> [u8; 16] { + self.header.user_uuid + } +} + +fn decode_header(b: &[u8]) -> Result
{ + if b.len() < HEADER || b[24..40] != MAGIC { + return Err(Error::Corrupt("no bcachefs superblock")); + } + let layout = Layout::decode(&b[240..HEADER])?; + let total = HEADER + 8 * le32(b, 124) as usize; + if total > b.len() || total as u64 > 512 << layout.max_bits { + return Err(Error::Corrupt("superblock larger than its copy")); + } + let flags: [u64; 7] = core::array::from_fn(|i| le64(b, 144 + 8 * i)); + match field(flags[0], 2, 6) { + 0 => {} + 1 if crc32c(&b[16..total]) == le32(b, 0) => {} + 1 => return Err(Error::Checksum("superblock")), + _ => return Err(Error::Unsupported("superblock checksum other than crc32c")), + } + let (version, version_min) = (le16(b, 16), le16(b, 18)); + let major = |v: u16| v >> 10; + if major(version) != 1 || major(version_min) != 1 || version > VERSION || version_min > version { + return Err(Error::Unsupported("superblock version other than 1.x up to 1.39")); + } + if field(flags[5], 32, 16) > u64::from(VERSION) { + return Err(Error::Unsupported("superblock's incompatible version above 1.39")); + } + let features = [le64(b, 208), le64(b, 216)]; + if features[0] & !KNOWN_FEATURES != 0 || features[1] != 0 { + return Err(Error::Unsupported("feature bit this reader does not know")); + } + if field(flags[0], 62, 1) != 0 { + return Err(Error::Unsupported("big-endian metadata")); + } + if field(flags[1], 10, 4) > 1 { + return Err(Error::Unsupported("encryption other than ChaCha20/Poly1305")); + } + let header = Header { + version, + version_min, + internal_uuid: b[40..56].try_into().unwrap(), + user_uuid: b[56..72].try_into().unwrap(), + label: b[72..104].try_into().unwrap(), + offset: le64(b, 104), + seq: le64(b, 112), + block_sectors: le16(b, 120), + dev_idx: b[122], + nr_devices: b[123], + time_base: le64(b, 128), + time_precision: le32(b, 140), + flags, + write_time: le64(b, 200), + features, + compat: [le64(b, 224), le64(b, 232)], + layout, + }; + if header.internal_uuid == [0; 16] || header.user_uuid == [0; 16] { + return Err(Error::Corrupt("superblock with a zero UUID")); + } + if !header.block_sectors.is_power_of_two() || header.block_sectors > 128 { + return Err(Error::Corrupt("block size")); + } + if !(1..=1_000_000_000).contains(&header.time_precision) { + return Err(Error::Corrupt("time precision")); + } + if header.nr_devices != 1 || header.dev_idx != 0 { + return Err(Error::Unsupported("more than one device")); + } + let node = field(flags[0], 12, 16); + if node < u64::from(header.block_sectors) || node > MAX_NODE_SECTORS { + return Err(Error::Unsupported("btree node size below a block or past 256 KiB")); + } + Ok(header) +} + +fn members(body: &[u8], header: &Header) -> Result { + if body.len() < 8 { + return Err(Error::Corrupt("members_v2 without its header")); + } + let size = usize::from(le16(body, 0)); + let entry = body.get(8..8 + size).filter(|_| size >= 32).ok_or(Error::Corrupt("members_v2 without the device's entry"))?; + let member = Member::decode(entry); + let node_sectors = field(header.flags[0], 12, 16); + let usable = member.nbuckets.checked_sub(u64::from(member.first_bucket)); + if member.nbuckets > (1 << 31) - 65 + || usable.is_none_or(|n| n < 512) + || u64::from(member.bucket_sectors) < u64::from(header.block_sectors) + || u64::from(member.bucket_sectors) < node_sectors + || member.bitmap_shift >= 58 + { + return Err(Error::Corrupt("member geometry")); + } + Ok(member) +} + +/// The journal's bucket ranges lie on the device. +fn journal(body: &[u8], member: &Member) -> Result<()> { + let ranges = pairs(body); + let valid = ranges.iter().all(|&(start, count)| { + count > 0 && start > 0 && start >= u64::from(member.first_bucket) && start.checked_add(count).is_some_and(|end| end <= member.nbuckets) + }); + // Each count is at most the bucket count, so the sum fits. + if !valid || ranges.iter().map(|r| r.1).sum::() > u64::from(u32::MAX) { + return Err(Error::Corrupt("journal buckets")); + } + Ok(()) +} + +fn pairs(body: &[u8]) -> Vec<(u64, u64)> { + body.as_chunks::<16>().0.iter().map(|p| (le64(p, 0), le64(p, 8))).collect() +} + +/// A `jset_entry` record. +pub(crate) struct Entry<'a> { + pub btree: u8, + pub level: u8, + pub kind: u8, + pub payload: &'a [u8], +} + +/// The records of `b`, back to back. +pub(crate) fn entries(b: &[u8]) -> Result>> { + let mut out = Vec::new(); + let mut at = 0; + while at + 8 <= b.len() { + let len = usize::from(le16(b, at)) * 8; + let payload = b.get(at + 8..at + 8 + len).ok_or(Error::Corrupt("journal record past its end"))?; + out.push(Entry { btree: b[at + 2], level: b[at + 3], kind: b[at + 4], payload }); + at += 8 + len; + } + Ok(out) +} + +/// `jset_entry` types `clean` holds. +pub(crate) mod entry_type { + pub const BTREE_ROOT: u8 = 1; + pub const USAGE: u8 = 5; + pub const CLOCK: u8 = 7; +} +/// The usage record's counter id for the key version. +pub(crate) const KEY_VERSION: u8 = 2; + +fn clean(body: &[u8]) -> Result { + if body.len() < 16 { + return Err(Error::Corrupt("clean field shorter than 16 bytes")); + } + let mut clean = Clean { journal_seq: le64(body, 8), roots: Vec::new(), key_version: None }; + for Entry { btree, level, kind: kind_, payload } in entries(&body[16..])? { + match kind_ { + entry_type::BTREE_ROOT => { + let btree = Btree::from_id(u32::from(btree)).ok_or(Error::Unsupported("root of a btree this reader does not know"))?; + let (key, value) = Format::UNPACKED.key(payload)?; + if key.kind != kind::BTREE_PTR_V2 || key.size != 0 || usize::from(payload[0]) * 8 != payload.len() { + return Err(Error::Corrupt("btree root record")); + } + if key.pos != Bpos::MAX || level > 3 { + return Err(Error::Corrupt("btree root at a level or position no root has")); + } + clean.roots.retain(|r: &Root| r.btree != btree); + clean.roots.push(Root { btree, level, key, ptr: NodePtr::decode(value)? }); + } + entry_type::USAGE if btree == KEY_VERSION && payload.len() >= 8 => clean.key_version = Some(le64(payload, 0)), + _ => {} + } + } + Ok(clean) +} + +fn crypt(body: &[u8]) -> Result { + if body.len() < 56 { + return Err(Error::Corrupt("crypt field shorter than 64 bytes")); + } + if field(le64(body, 0), 0, 4) != 0 { + return Err(Error::Corrupt("crypt with a key derivation other than scrypt")); + } + let params = le64(body, 8); + let kdf = Kdf { log_n: field(params, 0, 16) as u16, log_r: field(params, 16, 16) as u16, log_p: field(params, 32, 16) as u16 }; + Ok(Crypt { kdf, wrapped: body[16..56].try_into().unwrap() }) +} + +fn replicas(body: &[u8], with_required: bool) -> Result> { + let mut out = Vec::new(); + let mut at = 0; + while at + 2 <= body.len() && body[at + 1] != 0 { + let (data_type, count) = (body[at], usize::from(body[at + 1])); + let (required, devs) = if with_required { + (*body.get(at + 2).ok_or(Error::Corrupt("replicas entry"))?, at + 3) + } else { + (1, at + 2) + }; + let devs = body.get(devs..devs + count).ok_or(Error::Corrupt("replicas entry"))?.to_vec(); + at += devs.len() + if with_required { 3 } else { 2 }; + out.push(Replicas { data_type, required, devs }); + } + Ok(out) +} diff --git a/lib/bcachefs/src/engine/tests.rs b/lib/bcachefs/src/engine/tests.rs new file mode 100644 index 00000000000..600207c8fef --- /dev/null +++ b/lib/bcachefs/src/engine/tests.rs @@ -0,0 +1,616 @@ +//! The reader against volumes upstream's tools wrote, and mkfs's volumes +//! against the reader and ToyOS's check. + +use std::io::Read; +use std::vec::Vec; + +use toyos_blockdev::{block_on_ready, BlockDev, Bytes, BLOCK}; +use toyos_crypto::{scrypt::Limits, Random}; + +use super::bkey::Bpos; +use super::node::{self, Build}; +use super::bkey::Key; +use super::value::{Extent, InodeValue}; +use super::*; + +type Image = Bytes>; + +fn fixture(name: &str) -> Image { + let path = format!("{}/tests/fixtures/{name}", env!("CARGO_MANIFEST_DIR")); + let mut raw = Vec::new(); + flate2::read::GzDecoder::new(std::fs::File::open(&path).unwrap()).read_to_end(&mut raw).unwrap(); + Bytes::new(raw) +} + +/// The passphrase the encrypted fixtures were formatted with; the tools +/// strip the newline its file ends in. +const PASSPHRASE: &[u8] = b"toyos-test-passphrase"; +/// Upstream's default scrypt: N = 2^14, r = 8, p = 16. +const LIMITS: Limits = Limits { memory: 17 << 20, work: 1 << 21 }; + +/// `seq 1 40000`'s output. +fn seq() -> Vec { + (1..=40000).flat_map(|n| format!("{n}\n").into_bytes()).collect() +} + +fn path(v: &Volume, path: &str) -> u64 { + block_on_ready(async { + let mut ino = v.root(); + for name in path.split('/') { + ino = v.lookup(ino, name.as_bytes()).await.unwrap().unwrap_or_else(|| panic!("{path}: no {name}")).ino; + } + ino + }) +} + +fn names(v: &Volume, dir: u64) -> Vec { + let (entries, more) = block_on_ready(v.readdir(dir, 0, 100)).unwrap(); + assert!(!more); + let mut names: Vec = entries.into_iter().map(|e| String::from_utf8(e.name).unwrap()).collect(); + names.sort(); + names +} + +fn read_all(v: &Volume, ino: u64) -> Result> { + block_on_ready(async { + let size = v.inode(ino).await?.size as usize; + let mut out = vec![0u8; size]; + assert_eq!(v.read(ino, 0, &mut out).await?, size); + Ok(out) + }) +} + +#[test] +fn the_crc32c_fixture_reads_its_tree() { + let dev = fixture("crc32c.img.gz"); + let v = block_on_ready(Volume::open(&dev)).unwrap(); + assert_eq!(v.root(), 4096); + assert_eq!(names(&v, v.root()), ["Documents", "empty", "lost+found"]); + assert_eq!(names(&v, path(&v, "Documents")), ["a.txt", "deep"]); + assert_eq!(names(&v, path(&v, "Documents/deep")), ["link", "seq.txt"]); + assert!(names(&v, path(&v, "empty")).is_empty()); + assert_eq!(read_all(&v, path(&v, "Documents/a.txt")).unwrap(), b"hello-toyos", "inline data"); + assert_eq!(block_on_ready(v.readlink(path(&v, "Documents/deep/link"))).unwrap(), b"../a.txt"); + let documents = block_on_ready(v.inode(path(&v, "Documents"))).unwrap(); + assert!(documents.is_dir()); + assert_eq!(documents.nlink, 3, "two names and one subdirectory"); + assert_eq!(block_on_ready(v.lookup(v.root(), b"absent")).unwrap(), None); + // Fourteen 32-sector extents, each checksummed type 5. + let seq_txt = path(&v, "Documents/deep/seq.txt"); + assert_eq!(read_all(&v, seq_txt).unwrap(), seq(), "every extent verified"); +} + +/// Type 5 is CRC-32C's register with no initial or final inversion: the +/// fixture's first extent stores 0x7080C241, which standard CRC-32C would +/// give as 0xE4E4C9C4, and its first two extents give 0x96C86455. +#[test] +fn the_fixtures_type_5_values_are_the_uninverted_crc32c() { + let dev = fixture("crc32c.img.gz"); + let v = block_on_ready(Volume::open(&dev)).unwrap(); + let ino = path(&v, "Documents/deep/seq.txt"); + let mut first = None; + block_on_ready(v.walk(Btree::Extents, Bpos::new(ino, 1, 0), |_, value| { + first = Some(Extent::decode(value)?.crc.unwrap()); + Ok(false) + })) + .unwrap(); + let crc = first.unwrap(); + assert_eq!((crc.csum_type, crc.compressed), (5, 32)); + assert_eq!(crc.csum[..4], 0x7080_C241u32.to_le_bytes()); + let seq = seq(); + use crate::crc32c::{crc32c, crc32c_zero}; + assert_eq!(crc32c_zero(&seq[..16384]), 0x7080_C241); + assert_eq!(crc32c(&seq[..16384]), 0xE4E4_C9C4); + assert_eq!(crc32c_zero(&seq[..32768]), 0x96C8_6455); + assert_eq!(crc32c_zero(&seq[..512]), 0xE5BA_59C6); +} + +fn encrypted(name: &str) { + let dev = fixture(name); + assert!(matches!(block_on_ready(Volume::open(&dev)), Err(Error::Locked))); + assert!(matches!(block_on_ready(Volume::unlock(&dev, b"toyos-test-passphrasf", LIMITS)), Err(Error::Passphrase))); + let small = Limits { memory: 1 << 20, work: 1 << 21 }; + assert!(matches!(block_on_ready(Volume::unlock(&dev, PASSPHRASE, small)), Err(Error::Unsupported(_))), "scrypt past the caller's memory"); + let v = block_on_ready(Volume::unlock(&dev, PASSPHRASE, LIMITS)).unwrap(); + assert!(names(&v, v.root()).starts_with(&["a.txt".into()])); + assert_eq!(read_all(&v, path(&v, "a.txt")).unwrap(), b"hello-toyos"); + assert_eq!(read_all(&v, path(&v, "seq.txt")).unwrap(), seq(), "every extent's MAC verified and decrypted"); + let mut middle = [0u8; 10]; + assert_eq!(block_on_ready(v.read(path(&v, "seq.txt"), 100_000, &mut middle)).unwrap(), 10); + assert_eq!(middle, seq()[100_000..100_010]); +} + +#[test] +fn the_80_bit_mac_fixture_unlocks_and_reads() { + encrypted("encrypted.img.gz"); +} + +#[test] +fn the_128_bit_mac_fixture_unlocks_and_reads() { + encrypted("encrypted-wide-macs.img.gz"); +} + +/// Flips a byte at `at` bytes into the device. +fn flip(dev: &Image, at: u64) { + let mut block = [[0u8; BLOCK]]; + let lba = at / BLOCK as u64; + block_on_ready(dev.read(lba, &mut block)).unwrap(); + block[0][(at % BLOCK as u64) as usize] ^= 0x40; + block_on_ready(dev.write(lba, &block)).unwrap(); +} + +#[test] +fn a_flipped_byte_in_an_encrypted_extent_or_node_fails_its_mac() { + for name in ["encrypted.img.gz", "encrypted-wide-macs.img.gz"] { + let dev = fixture(name); + let v = block_on_ready(Volume::unlock(&dev, PASSPHRASE, LIMITS)).unwrap(); + let ino = path(&v, "seq.txt"); + let mut first = None; + block_on_ready(v.walk(Btree::Extents, Bpos::new(ino, 1, 0), |k, value| { + first = Some(Extent::decode(value)?.ptr.sector); + assert_eq!(k.kind, kind::EXTENT); + Ok(false) + })) + .unwrap(); + flip(&dev, first.unwrap() * SECTOR as u64 + 100); + assert_eq!(read_all(&v, ino), Err(Error::Checksum("extent")), "{name}"); + + let inodes = v.roots().iter().find(|r| r.btree == Btree::Inodes).unwrap().ptr.ptr.sector; + flip(&dev, inodes * SECTOR as u64 + 200); + assert_eq!(block_on_ready(v.inode(4096)), Err(Error::Checksum("btree node")), "{name}"); + } +} + +/// xorshift64*, for the tests' identifiers and keys. +struct Seeded(u64); + +impl Random for Seeded { + fn fill(&mut self, out: &mut [u8]) -> core::result::Result<(), toyos_crypto::Error> { + for b in out { + self.0 ^= self.0 << 13; + self.0 ^= self.0 >> 7; + self.0 ^= self.0 << 17; + *b = (self.0.wrapping_mul(0x2545_f491_4f6c_dd1d) >> 56) as u8; + } + Ok(()) + } +} + +/// A volume secret as DATA's passphrase: 64 lowercase hex characters, here +/// of bytes 00–1f. +fn hex_secret() -> Vec { + (0u8..32).flat_map(|b| format!("{b:02x}").into_bytes()).collect() +} + +fn formatted(bytes: usize, encryption: Option) -> Image { + let dev = Bytes::new(vec![0u8; bytes]); + let options = Options { label: b"toyos", time: 1_761_177_600_000_000_000, encryption }; + block_on_ready(mkfs(&dev, &options, &mut Seeded(0x5eed))).unwrap(); + dev +} + +fn empty_and_clean(v: &Volume) { + assert_eq!(v.root(), 4096); + assert!(names(v, v.root()).is_empty(), "no lost+found"); + let root = block_on_ready(v.inode(v.root())).unwrap(); + assert_eq!((root.mode, root.nlink, root.size), (0o40755, 2, 0)); + assert_eq!(root.mtime, 1_761_177_600_000_000_000, "the time base"); + assert_eq!(block_on_ready(fsck(v)).unwrap(), []); +} + +#[test] +fn an_unencrypted_volume_reads_back_empty_and_checks_clean() { + // 8 MiB: the fewest buckets, 512. 32 MiB: 16 KiB buckets and nodes, so + // alloc spans three leaves under an interior root. 1 GiB: 512 KiB + // buckets of two 256 KiB nodes, so a half-filled bucket takes a + // fragmentation entry. + for size in [8 << 20, 32 << 20, 1 << 30] { + let dev = formatted(size, None); + empty_and_clean(&block_on_ready(Volume::open(&dev)).unwrap()); + } +} + +#[test] +fn an_encrypted_volume_unlocks_from_its_hex_secret_and_checks_clean() { + let secret = hex_secret(); + let dev = formatted(64 << 20, Some(Encryption { passphrase: &secret, kdf: MIN_KDF })); + assert!(matches!(block_on_ready(Volume::open(&dev)), Err(Error::Locked))); + let tiny = Limits { memory: 1024, work: 2 }; + assert!(matches!(block_on_ready(Volume::unlock(&dev, b"0001", tiny)), Err(Error::Passphrase))); + let v = block_on_ready(Volume::unlock(&dev, &secret, tiny)).unwrap(); + assert!(v.superblock().encrypted()); + empty_and_clean(&v); +} + +#[test] +fn mkfs_is_a_function_of_its_random_source_and_time() { + let a = formatted(32 << 20, None).into_inner(); + let b = formatted(32 << 20, None).into_inner(); + assert!(a == b); +} + +#[test] +fn a_lost_primary_superblock_is_read_from_another_copy_and_found() { + let dev = formatted(32 << 20, None); + block_on_ready(dev.write(1, &[[0u8; BLOCK]])).unwrap(); + let v = block_on_ready(Volume::open(&dev)).unwrap(); + assert_eq!(names(&v, v.root()), Vec::::new()); + assert_eq!(block_on_ready(fsck(&v)).unwrap(), [Finding::Copy(8)]); +} + +/// Rewrites `btree`'s root leaf on an unencrypted mkfs volume with its keys +/// as `edit` leaves them, the bset stamped `bset_seq`. The keys bypass the +/// writer's type check, as a hostile image's would. +fn rewrite_leaf(dev: &Image, btree: Btree, bset_seq: u64, edit: impl FnOnce(&mut Vec<(Key, Vec)>)) { + let v = block_on_ready(Volume::open(dev)).unwrap(); + let root = *v.roots().iter().find(|r| r.btree == btree).unwrap(); + assert_eq!(root.level, 0); + let mut keys = block_on_ready(v.node(btree, 0, &root.ptr, root.key.pos)).unwrap().keys; + edit(&mut keys); + keys.sort_by_key(|(k, _)| k.pos); + let encoded: Vec> = keys.iter().map(|(k, val)| k.encode(val)).collect(); + // A placeholder bset, then the keys put in raw, so the writer's own + // assertions never see them. + let build = Build { btree, level: 0, seq: root.ptr.seq, min: Bpos::MIN, max: Bpos::MAX, journal_seq: bset_seq, keys: &[] }; + let mut bytes = node::encode(node::magic(&v.superblock().internal_uuid()), BLOCK, None, &build).unwrap(); + let flat = encoded.concat(); + let end = 160 + flat.len(); + bytes.resize(end.next_multiple_of(BLOCK), 0); + bytes[160..end].copy_from_slice(&flat); + bytes[158..160].copy_from_slice(&((flat.len() / 8) as u16).to_le_bytes()); + let crc = crate::crc32c::crc32c(&bytes[16..end]); + bytes[..4].copy_from_slice(&crc.to_le_bytes()); + assert!(bytes.len() <= usize::from(root.ptr.written) * 512, "the rewrite outgrew the node's written sectors"); + let mut blocks = vec![[0u8; BLOCK]; bytes.len() / BLOCK]; + blocks.as_flattened_mut().copy_from_slice(&bytes); + block_on_ready(dev.write(root.ptr.ptr.sector / 8, &blocks)).unwrap(); +} + +/// The inodes root stamped `bset_seq`, the root directory `inode_seq`. +fn stamp(dev: &Image, bset_seq: u64, inode_seq: u64) { + rewrite_leaf(dev, Btree::Inodes, bset_seq, |keys| { + for (_, val) in keys.iter_mut() { + let mut inode = InodeValue::decode(val).unwrap(); + inode.journal_seq = inode_seq; + *val = inode.encode(); + } + }); +} + +#[test] +fn a_journal_sequence_stamped_past_clean_is_found() { + let dev = formatted(32 << 20, None); + stamp(&dev, 0, 0); + let v = block_on_ready(Volume::open(&dev)).unwrap(); + assert_eq!(block_on_ready(fsck(&v)).unwrap(), [], "the rewrite alone changes nothing"); + let sector = v.roots().iter().find(|r| r.btree == Btree::Inodes).unwrap().ptr.ptr.sector; + + stamp(&dev, 1, 0); + let v = block_on_ready(Volume::open(&dev)).unwrap(); + assert_eq!(block_on_ready(fsck(&v)).unwrap(), [Finding::BsetAboveClean { btree: Btree::Inodes, sector, seq: 1 }]); + + stamp(&dev, 0, 1); + let v = block_on_ready(Volume::open(&dev)).unwrap(); + assert_eq!(block_on_ready(fsck(&v)).unwrap(), [Finding::InodeAboveClean { ino: 4096, seq: 1 }]); +} + +#[test] +fn a_node_without_a_mac_on_an_encrypted_volume_is_refused() { + let secret = hex_secret(); + let dev = formatted(32 << 20, Some(Encryption { passphrase: &secret, kdf: MIN_KDF })); + let tiny = Limits { memory: 1024, work: 2 }; + let v = block_on_ready(Volume::unlock(&dev, &secret, tiny)).unwrap(); + let root = *v.roots().iter().find(|r| r.btree == Btree::Inodes).unwrap(); + let node = block_on_ready(v.node(Btree::Inodes, 0, &root.ptr, root.key.pos)).unwrap(); + let keys: Vec> = node.keys.iter().map(|(k, val)| k.encode(val)).collect(); + // The same keys, checksummed crc32c instead of MACed. + let build = Build { btree: Btree::Inodes, level: 0, seq: root.ptr.seq, min: Bpos::MIN, max: Bpos::MAX, journal_seq: 0, keys: &keys }; + let bytes = node::encode(node::magic(&v.superblock().internal_uuid()), BLOCK, None, &build).unwrap(); + let mut blocks = vec![[0u8; BLOCK]; bytes.len() / BLOCK]; + blocks.as_flattened_mut().copy_from_slice(&bytes); + block_on_ready(dev.write(root.ptr.ptr.sector / 8, &blocks)).unwrap(); + assert_eq!(block_on_ready(v.inode(4096)), Err(Error::Corrupt("unauthenticated btree node on an encrypted volume"))); +} + +#[test] +fn a_device_under_512_buckets_is_refused() { + let dev = Bytes::new(vec![0u8; 4 << 20]); + let options = Options { label: b"", time: 0, encryption: None }; + assert_eq!(block_on_ready(mkfs(&dev, &options, &mut Seeded(1))), Err(Error::Invalid("device smaller than 512 buckets"))); +} + +#[test] +fn a_scrypt_cost_past_any_limit_is_refused_at_mkfs() { + let dev = Bytes::new(vec![0u8; 8 << 20]); + let kdf = Kdf { log_n: 63, log_r: 31, log_p: 31 }; + let options = Options { label: b"", time: 0, encryption: Some(Encryption { passphrase: b"x", kdf }) }; + assert!(matches!(block_on_ready(mkfs(&dev, &options, &mut Seeded(1))), Err(Error::Unsupported(_)))); +} + +/// The primary copy re-encoded after `edit`, written over every copy. +fn rewrite_superblock(dev: &Image, edit: impl FnOnce(&mut super::superblock::Header, &mut Vec)) { + let mut block = [[0u8; BLOCK]]; + block_on_ready(dev.read(1, &mut block)).unwrap(); + let sb = super::superblock::Superblock::decode(&block[0], 8).unwrap(); + let (mut header, mut fields) = (sb.header.clone(), sb.fields.clone()); + edit(&mut header, &mut fields); + for &offset in &sb.header.layout.offsets { + let bytes = super::superblock::Superblock::encode(&header, &fields, offset); + let mut blocks = vec![[0u8; BLOCK]; bytes.len().div_ceil(BLOCK)]; + blocks.as_flattened_mut()[..bytes.len()].copy_from_slice(&bytes); + block_on_ready(dev.write(offset / 8, &blocks)).unwrap(); + } +} + +fn open_err(dev: &Image) -> Error { + match block_on_ready(Volume::open(dev)) { + Ok(_) => panic!("opened"), + Err(e) => e, + } +} + +#[test] +fn a_members_field_without_its_header_is_refused() { + let dev = formatted(8 << 20, None); + rewrite_superblock(&dev, |_, fields| fields.iter_mut().find(|f| f.kind == 11).unwrap().body.clear()); + assert_eq!(open_err(&dev), Error::Corrupt("members_v2 without its header")); +} + +#[test] +fn journal_buckets_whose_counts_overflow_are_refused() { + let dev = formatted(8 << 20, None); + rewrite_superblock(&dev, |_, fields| { + let body = [(1u64, 1u64 << 63), (2, 1 << 63)].iter().flat_map(|(a, b)| [a.to_le_bytes(), b.to_le_bytes()].concat()).collect(); + fields.iter_mut().find(|f| f.kind == 9).unwrap().body = body; + }); + assert_eq!(open_err(&dev), Error::Corrupt("journal buckets")); +} + +#[test] +fn a_superblock_copy_past_the_device_is_found_not_read() { + let dev = formatted(8 << 20, None); + rewrite_superblock(&dev, |header, _| header.layout.offsets[2] = 1 << 60); + let v = block_on_ready(Volume::open(&dev)).unwrap(); + assert!(block_on_ready(fsck(&v)).unwrap().contains(&Finding::Copy(1 << 60))); +} + +#[test] +fn a_link_count_past_32_bits_is_refused() { + let dev = formatted(8 << 20, None); + rewrite_leaf(&dev, Btree::Inodes, 0, |keys| { + let mut inode = InodeValue::decode(&keys[0].1).unwrap(); + inode.set(super::value::inode_field::NLINK, u64::from(u32::MAX)); + keys[0].1 = inode.encode(); + }); + let v = block_on_ready(Volume::open(&dev)).unwrap(); + assert_eq!(block_on_ready(v.inode(4096)), Err(Error::Corrupt("inode link count past 2^32"))); +} + +/// A dirent value: target, type, name. +fn dirent(target: u64, kind: u8, name: &[u8]) -> Vec { + [&target.to_le_bytes()[..], &[kind], name].concat() +} + +#[test] +fn a_dirent_at_the_last_offset_is_refused() { + let dev = formatted(8 << 20, None); + rewrite_leaf(&dev, Btree::Dirents, 0, |keys| keys.push((Key::new(kind::DIRENT, Bpos::new(4096, u64::MAX, u32::MAX)), dirent(4096 + 1, 8, b"x")))); + let v = block_on_ready(Volume::open(&dev)).unwrap(); + assert_eq!(block_on_ready(v.readdir(4096, 0, 10)).err(), Some(Error::Corrupt("dirent at the last offset"))); +} + +#[test] +fn a_symlink_past_4_kib_is_refused_before_it_is_allocated() { + let dev = formatted(8 << 20, None); + rewrite_leaf(&dev, Btree::Inodes, 0, |keys| { + let mut link = InodeValue::decode(&keys[0].1).unwrap(); + link.flags = 0o120777 << 36; + link.size = 1 << 40; + keys.push((Key::new(kind::INODE_V3, Bpos::new(0, 1 << 31, u32::MAX)), link.encode())); + }); + let v = block_on_ready(Volume::open(&dev)).unwrap(); + assert_eq!(block_on_ready(v.readlink(1 << 31)), Err(Error::Unsupported("symlink target past 4 KiB"))); +} + +#[test] +fn a_snapshotted_root_subvolume_is_refused() { + let dev = formatted(8 << 20, None); + rewrite_leaf(&dev, Btree::Subvolumes, 0, |keys| keys[0].1 = super::value::subvolume(u32::MAX - 1, 4096).to_vec()); + assert_eq!(open_err(&dev), Error::Unsupported("a root subvolume that has been snapshotted")); +} + +#[test] +fn a_strict_key_in_another_btrees_leaf_is_refused_and_a_set_key_is_found() { + let dev = formatted(8 << 20, None); + rewrite_leaf(&dev, Btree::Inodes, 0, |keys| keys.push((Key::new(kind::ALLOC_V4, Bpos::new(0, 5000, u32::MAX)), vec![0; 64]))); + let v = block_on_ready(Volume::open(&dev)).unwrap(); + assert_eq!(block_on_ready(v.inode(4096)).err(), Some(Error::Corrupt("key of a type its btree does not hold"))); + + // A set key is non-strict: upstream reads it anywhere, and refuses only + // its commit. The reader passes it; ToyOS's check finds it. + let dev = formatted(8 << 20, None); + let pos = Bpos::new(0, 5000, u32::MAX); + rewrite_leaf(&dev, Btree::Inodes, 0, |keys| keys.push((Key::new(kind::SET, pos), vec![]))); + let v = block_on_ready(Volume::open(&dev)).unwrap(); + assert!(block_on_ready(v.inode(4096)).is_ok()); + assert_eq!(block_on_ready(fsck(&v)).unwrap(), [Finding::KeyType { btree: Btree::Inodes, level: 0, kind: kind::SET, pos }]); +} + +#[test] +#[should_panic(expected = "a key of type 27 in a Inodes node")] +fn the_writer_refuses_a_key_its_btree_does_not_hold() { + let key = Key::new(kind::ALLOC_V4, Bpos::new(0, 1, 0)).encode(&[0; 64]); + let build = Build { btree: Btree::Inodes, level: 0, seq: 1, min: Bpos::MIN, max: Bpos::MAX, journal_seq: 0, keys: &[key] }; + let _ = node::encode(0, BLOCK, None, &build); +} + +#[test] +fn a_passphrase_upstream_would_cut_or_strip_is_refused_at_mkfs() { + for passphrase in [&b"secret\n"[..], b"sec\0ret"] { + let dev = Bytes::new(vec![0u8; 8 << 20]); + let options = Options { label: b"", time: 0, encryption: Some(Encryption { passphrase, kdf: MIN_KDF }) }; + assert_eq!(block_on_ready(mkfs(&dev, &options, &mut Seeded(1))), Err(Error::Invalid("passphrase holding NUL or ending in a newline"))); + } +} + +/// What the reader and the check do over a volume, bounded so a mutated +/// tree's loops and sizes end: open, check and list the root, then with +/// `deep` every directory to depth 8, every file's first 64 KiB and every +/// symlink. +fn exercise(dev: &Image, unlock: Option<&[u8]>, deep: bool) { + let opened = match unlock { + Some(passphrase) => block_on_ready(Volume::unlock(dev, passphrase, Limits { memory: 17 << 20, work: 1 << 21 })), + None => block_on_ready(Volume::open(dev)), + }; + let Ok(v) = opened else { return }; + let _ = block_on_ready(fsck(&v)); + let mut pending = vec![(v.root(), 0)]; + let mut seen = 0; + while let Some((dir, depth)) = pending.pop() { + let Ok((entries, _)) = block_on_ready(v.readdir(dir, 0, 64)) else { continue }; + for e in entries { + seen += 1; + if seen > 256 { + return; + } + let _ = block_on_ready(v.lookup(dir, &e.name)); + if !deep { + continue; + } + match e.kind { + DirentType::Directory if depth < 8 => pending.push((e.ino, depth + 1)), + DirentType::Symlink => drop(block_on_ready(v.readlink(e.ino))), + _ => drop(block_on_ready(v.read(e.ino, 0, &mut [0u8; 65536]))), + } + } + } +} + +/// Every node's place and written length. +fn nodes_of(dev: &Image, unlock: Option<&[u8]>) -> Vec<(usize, usize)> { + let v = match unlock { + Some(p) => block_on_ready(Volume::unlock(dev, p, LIMITS)).unwrap(), + None => block_on_ready(Volume::open(dev)).unwrap(), + }; + let mut out = Vec::new(); + for root in v.roots() { + let mut pending = vec![(root.level, root.ptr, root.key.pos)]; + while let Some((level, ptr, max)) = pending.pop() { + out.push((ptr.ptr.sector as usize * 512, usize::from(ptr.written) * 512)); + let node = block_on_ready(v.node(root.btree, level, &ptr, max)).unwrap(); + if level > 0 { + for (k, val) in &node.keys { + pending.push((level - 1, super::value::NodePtr::decode(val).unwrap(), k.pos)); + } + } + } + } + out +} + +fn le(b: &[u8], at: usize, n: usize) -> usize { + b[at..at + n].iter().rev().fold(0usize, |a, &x| a << 8 | usize::from(x)) +} + +/// The crc32c of a superblock copy at `at`, recomputed so a mutation +/// reaches the decoders behind it. +fn reseal_superblock(b: &mut [u8], at: usize, span: usize) { + let total = 752 + 8 * le(b, at + 124, 4); + if b[at + 144] >> 2 & 0x3f == 1 && total <= span { + let crc = crate::crc32c::crc32c(&b[at + 16..at + total]); + b[at..at + 4].copy_from_slice(&crc.to_le_bytes()); + } +} + +/// The same for a node's first record, where it is checksummed crc32c. +fn reseal_node(b: &mut [u8], at: usize, len: usize) { + let end = 160 + 8 * le(b, at + 158, 2); + if b[at + 152] & 0xf == 1 && end <= len { + let crc = crate::crc32c::crc32c(&b[at + 16..at + end]); + b[at..at + 4].copy_from_slice(&crc.to_le_bytes()); + } +} + +/// Writes `bytes`' blocks `[at, at + len)`. +fn put(dev: &Image, bytes: &[u8], at: usize, len: usize) { + let (first, last) = (at / BLOCK, (at + len).div_ceil(BLOCK)); + let blocks: Vec<[u8; BLOCK]> = bytes[first * BLOCK..last * BLOCK].as_chunks::().0.to_vec(); + block_on_ready(dev.write(first as u64, &blocks)).unwrap(); +} + +const BOUNDARY: [u64; 6] = [0, 1, u64::MAX, u64::MAX - 1, 1 << 63, u32::MAX as u64]; + +/// Structured mutations of upstream's and mkfs's volumes, checksums +/// resealed so each reaches what lies behind them. Every aligned word of the +/// superblock is set to each boundary value in every copy at once, so copy +/// selection cannot step around it; nodes take seeded random bytes and +/// boundary words. Whatever the reader and the check answer, neither panics. +#[test] +fn mutated_volumes_are_refused_without_a_panic() { + let secret = hex_secret(); + // The encrypted fixtures' scrypt is upstream's default, far too costly + // per round; mkfs's encrypted volume takes the same paths at N = 2. + let images: [(Image, Option<&[u8]>, usize); 3] = [ + (fixture("crc32c.img.gz"), None, 1500), + (formatted(8 << 20, None), None, 1500), + (formatted(8 << 20, Some(Encryption { passphrase: &secret, kdf: MIN_KDF })), Some(&secret), 300), + ]; + let mut rng = Seeded(0xf022); + let mut draw = |n: usize| { + let mut b = [0u8; 8]; + rng.fill(&mut b).unwrap(); + (u64::from_le_bytes(b) % n as u64) as usize + }; + for (dev, unlock, rounds) in images { + let nodes = nodes_of(&dev, unlock); + let pristine = dev.get().clone(); + let used = 752 + 8 * le(&pristine, 4096 + 124, 4); + let span = used.next_multiple_of(BLOCK); + let primary = super::superblock::Superblock::decode(&pristine[4096..4096 + span], 8).unwrap(); + let copies: Vec = primary.header.layout.offsets.iter().map(|&o| o as usize * 512).collect(); + // The header and each field's first 128 bytes: the rest of a large + // field is records the sweep's offsets already exercise. + let mut offsets: Vec = (0..752).collect(); + let mut field = 752; + while field + 8 <= used { + let len = (8 * le(&pristine, 4096 + field, 4)).max(8); + offsets.extend(field..(field + len.min(128)).min(used)); + field += len; + } + for width in [4, 8] { + for at in offsets.iter().copied().filter(|at| at % width == 0) { + for value in BOUNDARY { + let mut bytes = pristine.clone(); + for © in &copies { + bytes[copy + at..copy + at + width].copy_from_slice(&value.to_le_bytes()[..width]); + reseal_superblock(&mut bytes, copy, span); + put(&dev, &bytes, copy, span); + } + exercise(&dev, unlock, false); + for © in &copies { + put(&dev, &pristine, copy, span); + } + } + } + } + for _ in 0..rounds { + let (at, len) = nodes[draw(nodes.len())]; + let mut bytes = pristine[at..at + len].to_vec(); + for _ in 0..1 + draw(3) { + if draw(2) == 0 { + bytes[draw(len)] = draw(256) as u8; + } else { + let width = [2, 4, 8][draw(3)]; + let off = draw(len / width) * width; + bytes[off..off + width].copy_from_slice(&BOUNDARY[draw(BOUNDARY.len())].to_le_bytes()[..width]); + } + } + reseal_node(&mut bytes, 0, len); + let mut image = pristine.clone(); + image[at..at + len].copy_from_slice(&bytes); + put(&dev, &image, at, len); + exercise(&dev, unlock, true); + put(&dev, &pristine, at, len); + } + } +} diff --git a/lib/bcachefs/src/engine/value.rs b/lib/bcachefs/src/engine/value.rs new file mode 100644 index 00000000000..8dcd56aa788 --- /dev/null +++ b/lib/bcachefs/src/engine/value.rs @@ -0,0 +1,579 @@ +//! Values: what follows a key, by key type. + +use alloc::vec::Vec; + +use super::bkey::Bpos; +use super::{field, le16, le32, le64, put16, put32, put64, Error, Result}; + +/// The inode fields' varint: `n` bytes for a value of up to 7·`n` bits, `n` at most 8, +/// or 0xff and eight bytes. +pub(crate) fn varint(out: &mut Vec, v: u64) { + let n = (64 - (v | 1).leading_zeros()).div_ceil(7); + if n >= 9 { + out.push(0xff); + out.extend_from_slice(&v.to_le_bytes()); + } else { + let encoded = (v << n) | ((1 << (n - 1)) - 1); + out.extend_from_slice(&encoded.to_le_bytes()[..n as usize]); + } +} + +/// The varint at the start of `b` and its length. +pub(crate) fn read_varint(b: &[u8]) -> Result<(u64, usize)> { + let first = *b.first().ok_or(Error::Corrupt("varint past the end of its value"))?; + let n = first.trailing_ones() as usize + 1; + if n == 9 { + let v = b.get(1..9).ok_or(Error::Corrupt("varint past the end of its value"))?; + return Ok((u64::from_le_bytes(v.try_into().unwrap()), 9)); + } + let bytes = b.get(..n).ok_or(Error::Corrupt("varint past the end of its value"))?; + let mut word = [0u8; 8]; + word[..n].copy_from_slice(bytes); + Ok((u64::from_le_bytes(word) >> n, n)) +} + +/// What an inode_v3 value holds. Times are in the volume's +/// units since its time base, as stored. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub(crate) struct InodeValue { + pub journal_seq: u64, + pub hash_seed: u64, + pub flags: u64, + pub sectors: u64, + pub size: u64, + /// The 28 packed fields, field `n` at index `n - 1`. + pub fields: [u64; INODE_FIELDS], +} + +pub(crate) const INODE_FIELDS: usize = 28; +/// Each packed field's width in bits; 96 is two varints, low then high. +const INODE_WIDTH: [u32; INODE_FIELDS] = + [96, 96, 96, 96, 32, 32, 32, 32, 32, 8, 8, 32, 8, 8, 16, 16, 16, 16, 16, 64, 64, 32, 32, 8, 32, 8, 8, 8]; +/// A 1.39 writer's fields-start, in u64s. +const FIELDS_START: u64 = 6; + +/// Packed field numbers this crate reads or writes. +pub(crate) mod inode_field { + pub const ATIME: usize = 1; + pub const CTIME: usize = 2; + pub const MTIME: usize = 3; + pub const OTIME: usize = 4; + pub const UID: usize = 5; + pub const GID: usize = 6; + pub const NLINK: usize = 7; + pub const GENERATION: usize = 8; + pub const RDEV: usize = 9; + pub const DIR: usize = 20; + pub const DIR_OFFSET: usize = 21; + pub const SUBVOL: usize = 22; + pub const DEPTH: usize = 25; +} + +/// Inode flag bits. +pub(crate) mod inode_flag { + pub const UNLINKED: u64 = 1 << 7; + pub const DIRENT_31_BIT: u64 = 1 << 11; +} + +impl InodeValue { + pub(crate) fn field(&self, n: usize) -> u64 { + self.fields[n - 1] + } + + pub(crate) fn set(&mut self, n: usize, v: u64) { + self.fields[n - 1] = v; + } + + pub(crate) fn mode(&self) -> u16 { + field(self.flags, 36, 16) as u16 + } + + pub(crate) fn hash_type(&self) -> u8 { + field(self.flags, 20, 4) as u8 + } + + pub(crate) fn decode(v: &[u8]) -> Result { + if v.len() < 48 { + return Err(Error::Corrupt("inode value shorter than 48 bytes")); + } + let flags = le64(v, 16); + let (count, start) = (field(flags, 24, 7) as usize, field(flags, 31, 5)); + if start < FIELDS_START || start as usize * 8 > v.len() || field(flags, 20, 4) >= 4 { + return Err(Error::Corrupt("inode fields start or hash type out of range")); + } + if count > INODE_FIELDS { + return Err(Error::Corrupt("inode with more fields than the format has")); + } + let mut fields = [0u64; INODE_FIELDS]; + let mut at = start as usize * 8; + for (i, field) in fields.iter_mut().enumerate().take(count) { + let (lo, n) = read_varint(&v[at..])?; + at += n; + let width = INODE_WIDTH[i]; + if width == 96 { + let (hi, n) = read_varint(&v[at..])?; + at += n; + if hi != 0 { + return Err(Error::Corrupt("inode time with a high part")); + } + } else if width < 64 && lo >> width != 0 { + return Err(Error::Corrupt("inode field wider than the format allows")); + } + *field = lo; + } + Ok(InodeValue { journal_seq: le64(v, 0), hash_seed: le64(v, 8), flags, sectors: le64(v, 24), size: le64(v, 32), fields }) + } + + /// Every field to the last nonzero one, zero-padded to u64s. + /// `flags`' field count and fields-start are this encoding's. + pub(crate) fn encode(&self) -> Vec { + let count = self.fields.iter().rposition(|&f| f != 0).map_or(0, |i| i + 1); + let flags = self.flags & !(0x7f << 24) & !(0x1f << 31) | (count as u64) << 24 | FIELDS_START << 31; + let mut v = alloc::vec![0u8; 48]; + put64(&mut v, 0, self.journal_seq); + put64(&mut v, 8, self.hash_seed); + put64(&mut v, 16, flags); + put64(&mut v, 24, self.sectors); + put64(&mut v, 32, self.size); + for (i, &f) in self.fields[..count].iter().enumerate() { + varint(&mut v, f); + if INODE_WIDTH[i] == 96 { + varint(&mut v, 0); + } + } + v.resize(v.len().next_multiple_of(8), 0); + v + } +} + +/// A directory entry's type: the target's mode type, or a subvolume. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum DirentType { + Fifo, + Char, + Directory, + Block, + Regular, + Symlink, + Socket, + Subvolume, +} + +impl DirentType { + fn from_bits(bits: u8) -> Option { + Some(match bits { + 1 => DirentType::Fifo, + 2 => DirentType::Char, + 4 => DirentType::Directory, + 6 => DirentType::Block, + 8 => DirentType::Regular, + 10 => DirentType::Symlink, + 12 => DirentType::Socket, + 16 => DirentType::Subvolume, + _ => return None, + }) + } +} + +/// A dirent value, non-casefold layout. +pub(crate) struct Dirent<'a> { + pub target: u64, + pub kind: DirentType, + pub name: &'a [u8], +} + +impl<'a> Dirent<'a> { + pub(crate) fn decode(v: &'a [u8]) -> Result> { + if v.len() < 16 { + return Err(Error::Corrupt("dirent value shorter than 16 bytes")); + } + if v[8] & 0x80 != 0 { + return Err(Error::Unsupported("casefolded dirent")); + } + let kind = DirentType::from_bits(v[8] & 0x1f).ok_or(Error::Corrupt("dirent of no known type"))?; + let name = &v[9..]; + let end = name.iter().rposition(|&b| b != 0).map_or(0, |i| i + 1); + let name = &name[..end]; + if name.is_empty() || name.contains(&0) || name.contains(&b'/') || name == b"." || name == b".." { + return Err(Error::Corrupt("dirent name empty, holding NUL or '/', or a dot entry")); + } + Ok(Dirent { target: le64(v, 0), kind, name }) + } +} + +/// One pointer into the device. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct Ptr { + pub sector: u64, + pub dev: u8, + pub gen: u8, + pub cached: bool, + pub unwritten: bool, +} + +impl Ptr { + fn decode(w: u64) -> Ptr { + Ptr { + cached: w & 2 != 0, + unwritten: w & 8 != 0, + sector: field(w, 4, 44), + dev: field(w, 48, 8) as u8, + gen: field(w, 56, 8) as u8, + } + } + + pub(crate) fn encode(self) -> u64 { + assert!(self.sector >> 44 == 0, "a sector past the pointer's 44 bits"); + 1 | u64::from(self.cached) << 1 | u64::from(self.unwritten) << 3 | self.sector << 4 | u64::from(self.dev) << 48 | u64::from(self.gen) << 56 + } +} + +/// A checksum entry, sizes in sectors. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct Crc { + pub compressed: u32, + pub uncompressed: u32, + pub offset: u32, + pub nonce: u32, + pub csum_type: u8, + pub compression: u8, + /// Its checksum or MAC, as many bytes as the entry holds. + pub csum: [u8; 16], + pub csum_len: usize, +} + +/// An extent value's single pointer and the checksum entry before it. +pub(crate) struct Extent { + pub crc: Option, + pub ptr: Ptr, +} + +impl Extent { + /// The entry list, as far as a one-device volume with no erasure + /// coding can hold. + pub(crate) fn decode(v: &[u8]) -> Result { + let words: Vec = v.as_chunks::<8>().0.iter().map(|w| u64::from_le_bytes(*w)).collect(); + let (mut crc, mut ptr, mut i) = (None, None, 0); + while i < words.len() { + let w = words[i]; + let size = match w.trailing_zeros() { + 0 => { + if ptr.replace(Ptr::decode(w)).is_some() { + return Err(Error::Unsupported("extent with more than one pointer")); + } + 1 + } + 1 => { + crc = Some(once(crc, ptr.is_some(), crc32(w))?); + 1 + } + 2 => { + let w1 = *words.get(i + 1).ok_or(Error::Corrupt("crc64 entry past the extent's end"))?; + crc = Some(once(crc, ptr.is_some(), crc64(w, w1))?); + 2 + } + 3 => { + let rest = words.get(i + 1..i + 3).ok_or(Error::Corrupt("crc128 entry past the extent's end"))?; + crc = Some(once(crc, ptr.is_some(), crc128(w, rest[0], rest[1]))?); + 3 + } + 4 => return Err(Error::Unsupported("erasure-coded extent")), + // Flags, the first of which marks the data lost. + 6 if w >> 7 & 1 != 0 => return Err(Error::Corrupt("extent poisoned")), + // Old rebalance, flags, reconcile and its back-pointer: one + // word each, background work this reader has no part in. + 5..=8 => 1, + _ => return Err(Error::Corrupt("extent entry of no known type")), + }; + i += size; + } + let ptr = ptr.ok_or(Error::Corrupt("extent without a pointer"))?; + Ok(Extent { crc, ptr }) + } +} + +/// The extent's checksum entry, which precedes its one pointer. +fn once(had: Option, after_ptr: bool, crc: Crc) -> Result { + match (had, after_ptr) { + (None, false) => Ok(crc), + (_, true) => Err(Error::Corrupt("checksum entry after the extent's pointer")), + (Some(_), false) => Err(Error::Corrupt("two checksum entries for one pointer")), + } +} + +fn crc32(w: u64) -> Crc { + let mut csum = [0u8; 16]; + csum[..4].copy_from_slice(&((w >> 32) as u32).to_le_bytes()); + Crc { + compressed: field(w, 2, 7) as u32 + 1, + uncompressed: field(w, 9, 7) as u32 + 1, + offset: field(w, 16, 7) as u32, + nonce: 0, + csum_type: field(w, 24, 4) as u8, + compression: field(w, 28, 4) as u8, + csum, + csum_len: 4, + } +} + +fn crc64(w: u64, w1: u64) -> Crc { + let mut csum = [0u8; 16]; + csum[..8].copy_from_slice(&w1.to_le_bytes()); + csum[8..10].copy_from_slice(&((w >> 48) as u16).to_le_bytes()); + Crc { + compressed: field(w, 3, 9) as u32 + 1, + uncompressed: field(w, 12, 9) as u32 + 1, + offset: field(w, 21, 9) as u32, + nonce: field(w, 30, 10) as u32, + csum_type: field(w, 40, 4) as u8, + compression: field(w, 44, 4) as u8, + csum, + csum_len: 10, + } +} + +fn crc128(w: u64, w1: u64, w2: u64) -> Crc { + let mut csum = [0u8; 16]; + csum[..8].copy_from_slice(&w1.to_le_bytes()); + csum[8..].copy_from_slice(&w2.to_le_bytes()); + Crc { + compressed: field(w, 4, 13) as u32 + 1, + uncompressed: field(w, 17, 13) as u32 + 1, + offset: field(w, 30, 13) as u32, + nonce: field(w, 43, 13) as u32, + csum_type: field(w, 56, 4) as u8, + compression: field(w, 60, 4) as u8, + csum, + csum_len: 16, + } +} + +/// A btree_ptr_v2 value: a child node and where it is. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct NodePtr { + pub seq: u64, + /// Sectors of the child that are valid. + pub written: u16, + pub min: Bpos, + pub ptr: Ptr, +} + +impl NodePtr { + pub(crate) const BYTES: usize = 48; + + pub(crate) fn decode(v: &[u8]) -> Result { + if v.len() < Self::BYTES { + return Err(Error::Corrupt("btree pointer without a device pointer")); + } + if v.len() > Self::BYTES { + return Err(Error::Unsupported("btree pointer with more than one device pointer")); + } + let ptr = le64(v, 40); + if ptr & 1 == 0 { + return Err(Error::Corrupt("btree pointer whose entry is no pointer")); + } + let ptr = Ptr::decode(ptr); + if le16(v, 16) == 0 || ptr.cached || ptr.unwritten { + return Err(Error::Corrupt("btree pointer with no written sectors, or cached or unwritten")); + } + Ok(NodePtr { seq: le64(v, 8), written: le16(v, 16), min: Bpos::decode(&v[20..40]), ptr }) + } + + pub(crate) fn encode(&self) -> [u8; Self::BYTES] { + let mut v = [0u8; Self::BYTES]; + put64(&mut v, 8, self.seq); + put16(&mut v, 16, self.written); + self.min.encode(&mut v[20..40]); + put64(&mut v, 40, self.ptr.encode()); + v + } +} + +/// An alloc_v4 value as a v1 writer sets it: no stripes, no +/// discard, generation 0. +pub(crate) struct Alloc { + pub data_type: u8, + pub dirty_sectors: u32, +} + +/// Data types. +pub(crate) mod data_type { + pub const FREE: u8 = 0; + pub const SB: u8 = 1; + pub const JOURNAL: u8 = 2; + pub const BTREE: u8 = 3; +} + +impl Alloc { + pub(crate) fn encode(&self) -> [u8; 64] { + let mut v = [0u8; 64]; + // need_discard and needs-gen-increment set, trailing records at 8. + put32(&mut v, 8, 0x23); + v[14] = self.data_type; + put32(&mut v, 16, self.dirty_sectors); + // The IO clock when the bucket filled, never below 1. + put64(&mut v, 24, 1); + put64(&mut v, 32, 1); + v + } +} + +/// A backpointer value for a btree node pointer. +pub(crate) fn backpointer(btree: u8, node_level: u8, sectors: u32, node_max: Bpos) -> [u8; 32] { + let mut v = [0u8; 32]; + v[0] = btree; + v[1] = node_level + 1; + v[2] = data_type::BTREE; + put32(&mut v, 8, sectors); + node_max.encode(&mut v[12..32]); + v +} + +/// A live snapshot-tree root, with its subvolume. +pub(crate) fn snapshot(subvolume: u32, tree: u32) -> [u8; 64] { + let mut v = [0u8; 64]; + put32(&mut v, 0, SNAPSHOT_HAS_SUBVOLUME); + put32(&mut v, 16, subvolume); + put32(&mut v, 20, tree); + put32(&mut v, 56, SNAPSHOT_LIVE); + v +} + +pub(crate) const SNAPSHOT_HAS_SUBVOLUME: u32 = 1 << 1; +pub(crate) const SNAPSHOT_LIVE: u32 = 0x757c_47a2; +pub(crate) const SUBVOLUME_LIVE: u32 = 0x4ad5_447e; + +/// A live subvolume rooted at `inode` in `snapshot`. +pub(crate) fn subvolume(snapshot: u32, inode: u64) -> [u8; 48] { + let mut v = [0u8; 48]; + put32(&mut v, 4, snapshot); + put64(&mut v, 8, inode); + put32(&mut v, 40, SUBVOLUME_LIVE); + v +} + +/// A snapshot tree: its master subvolume and root snapshot. +pub(crate) fn snapshot_tree(master_subvolume: u32, root_snapshot: u32) -> [u8; 8] { + let mut v = [0u8; 8]; + put32(&mut v, 0, master_subvolume); + put32(&mut v, 4, root_snapshot); + v +} + +/// An unsharded inode-allocation cursor: its generation and next number. +pub(crate) fn inode_cursor(generation: u32, next: u64) -> [u8; 16] { + let mut v = [0u8; 16]; + put32(&mut v, 4, generation); + put64(&mut v, 8, next); + v +} + +/// The fields of a subvolume a check compares. +pub(crate) struct Subvolume { + pub snapshot: u32, + pub inode: u64, + pub state: u32, + pub fs_path_parent: u32, +} + +impl Subvolume { + pub(crate) fn decode(v: &[u8]) -> Result { + if v.len() < 48 { + return Err(Error::Corrupt("subvolume value shorter than 48 bytes")); + } + Ok(Subvolume { snapshot: le32(v, 4), inode: le64(v, 8), fs_path_parent: le32(v, 20), state: le32(v, 40) }) + } +} + +/// An accounting key's position: its class record read as one +/// big-endian 160-bit integer. +pub(crate) fn accounting_pos(record: &[u8]) -> Bpos { + let mut r = [0u8; 20]; + r[..record.len()].copy_from_slice(record); + r.reverse(); + Bpos::decode(&r) +} + +/// Accounting classes. +pub(crate) mod class { + pub const NR_INODES: u8 = 0; + pub const REPLICAS: u8 = 2; + pub const DEV_DATA_TYPE: u8 = 3; + pub const SNAPSHOT: u8 = 5; + pub const BTREE: u8 = 6; +} + +/// The counters of an accounting value. +pub(crate) fn counters(c: &[u64]) -> Vec { + c.iter().flat_map(|c| c.to_le_bytes()).collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn varints_take_seven_bits_a_byte_up_to_eight_bytes() { + for (v, len) in [(0u64, 1), (1, 1), (127, 1), (128, 2), ((1 << 56) - 1, 8), (1 << 56, 9), (u64::MAX, 9)] { + let mut b = Vec::new(); + varint(&mut b, v); + assert_eq!(b.len(), len, "{v}"); + assert_eq!(read_varint(&b), Ok((v, len)), "{v}"); + } + let mut b = Vec::new(); + varint(&mut b, 1); + assert_eq!(b, [0x02], "one byte, low bit 0, the value above it"); + assert!(read_varint(&[0x01]).is_err(), "a two-byte varint cut short"); + } + + #[test] + fn an_inode_value_encodes_to_its_last_nonzero_field() { + let mut inode = InodeValue { hash_seed: 7, flags: 0o40755 << 36 | 3 << 20, ..Default::default() }; + inode.set(inode_field::SUBVOL, 1); + let v = inode.encode(); + // 48 fixed bytes; four zero times of two bytes, 17 zero fields, then + // the subvolume id: 26 bytes padded to 32. + assert_eq!(v.len(), 80); + let read = InodeValue::decode(&v).unwrap(); + assert_eq!(field(read.flags, 24, 7), 22, "the subvolume id is field 22"); + assert_eq!((read.mode(), read.hash_type(), read.field(inode_field::SUBVOL)), (0o40755, 3, 1)); + assert_eq!(read.fields, inode.fields); + } + + #[test] + fn a_crc64_entry_holds_an_80_bit_mac() { + let w = 0b100 | 31 << 3 | 31 << 12 | 3 << 40 | 0xbeef << 48; + let crc = crc64(w, 0x0706_0504_0302_0100); + assert_eq!((crc.compressed, crc.uncompressed, crc.csum_type, crc.csum_len), (32, 32, 3, 10)); + assert_eq!(&crc.csum[..10], &[0, 1, 2, 3, 4, 5, 6, 7, 0xef, 0xbe]); + } + + #[test] + fn an_extent_is_its_checksum_entry_then_its_pointer() { + let ptr = Ptr { sector: 5248, dev: 0, gen: 0, cached: false, unwritten: false }.encode(); + let crc = 0b10 | 31 << 2 | 31 << 9 | 5 << 24 | 0xdead_beef << 32; + let value = [crc, ptr].map(u64::to_le_bytes).concat(); + let extent = Extent::decode(&value).unwrap(); + assert_eq!((extent.ptr.sector, extent.crc.unwrap().csum_type, extent.crc.unwrap().compressed), (5248, 5, 32)); + let after = [ptr, crc].map(u64::to_le_bytes).concat(); + assert!(Extent::decode(&after).is_err(), "a checksum entry after the pointer"); + let poisoned = [0b100_0000 | 1 << 7, crc, ptr].map(u64::to_le_bytes).concat(); + assert_eq!(Extent::decode(&poisoned).err(), Some(Error::Corrupt("extent poisoned"))); + let flags = [0b100_0000, crc, ptr].map(u64::to_le_bytes).concat(); + assert!(Extent::decode(&flags).is_ok(), "a flags entry without the poison bit"); + } + + #[test] + fn an_accounting_record_is_its_position_read_big_endian() { + // Replicas (btree, 1 device, 1 required, device 0). + assert_eq!(accounting_pos(&[2, 3, 1, 1, 0]), Bpos::new(0x0203_0101_0000_0000, 0, 0)); + // inum 0x80000003. + assert_eq!(accounting_pos(&[8, 3, 0, 0, 0x80]), Bpos::new(0x0803_0000_8000_0000, 0, 0)); + } + + #[test] + fn a_node_pointer_reads_back() { + let ptr = NodePtr { seq: 0x1234, written: 8, min: Bpos::new(0, 83, 0), ptr: Ptr { sector: 4640, dev: 0, gen: 1, cached: false, unwritten: false } }; + assert_eq!(NodePtr::decode(&ptr.encode()), Ok(ptr)); + } +} diff --git a/lib/bcachefs/src/lib.rs b/lib/bcachefs/src/lib.rs index 64afa13218a..becd6579e16 100644 --- a/lib/bcachefs/src/lib.rs +++ b/lib/bcachefs/src/lib.rs @@ -1,6 +1,6 @@ -//! The `/home` filesystem. [`upstream`] implements the real bcachefs on-disk -//! format, read side; the modules beside it are the interim ToyOS format the -//! kernel still links. +//! bcachefs. [`engine`] implements its on-disk format; the modules beside it +//! are the interim ToyOS format the build, the loader and the file server +//! still use. //! //! **Every number this crate parses came off a disk it does not own, and a //! CRC is not authentication — whoever writes the image writes the CRC.** Each @@ -21,7 +21,8 @@ mod superblock; mod alloc_bitmap; mod btree; mod fs; -pub mod upstream; +#[warn(dead_code)] +pub mod engine; pub use block_io::{BlockIO, BlockBuf, BlockNum, DeviceError, TransferError}; #[cfg(feature = "std")] diff --git a/lib/bcachefs/src/upstream/bkey.rs b/lib/bcachefs/src/upstream/bkey.rs deleted file mode 100644 index c68cd6299a2..00000000000 --- a/lib/bcachefs/src/upstream/bkey.rs +++ /dev/null @@ -1,506 +0,0 @@ -//! Keys: the position they sort by, the per-node packing, and unpacking. -//! -//! A packed key's first `key_u64s` words, read little-endian, are one integer. -//! Its three header bytes are the low 24 bits; the node's `bkey_format` lays -//! the six fields end to end below the integer's top bit, `INODE` highest and -//! `VERSION_LO` lowest, each `bits_per_field` wide, and each unpacks to its -//! packed value plus its `field_offset`. The bits between the header and the -//! lowest field mean nothing, and a field crosses word boundaries freely. - -use super::raw::Raw; -use super::UpstreamError; - -/// `BKEY_U64s`: the unpacked key is five u64s, and its value follows them. -pub const BKEY_U64S: usize = 5; -pub const BKEY_BYTES: usize = BKEY_U64S * 8; -/// `KEY_PACKED_BITS_START`: the three header bytes a packed key keeps -/// unpacked, below every field bit. -const PACKED_BITS_START: u32 = 24; -const NR_FIELDS: usize = 6; - -pub const KEY_FORMAT_LOCAL_BTREE: u8 = 0; -pub const KEY_FORMAT_CURRENT: u8 = 1; - -/// Value types this reader decodes or steps over, `BCH_BKEY_TYPES()`. -pub const TYPE_DELETED: u8 = 0; -pub const TYPE_WHITEOUT: u8 = 1; -pub const TYPE_ERROR: u8 = 2; -pub const TYPE_HASH_WHITEOUT: u8 = 4; -pub const TYPE_EXTENT: u8 = 6; -pub const TYPE_RESERVATION: u8 = 7; -pub const TYPE_DIRENT: u8 = 10; -pub const TYPE_INLINE_DATA: u8 = 17; -pub const TYPE_BTREE_PTR_V2: u8 = 18; -pub const TYPE_SUBVOLUME: u8 = 21; -pub const TYPE_INODE_V3: u8 = 29; -pub const TYPE_EXTENT_WHITEOUT: u8 = 36; - -/// A key's position: the btree's sort order, low to high, is exactly this -/// tuple's. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)] -pub struct Bpos { - pub inode: u64, - pub offset: u64, - pub snapshot: u32, -} - -impl Bpos { - pub const MIN: Self = Self { inode: 0, offset: 0, snapshot: 0 }; - - pub fn new(inode: u64, offset: u64, snapshot: u32) -> Self { - Self { inode, offset, snapshot } - } - - /// Read the on-disk `struct bpos` at `off`; its words are stored low to - /// high, which is the reverse of how they compare. - pub fn read(raw: &Raw<'_>, off: usize) -> Result { - Ok(Self { - snapshot: raw.u32(off)?, - offset: raw.u64(off + 4)?, - inode: raw.u64(off + 12)?, - }) - } -} - -pub const BPOS_BYTES: usize = 20; - -/// One field of a `bkey_format`: its packed width, and what unpacking adds to it. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct FieldPacking { - bits: u8, - offset: u64, -} - -impl FieldPacking { - /// A field packed at its unpacked width, with nothing added back. - const fn whole(bits: u8) -> Self { - Self { bits, offset: 0 } - } - - /// Whether every value this field can unpack to, `offset + 2^bits - 1` at - /// the most, fits in the unpacked field it fills. - fn fits(self, unpacked: Unpacked) -> bool { - // A span past `u128` is past any unpacked field. - let top = 2u128 - .checked_pow(self.bits.into()) - .and_then(|span| span.checked_add(self.offset.into())); - top.is_some_and(|top| top <= 2u128.pow(unpacked as u32)) - } -} - -/// The width of a member of the unpacked `struct bkey`. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum Unpacked { - U32 = 32, - U64 = 64, -} - -/// A btree node's key format: how many bits each field is packed into, and -/// what is added back to each on the way out. -/// -/// **Holding one is proof it is valid**: the only way to make one from disk -/// bytes is `TryFrom`, so unpacking never meets a field wider -/// than the one it fills or a key length its fields do not add up to. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct BkeyFormat { - key_u64s: u8, - fields: [FieldPacking; NR_FIELDS], -} - -pub const FORMAT_BYTES: usize = 56; - -/// `BKEY_FORMAT_CURRENT`'s widths: what each packed field is unpacked into. -const UNPACKED_BITS: [Unpacked; NR_FIELDS] = - [Unpacked::U64, Unpacked::U64, Unpacked::U32, Unpacked::U32, Unpacked::U32, Unpacked::U64]; - -/// A `struct bkey_format` exactly as stored, before anything in it is believed. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct StoredFormat { - key_u64s: u8, - nr_fields: u8, - fields: [FieldPacking; NR_FIELDS], -} - -impl StoredFormat { - fn parse(bytes: &[u8; FORMAT_BYTES]) -> Self { - let [key_u64s, nr_fields, b0, b1, b2, b3, b4, b5, offsets @ ..] = *bytes; - let bits = [b0, b1, b2, b3, b4, b5]; - // 48 bytes are exactly one word per field. - let offsets = offsets.as_chunks::<8>().0; - let fields = core::array::from_fn(|j| FieldPacking { bits: bits[j], offset: u64::from_le_bytes(offsets[j]) }); - Self { key_u64s, nr_fields, fields } - } -} - -/// Each way a stored `bkey_format` describes keys this reader cannot unpack. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FormatError { - /// `nr_fields` is not `BKEY_NR_FIELDS`. - FieldCount, - /// A field's largest value, `field_offset + 2^bits_per_field - 1`, is past - /// what the unpacked field holds. - FieldTooWide, - /// `key_u64s` is not the words the header and the fields' bits fill. - WordCount, -} - -impl From for UpstreamError { - fn from(err: FormatError) -> Self { - UpstreamError::Refused(match err { - FormatError::FieldCount => "btree node's key format has the wrong field count", - FormatError::FieldTooWide => "btree node's key format packs a field wider than the one it fills", - FormatError::WordCount => "btree node's key format is not as many words as its fields need", - }) - } -} - -impl TryFrom for BkeyFormat { - type Error = FormatError; - - /// `key_u64s` is required to be exactly the words the fields need, so a - /// format cannot carry slack a key could hide in. - fn try_from(stored: StoredFormat) -> Result { - if usize::from(stored.nr_fields) != NR_FIELDS { - return Err(FormatError::FieldCount); - } - if !stored.fields.iter().zip(UNPACKED_BITS).all(|(field, unpacked)| field.fits(unpacked)) { - return Err(FormatError::FieldTooWide); - } - let field_bits: u32 = stored.fields.iter().map(|field| u32::from(field.bits)).sum(); - if u32::from(stored.key_u64s) != (PACKED_BITS_START + field_bits).div_ceil(64) { - return Err(FormatError::WordCount); - } - Ok(Self { key_u64s: stored.key_u64s, fields: stored.fields }) - } -} - -impl BkeyFormat { - /// `BKEY_FORMAT_CURRENT`: what a key outside any btree node — a journal - /// entry's, a clean section's — is written in. - pub fn unpacked() -> Self { - Self { key_u64s: BKEY_U64S as u8, fields: UNPACKED_BITS.map(|width| FieldPacking::whole(width as u8)) } - } - - /// Read the `bkey_format` stored at `off`, refusing one whose keys could - /// not be unpacked into `struct bkey`. - pub fn read(raw: &Raw<'_>, off: usize) -> Result { - let bytes = raw.slice(off, FORMAT_BYTES)?.try_into().expect("a FORMAT_BYTES window"); - Ok(Self::try_from(StoredFormat::parse(bytes))?) - } -} - -/// A key, unpacked, and where its value sits inside the same window. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Key { - /// The key and value together, in u64s: how far the next key is. - pub u64s: u8, - pub kind: u8, - /// Extent size in sectors; zero for every non-extent key. - pub size: u32, - pub pos: Bpos, - /// Byte offset of the value from the start of this key. - pub val_at: usize, - /// Where this key starts inside the node it was read from. - pub base: usize, -} - -impl Key { - /// The value's length in bytes. - pub fn val_bytes(&self) -> usize { - self.u64s as usize * 8 - self.val_at - } - - /// Record where in the node this key was found, so its value can be located. - pub fn with_base(mut self, base: usize) -> Self { - self.base = base; - self - } - - /// Decode the key at the start of `raw`, unpacking it through `format` - /// when the key says it is packed. - pub fn read(raw: &Raw<'_>, format: &BkeyFormat) -> Result { - let u64s = raw.u8(0)?; - let fmt_byte = raw.u8(1)?; - let kind = raw.u8(2)?; - let packed_format = fmt_byte & 0x7F; - - let key_u64s = match packed_format { - KEY_FORMAT_LOCAL_BTREE => usize::from(format.key_u64s), - KEY_FORMAT_CURRENT => BKEY_U64S, - _ => return Err(UpstreamError::Refused("key names a format the node does not define")), - }; - if (u64s as usize) < key_u64s { - return Err(UpstreamError::Refused("key is shorter than the format it names")); - } - // Every byte the key claims has to be there before any field is read. - let whole = raw.sub(0, u64s as usize * 8, "key runs past the end of its bset")?; - - let (size, pos) = if packed_format == KEY_FORMAT_CURRENT { - (whole.u32(16)?, Bpos::read(&whole, 20)?) - } else { - PackedKey::new(&whole, format)?.unpack()? - }; - - Ok(Self { u64s, kind, size, pos, val_at: key_u64s * 8, base: 0 }) - } -} - -/// A key packed in a node's format: its first `key_u64s` words, header -/// included, read little-endian as one integer. -struct PackedKey<'a> { - bytes: &'a [u8], - format: &'a BkeyFormat, -} - -const FIELD_DOES_NOT_FIT: UpstreamError = - UpstreamError::Refused("packed key's field does not fit the one it unpacks into"); - -impl<'a> PackedKey<'a> { - /// The key part of `whole`; the value past it is not the key's. - fn new(whole: &Raw<'a>, format: &'a BkeyFormat) -> Result { - let bytes = whole.slice(0, usize::from(format.key_u64s) * 8)?; - Ok(Self { bytes, format }) - } - - /// Bits `[lo, lo + width)` of the key as an integer, for `width <= 64`. - fn bit_range(&self, lo: u16, width: u8) -> u128 { - // Nine bytes from the one holding bit `lo` cover any 64-bit field. - let mut window = [0u8; 16]; - for (dst, src) in window.iter_mut().zip(self.bytes.iter().skip(usize::from(lo / 8))) { - *dst = *src; - } - (u128::from_le_bytes(window) >> (lo % 8)) & ((1u128 << width) - 1) - } - - /// `(size, pos)`: every field's packed value plus its `field_offset`. - fn unpack(&self) -> Result<(u32, Bpos), UpstreamError> { - // The format's validity puts the lowest field at bit 24 or above, so - // `top` never passes below the header. - let mut top = u16::from(self.format.key_u64s) * 64; - let [inode, offset, snapshot, size, _version_hi, _version_lo] = - self.format.fields.map(|field| { - top -= u16::from(field.bits); - self.bit_range(top, field.bits) + u128::from(field.offset) - }); - let wide = |value: u128| u64::try_from(value).map_err(|_| FIELD_DOES_NOT_FIT); - let narrow = |value: u128| u32::try_from(value).map_err(|_| FIELD_DOES_NOT_FIT); - Ok((narrow(size)?, Bpos { inode: wide(inode)?, offset: wide(offset)?, snapshot: narrow(snapshot)? })) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use alloc::vec; - use alloc::vec::Vec; - - const FIELD_INODE: usize = 0; - const FIELD_SNAPSHOT: usize = 2; - const FIELD_SIZE: usize = 3; - - fn format_bytes(key_u64s: u8, bits: [u8; 6], offsets: [u64; 6]) -> Vec { - let mut out = vec![key_u64s, 6]; - out.extend_from_slice(&bits); - for off in offsets { - out.extend_from_slice(&off.to_le_bytes()); - } - out - } - - /// The format `BKEY_FORMAT_CURRENT` is: every field at its natural width, - /// no offsets. A key packed in it unpacks to what an unpacked key holds: - /// its fields land on exactly the bytes of `struct bkey`. - #[test] - fn the_identity_format_round_trips_a_position() { - let bits = [64u8, 64, 32, 32, 32, 64]; - let raw = format_bytes(BKEY_U64S as u8, bits, [0; 6]); - let format = BkeyFormat::read(&Raw::new(&raw, "format"), 0).expect("a valid format"); - - // Pack (inode, offset, snapshot, size, version) most significant bit - // first, from the top of the last word down. - let want = Bpos::new(0x1122_3344_5566_7788, 0x99AA_BBCC_DDEE_FF00, 0x1234_5678); - let size = 0x0BAD_F00Du32; - let mut bitstring: Vec = Vec::new(); - let mut push = |v: u64, n: u32| { - for i in (0..n).rev() { - bitstring.push((v >> i) & 1 == 1); - } - }; - push(want.inode, 64); - push(want.offset, 64); - push(want.snapshot as u64, 32); - push(size as u64, 32); - push(0, 32); - push(0, 64); - - let mut words = [0u64; BKEY_U64S]; - let mut at = 0; - for word in (0..BKEY_U64S).rev() { - for bit in (0..64).rev() { - if at < bitstring.len() && bitstring[at] { - words[word] |= 1u64 << bit; - } - at += 1; - } - } - - let mut key = Vec::new(); - for word in words { - key.extend_from_slice(&word.to_le_bytes()); - } - key[0] = BKEY_U64S as u8; - key[1] = KEY_FORMAT_LOCAL_BTREE; - key[2] = TYPE_DIRENT; - - let decoded = Key::read(&Raw::new(&key, "key"), &format).expect("a packed key"); - assert_eq!(decoded.pos, want); - assert_eq!(decoded.size, size); - assert_eq!(decoded.kind, TYPE_DIRENT); - assert_eq!(decoded.val_at, BKEY_BYTES); - } - - #[test] - fn a_field_wider_than_the_one_it_fills_is_refused() { - for (field, bits) in [(FIELD_SNAPSHOT, 64u8), (FIELD_SIZE, 33), (FIELD_INODE, 65)] { - let mut widths = [64u8, 64, 32, 32, 32, 64]; - widths[field] = bits; - let total: u32 = PACKED_BITS_START + widths.iter().map(|b| *b as u32).sum::(); - let raw = format_bytes(total.div_ceil(64) as u8, widths, [0; 6]); - assert_eq!( - BkeyFormat::read(&Raw::new(&raw, "format"), 0).err(), - Some(UpstreamError::Refused( - "btree node's key format packs a field wider than the one it fills" - )), - "a {bits}-bit field {field} was accepted" - ); - } - - // A field at its full width may carry no offset, or the two together - // exceed what the unpacked field holds. - let mut offsets = [0u64; 6]; - offsets[FIELD_SNAPSHOT] = 1; - let full = format_bytes(BKEY_U64S as u8, [64, 64, 32, 32, 32, 64], offsets); - assert!(BkeyFormat::read(&Raw::new(&full, "format"), 0).is_err()); - - // A narrower field whose offset pushes its top past the unpacked max. - let mut offsets = [0u64; 6]; - offsets[FIELD_SNAPSHOT] = u32::MAX as u64; - let pushed = format_bytes(5, [64, 64, 31, 32, 32, 64], offsets); - assert!(BkeyFormat::read(&Raw::new(&pushed, "format"), 0).is_err()); - } - - /// A key whose `u64s` is below its format's key length, or past the end of - /// the window, is refused rather than read short. - #[test] - fn a_key_that_does_not_fit_is_refused() { - let raw = format_bytes(BKEY_U64S as u8, [64, 64, 32, 32, 32, 64], [0; 6]); - let format = BkeyFormat::read(&Raw::new(&raw, "format"), 0).expect("a valid format"); - - let mut short = vec![0u8; BKEY_BYTES]; - short[0] = 4; - short[1] = KEY_FORMAT_LOCAL_BTREE; - assert!(Key::read(&Raw::new(&short, "key"), &format).is_err()); - - let mut past = vec![0u8; BKEY_BYTES]; - past[0] = 200; - past[1] = KEY_FORMAT_LOCAL_BTREE; - assert!(Key::read(&Raw::new(&past, "key"), &format).is_err()); - - let mut alien = vec![0u8; BKEY_BYTES]; - alien[0] = BKEY_U64S as u8; - alien[1] = 42; - assert!(Key::read(&Raw::new(&alien, "key"), &format).is_err()); - } - - mod vectors { - /// `(inode, offset, snapshot, size)`. - type Unpacked = (u64, u64, u32, u32); - include!("bkey_vectors.rs"); - } - - fn stored(bytes: &[u8]) -> StoredFormat { - StoredFormat::parse(bytes.first_chunk().expect("a whole format")) - } - - /// Every format vector: valid ones read back unchanged, and each invalid - /// one is refused for the first condition it fails. - #[test] - fn format_vectors() { - for &(id, bytes, want) in vectors::FORMAT_CASES { - let got = BkeyFormat::read(&Raw::new(bytes, "format"), 0); - match want { - None => { - let format = got.unwrap_or_else(|err| panic!("{id}: refused a valid format: {err:?}")); - let stored = stored(bytes); - assert_eq!((format.key_u64s, format.fields), (stored.key_u64s, stored.fields), "{id}"); - } - Some(refusal) => assert_eq!(got, Err(UpstreamError::Refused(refusal)), "{id}"), - } - } - } - - /// A format one byte short of its 56 is refused, not read past. - #[test] - fn a_truncated_format_is_refused() { - let (_, bytes, _) = vectors::FORMAT_CASES[0]; - assert!(BkeyFormat::read(&Raw::new(&bytes[..FORMAT_BYTES - 1], "format"), 0).is_err()); - assert!(BkeyFormat::read(&Raw::new(bytes, "format"), 1).is_err()); - } - - /// `field_offset + 2^bits <= 2^unpacked`, decided over the integers for - /// every width a byte can state. - #[test] - fn field_fit_vectors() { - for &(bits, offset, unpacked, overflows) in vectors::OVERFLOW_CASES { - let width = match unpacked { - 32 => Unpacked::U32, - 64 => Unpacked::U64, - other => panic!("no unpacked field is {other} bits wide"), - }; - assert_eq!( - FieldPacking { bits, offset }.fits(width), - !overflows, - "{bits} bits at offset {offset:#x} into {unpacked}" - ); - } - } - - #[test] - fn unpack_vectors() { - for &(id, format, key, (inode, offset, snapshot, size)) in vectors::UNPACK_CASES { - let format = BkeyFormat::read(&Raw::new(format, "format"), 0).expect(id); - let got = PackedKey::new(&Raw::new(key, "key"), &format).and_then(|key| key.unpack()); - assert_eq!(got, Ok((size, Bpos::new(inode, offset, snapshot))), "{id}"); - } - } - - /// A field whose value is past its unpacked width is refused, not - /// truncated. No format `read` accepts reaches this, so the format is - /// built past its constructor. - #[test] - fn a_field_past_its_unpacked_width_is_refused_not_truncated() { - let narrow = FieldPacking { bits: 0, offset: u64::from(u32::MAX) + 1 }; - // The key's top bit packs a 1, which lands one past `u64::MAX`. - let wide = FieldPacking { bits: 1, offset: u64::MAX }; - let none = FieldPacking::whole(0); - for fields in [ - [wide, none, none, none, none, none], - [none, wide, none, none, none, none], - [none, none, narrow, none, none, none], - [none, none, none, narrow, none, none], - ] { - let format = BkeyFormat { key_u64s: 1, fields }; - let key = [1, KEY_FORMAT_LOCAL_BTREE, TYPE_DIRENT, 0, 0, 0, 0, 0x80]; - let got = PackedKey::new(&Raw::new(&key, "key"), &format).and_then(|key| key.unpack()); - assert_eq!(got, Err(FIELD_DOES_NOT_FIT), "{fields:?}"); - } - } - - /// The identity format read off a disk is the one `unpacked` states. - #[test] - fn the_identity_format_is_the_unpacked_one() { - let (id, bytes, _) = vectors::FORMAT_CASES[0]; - assert_eq!(id, "F1"); - assert_eq!(BkeyFormat::read(&Raw::new(bytes, "format"), 0), Ok(BkeyFormat::unpacked())); - } -} diff --git a/lib/bcachefs/src/upstream/bkey_vectors.rs b/lib/bcachefs/src/upstream/bkey_vectors.rs deleted file mode 100644 index 4e341cca94b..00000000000 --- a/lib/bcachefs/src/upstream/bkey_vectors.rs +++ /dev/null @@ -1,80 +0,0 @@ -pub const FORMAT_CASES: &[(&str, &[u8], Option<&str>)] = &[ - ("F1", &[0x05,0x06,0x40,0x40,0x20,0x20,0x20,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], None), - ("F2", &[0x03,0x06,0x40,0x40,0x20,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], None), - ("F3", &[0x01,0x06,0x00,0x08,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], None), - ("F4", &[0x04,0x06,0x40,0x40,0x20,0x00,0x00,0x28,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x05,0x05,0x00,0x00,0x06,0x00,0x00,0x00], None), - ("F5", &[0x01,0x06,0x00,0x10,0x08,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x52,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], None), - ("F6", &[0x04,0x06,0x40,0x40,0x20,0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], None), - ("F7", &[0x01,0x06,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], None), - ("F8", &[0x01,0x06,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff], None), - ("F9", &[0x00,0x06,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], Some("btree node's key format is not as many words as its fields need")), - ("F10", &[0x09,0x06,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], Some("btree node's key format is not as many words as its fields need")), - ("F11", &[0x05,0x05,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], Some("btree node's key format has the wrong field count")), - ("F12", &[0x05,0x07,0x40,0x40,0x20,0x20,0x20,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], Some("btree node's key format has the wrong field count")), - ("F13", &[0x06,0x06,0x40,0x40,0x40,0x20,0x20,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], Some("btree node's key format packs a field wider than the one it fills")), - ("F14", &[0x05,0x06,0x40,0x40,0x20,0x21,0x20,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], Some("btree node's key format packs a field wider than the one it fills")), - ("F15", &[0x05,0x06,0x41,0x40,0x20,0x20,0x20,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], Some("btree node's key format packs a field wider than the one it fills")), - ("F16", &[0x05,0x06,0x00,0x00,0x00,0x00,0x00,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], Some("btree node's key format packs a field wider than the one it fills")), - ("F17", &[0x05,0x06,0x40,0x40,0x20,0x20,0x20,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], Some("btree node's key format packs a field wider than the one it fills")), - ("F18", &[0x05,0x06,0x40,0x40,0x20,0x20,0x20,0x40,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], Some("btree node's key format packs a field wider than the one it fills")), - ("F19", &[0x05,0x06,0x40,0x40,0x1f,0x20,0x20,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], None), - ("F20", &[0x05,0x06,0x40,0x40,0x1f,0x20,0x20,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], Some("btree node's key format packs a field wider than the one it fills")), - ("F21", &[0x05,0x06,0x40,0x40,0x1f,0x20,0x20,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], Some("btree node's key format packs a field wider than the one it fills")), - ("F22", &[0x01,0x06,0x01,0x00,0x00,0x00,0x00,0x00,0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], None), - ("F23", &[0x01,0x06,0x01,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], Some("btree node's key format packs a field wider than the one it fills")), - ("F24", &[0x01,0x06,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], None), - ("F25", &[0x01,0x06,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], Some("btree node's key format packs a field wider than the one it fills")), - ("F26", &[0x01,0x06,0x00,0x00,0x08,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], Some("btree node's key format packs a field wider than the one it fills")), - ("F27", &[0x01,0x06,0x40,0x40,0x20,0x20,0x20,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], Some("btree node's key format is not as many words as its fields need")), - ("F28", &[0x04,0x06,0x40,0x40,0x20,0x20,0x20,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], Some("btree node's key format is not as many words as its fields need")), - ("F29", &[0x06,0x06,0x40,0x40,0x20,0x20,0x20,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], Some("btree node's key format is not as many words as its fields need")), - ("F30", &[0x01,0x06,0x28,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], None), - ("F31", &[0x02,0x06,0x28,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], Some("btree node's key format is not as many words as its fields need")), - ("F32", &[0x01,0x06,0x29,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], Some("btree node's key format is not as many words as its fields need")), - ("F33", &[0x02,0x06,0x29,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], None), - ("F34", &[0x01,0x05,0x41,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], Some("btree node's key format has the wrong field count")), - ("F35", &[0x05,0x06,0x41,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], Some("btree node's key format packs a field wider than the one it fills")), - ("F36", &[0x03,0x06,0x28,0x28,0x20,0x20,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], None), - ("F37", &[0x02,0x06,0x08,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], None), - ("F38", &[0x01,0x06,0x08,0x08,0x00,0x00,0x00,0x00,0xe8,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0xd0,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0xb8,0x0b,0x00,0x00,0x00,0x00,0x00,0x00,0xa0,0x0f,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], None), -]; -pub const OVERFLOW_CASES: &[(u8, u64, u8, bool)] = &[ - (0, 0x0, 64, false), - (0, 0xffffffffffffffff, 64, false), - (1, 0xffffffffffffffff, 64, true), - (1, 0xfffffffffffffffe, 64, false), - (64, 0x0, 64, false), - (64, 0x1, 64, true), - (63, 0x8000000000000000, 64, false), - (63, 0x8000000000000001, 64, true), - (65, 0x0, 64, true), - (255, 0x0, 64, true), - (0, 0x0, 32, false), - (0, 0xffffffff, 32, false), - (0, 0x100000000, 32, true), - (32, 0x0, 32, false), - (32, 0x1, 32, true), - (31, 0x80000000, 32, false), - (31, 0x80000001, 32, true), - (33, 0x0, 32, true), - (8, 0xffffffffffffffff, 32, true), - (1, 0xffffffff, 32, true), - (16, 0xffff0000, 32, false), - (16, 0xffff0001, 32, true), -]; -pub const UNPACK_CASES: &[(&str, &[u8], &[u8], Unpacked)] = &[ - ("U1", &[0x05,0x06,0x40,0x40,0x20,0x20,0x20,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], &[0x05,0x00,0x0a,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0d,0xf0,0xad,0x0b,0x78,0x56,0x34,0x12,0x00,0xff,0xee,0xdd,0xcc,0xbb,0xaa,0x99,0x88,0x77,0x66,0x55,0x44,0x33,0x22,0x11], (0x1122334455667788, 0x99aabbccddeeff00, 0x12345678, 0xbadf00d)), - ("U2", &[0x01,0x06,0x08,0x08,0x00,0x00,0x00,0x00,0xe8,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0xd0,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0xb8,0x0b,0x00,0x00,0x00,0x00,0x00,0x00,0xa0,0x0f,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], &[0x01,0x00,0x1d,0x00,0x00,0x00,0x09,0x07], (0x3ef, 0x7d9, 0xbb8, 0xfa0)), - ("U3", &[0x03,0x06,0x40,0x40,0x20,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], &[0x06,0x00,0x0a,0x00,0xff,0xff,0xff,0xff,0x5f,0x0f,0x35,0xc4,0xb6,0xc0,0xbd,0x18,0x00,0x10,0x00,0x00,0x00,0x00,0x00,0x00], (0x1000, 0x18bdc0b6c4350f5f, 0xffffffff, 0x0)), - ("U4", &[0x03,0x06,0x40,0x40,0x20,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], &[0x09,0x00,0x12,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff], (0xffffffffffffffff, 0xffffffffffffffff, 0xffffffff, 0x0)), - ("U5", &[0x03,0x06,0x28,0x28,0x20,0x20,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], &[0x03,0x00,0x0a,0x00,0x00,0x00,0xef,0xbe,0xad,0xde,0xef,0xcd,0xab,0x89,0xb4,0xc3,0xd2,0xe1,0xf0,0xe5,0xd4,0xc3,0xb2,0xa1], (0xa1b2c3d4e5, 0xf0e1d2c3b4, 0x89abcdef, 0xdeadbeef)), - ("U6", &[0x02,0x06,0x08,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], &[0x02,0x00,0x0a,0x00,0x00,0x00,0x00,0x10,0x32,0x54,0x76,0x98,0xba,0xdc,0xfe,0x5a], (0x5a, 0xfedcba9876543210, 0x0, 0x0)), - ("U7", &[0x01,0x06,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff], &[0x01,0x00,0x0a,0x00,0x00,0x00,0x00,0x00], (0xffffffffffffffff, 0xffffffffffffffff, 0xffffffff, 0xffffffff)), - ("U8", &[0x01,0x06,0x00,0x10,0x08,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x52,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], &[0x09,0x00,0x1b,0x00,0x00,0x00,0x01,0x00], (0x0, 0x53, 0x0, 0x0)), - ("U9", &[0x01,0x06,0x00,0x08,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], &[0x09,0x00,0x1b,0x00,0x00,0x00,0x00,0x01], (0x0, 0x1, 0x0, 0x0)), - ("U10", &[0x04,0x06,0x40,0x40,0x20,0x00,0x00,0x28,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x05,0x05,0x00,0x00,0x06,0x00,0x00,0x00], &[0x05,0x00,0x22,0x00,0x00,0x00,0x00,0xd9,0xfb,0xff,0xff,0x08,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x03,0x02], (0x203010100000000, 0x0, 0x0, 0x0)), - ("U11", &[0x01,0x06,0x08,0x08,0x00,0x00,0x00,0x00,0xe8,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0xd0,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0xb8,0x0b,0x00,0x00,0x00,0x00,0x00,0x00,0xa0,0x0f,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], &[0x01,0x80,0x1d,0xff,0xff,0xff,0x09,0x07], (0x3ef, 0x7d9, 0xbb8, 0xfa0)), - ("U12", &[0x01,0x06,0x08,0x08,0x00,0x00,0x00,0x00,0xe8,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0xd0,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0xb8,0x0b,0x00,0x00,0x00,0x00,0x00,0x00,0xa0,0x0f,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], &[0x03,0x00,0x1d,0x00,0x00,0x00,0x09,0x07,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff], (0x3ef, 0x7d9, 0xbb8, 0xfa0)), - ("U13", &[0x01,0x06,0x01,0x00,0x00,0x00,0x00,0x00,0xfe,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], &[0x01,0x00,0x0a,0x00,0x00,0x00,0x00,0x80], (0xffffffffffffffff, 0x0, 0x0, 0x0)), - ("U14", &[0x01,0x06,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], &[0x01,0x00,0x0a,0x00,0xff,0xff,0xff,0xff], (0x0, 0x0, 0x0, 0x0)), -]; diff --git a/lib/bcachefs/src/upstream/btree.rs b/lib/bcachefs/src/upstream/btree.rs deleted file mode 100644 index f5e70589072..00000000000 --- a/lib/bcachefs/src/upstream/btree.rs +++ /dev/null @@ -1,141 +0,0 @@ -//! Walking a btree: merging a node's bsets into one ordered list of live keys, -//! and descending from a root to the leaves a key range touches. - -use alloc::vec::Vec; - -use crate::block_io::BlockIO; - -use super::bkey::{Bpos, Key, TYPE_DELETED, TYPE_EXTENT_WHITEOUT, TYPE_HASH_WHITEOUT, TYPE_WHITEOUT}; -use super::node::{clean_roots, BtreePtr, Node, MAX_DEPTH}; -use super::sb::Superblock; -use super::UpstreamError; - -/// The btrees `BCH_BTREE_IDS()` numbers that a file needs. -pub const BTREE_EXTENTS: u32 = 0; -pub const BTREE_INODES: u32 = 1; -pub const BTREE_DIRENTS: u32 = 2; -pub const BTREE_SUBVOLUMES: u32 = 8; - -/// A node's keys, merged: one key per position, the newest bset winning, with -/// the bset it came from kept so an extent can resolve an overlap by it. -/// -/// The node is a log — a later bset overrides an earlier one at the same -/// position — so a reader that concatenated the bsets would serve deleted -/// files and stale extents. -pub fn merged(node: &Node) -> Result, UpstreamError> { - let mut keys = node.keys()?; - // Stable by position, then by bset, so the last entry of each position run - // is the newest one. - keys.sort_by(|(ga, a), (gb, b)| a.pos.cmp(&b.pos).then(ga.cmp(gb))); - - let mut out: Vec<(usize, Key)> = Vec::with_capacity(keys.len()); - for (generation, key) in keys { - match out.last_mut() { - Some((g, prev)) if prev.pos == key.pos => { - *g = generation; - *prev = key; - } - _ => out.push((generation, key)), - } - } - out.retain(|(_, k)| !is_tombstone(k.kind)); - Ok(out) -} - -/// Types that say "nothing is here", whatever an older bset said. -/// -/// `extent_whiteout` is one of them: it is what the extents btree writes where -/// a snapshot deletion took data away, and a reader that served it back would -/// hand out an extent with no value. -fn is_tombstone(kind: u8) -> bool { - matches!(kind, TYPE_DELETED | TYPE_WHITEOUT | TYPE_HASH_WHITEOUT | TYPE_EXTENT_WHITEOUT) -} - -/// A btree, opened at its root. -pub struct Btree<'a> { - io: &'a dyn BlockIO, - sb: &'a Superblock, - id: u32, - root: BtreePtr, - level: u8, -} - -impl<'a> Btree<'a> { - /// Open the btree `id` at the root the clean section records. - pub fn open(io: &'a dyn BlockIO, sb: &'a Superblock, id: u32) -> Result { - let (_, level, root) = clean_roots(sb)? - .into_iter() - .find(|(btree, _, _)| *btree == id) - .ok_or(UpstreamError::Refused("filesystem has no root for a btree a file needs"))?; - Ok(Self { io, sb, id, root, level }) - } - - /// Hand every live key whose position is in `from..=to` to `visit`, in - /// order, stopping early when it answers `false`. - pub fn range( - &self, - from: Bpos, - to: Bpos, - visit: &mut dyn FnMut(&Node, usize, &Key) -> Result, - ) -> Result<(), UpstreamError> { - self.descend(&self.root, self.level, from, to, visit).map(|_| ()) - } - - fn descend( - &self, - ptr: &BtreePtr, - level: u8, - from: Bpos, - to: Bpos, - visit: &mut dyn FnMut(&Node, usize, &Key) -> Result, - ) -> Result { - if level >= MAX_DEPTH { - return Err(UpstreamError::Refused("btree is deeper than the format allows")); - } - let node = Node::read(self.io, self.sb, ptr, self.id, level)?; - let keys = merged(&node)?; - - if level == 0 { - for (generation, key) in &keys { - if key.pos < from { - continue; - } - if key.pos > to { - return Ok(false); - } - if !visit(&node, *generation, key)? { - return Ok(false); - } - } - return Ok(true); - } - - // An interior key's position is its child's *last* key, so the first - // child that can hold `from` is the first key at or above it. - for (_, key) in &keys { - if key.pos < from { - continue; - } - let child = BtreePtr::read(&node.value(key)?)?; - if !self.descend(&child, level - 1, from, to, visit)? { - return Ok(false); - } - if key.pos >= to { - return Ok(false); - } - } - Ok(true) - } - - /// The one live key at exactly `pos`, if the btree holds one. - pub fn get(&self, pos: Bpos) -> Result, Key)>, UpstreamError> { - let mut found = None; - self.range(pos, pos, &mut |node, _, key| { - if key.pos == pos { - found = Some((node.value(key)?.bytes().to_vec(), *key)); - } - Ok(false) - })?; - Ok(found) - } -} diff --git a/lib/bcachefs/src/upstream/csum.rs b/lib/bcachefs/src/upstream/csum.rs deleted file mode 100644 index 75971126d54..00000000000 --- a/lib/bcachefs/src/upstream/csum.rs +++ /dev/null @@ -1,230 +0,0 @@ -//! The unkeyed checksums a bcachefs superblock may name, seeded as the format -//! computes them. `crc32c` and `crc64` are the bare Castagnoli and ECMA-182 -//! updates, seeded 0 and not inverted; their `nonzero` variants seed and -//! finish with all ones, which is CRC-32C and CRC-64/WE. `xxhash` is XXH64 -//! seeded 0. - -use super::UpstreamError; - -/// The checksum types `BCH_CSUM_TYPES()` numbers. A type this crate cannot -/// compute is refused by name at the boundary rather than skipped. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum CsumType { - None, - Crc32cNonzero, - Crc64Nonzero, - Crc32c, - Crc64, - Xxhash, -} - -impl CsumType { - pub fn from_disk(raw: u64) -> Result { - match raw { - 0 => Ok(Self::None), - 1 => Ok(Self::Crc32cNonzero), - 2 => Ok(Self::Crc64Nonzero), - 3 => Err(UpstreamError::Refused("checksum type chacha20_poly1305_80: encryption")), - 4 => Err(UpstreamError::Refused("checksum type chacha20_poly1305_128: encryption")), - 5 => Ok(Self::Crc32c), - 6 => Ok(Self::Crc64), - 7 => Ok(Self::Xxhash), - _ => Err(UpstreamError::Refused("checksum type is not one this format defines")), - } - } - - /// The 128-bit `bch_csum` upstream stores: the digest in `lo`, `hi` zero - /// for every type here. - pub fn digest(self, data: &[u8]) -> (u64, u64) { - let lo = match self { - Self::None => 0, - Self::Crc32c => crc32c_update(0, data) as u64, - Self::Crc32cNonzero => (crc32c_update(u32::MAX, data) ^ u32::MAX) as u64, - Self::Crc64 => crc64_ecma_update(0, data), - Self::Crc64Nonzero => crc64_ecma_update(u64::MAX, data) ^ u64::MAX, - Self::Xxhash => xxh64(data, 0), - }; - (lo, 0) - } - - /// True when `data` carries the checksum `stored` claims. - /// - /// `CsumType::None` verifies everything, which is upstream's behaviour and - /// is why a mount refuses `none` metadata rather than trusting this. - pub fn verify(self, data: &[u8], stored: (u64, u64)) -> bool { - self.digest(data) == stored - } -} - -const CRC32C_TABLE: [u32; 256] = { - let mut table = [0u32; 256]; - let mut i = 0u32; - while i < 256 { - let mut crc = i; - let mut j = 0; - while j < 8 { - crc = if crc & 1 != 0 { (crc >> 1) ^ 0x82F6_3B78 } else { crc >> 1 }; - j += 1; - } - table[i as usize] = crc; - i += 1; - } - table -}; - -/// The Castagnoli update from `crc`, with neither the pre- nor the -/// post-inversion the standalone CRC-32C carries. -pub fn crc32c_update(mut crc: u32, data: &[u8]) -> u32 { - for &byte in data { - crc = (crc >> 8) ^ CRC32C_TABLE[((crc ^ byte as u32) & 0xFF) as usize]; - } - crc -} - -const CRC64_TABLE: [u64; 256] = { - let mut table = [0u64; 256]; - let mut i = 0u64; - while i < 256 { - let mut crc = i << 56; - let mut j = 0; - while j < 8 { - crc = if crc >> 63 != 0 { - (crc << 1) ^ 0x42F0_E1EB_A9EA_3693 - } else { - crc << 1 - }; - j += 1; - } - table[i as usize] = crc; - i += 1; - } - table -}; - -/// The ECMA-182 update from `crc`, most significant bit first, inverting -/// nothing: seeded 0 it is CRC-64/ECMA-182. -pub fn crc64_ecma_update(mut crc: u64, data: &[u8]) -> u64 { - for &byte in data { - crc = CRC64_TABLE[(((crc >> 56) ^ byte as u64) & 0xFF) as usize] ^ (crc << 8); - } - crc -} - -const XXH_P1: u64 = 0x9E37_79B1_85EB_CA87; -const XXH_P2: u64 = 0xC2B2_AE3D_27D4_EB4F; -const XXH_P3: u64 = 0x1656_67B1_9E37_79F9; -const XXH_P4: u64 = 0x85EB_CA77_C2B2_AE63; -const XXH_P5: u64 = 0x27D4_EB2F_1656_67C5; - -fn xxh_round(acc: u64, input: u64) -> u64 { - acc.wrapping_add(input.wrapping_mul(XXH_P2)).rotate_left(31).wrapping_mul(XXH_P1) -} - -fn xxh_merge(acc: u64, val: u64) -> u64 { - (acc ^ xxh_round(0, val)).wrapping_mul(XXH_P1).wrapping_add(XXH_P4) -} - -/// XXH64 over the whole buffer. -pub fn xxh64(data: &[u8], seed: u64) -> u64 { - let mut rest = data; - let mut hash; - - if data.len() >= 32 { - let (mut v1, mut v2, mut v3, mut v4) = ( - seed.wrapping_add(XXH_P1).wrapping_add(XXH_P2), - seed.wrapping_add(XXH_P2), - seed, - seed.wrapping_sub(XXH_P1), - ); - while rest.len() >= 32 { - v1 = xxh_round(v1, le64(rest, 0)); - v2 = xxh_round(v2, le64(rest, 8)); - v3 = xxh_round(v3, le64(rest, 16)); - v4 = xxh_round(v4, le64(rest, 24)); - rest = &rest[32..]; - } - hash = v1 - .rotate_left(1) - .wrapping_add(v2.rotate_left(7)) - .wrapping_add(v3.rotate_left(12)) - .wrapping_add(v4.rotate_left(18)); - hash = xxh_merge(hash, v1); - hash = xxh_merge(hash, v2); - hash = xxh_merge(hash, v3); - hash = xxh_merge(hash, v4); - } else { - hash = seed.wrapping_add(XXH_P5); - } - - hash = hash.wrapping_add(data.len() as u64); - - while rest.len() >= 8 { - hash = (hash ^ xxh_round(0, le64(rest, 0))).rotate_left(27).wrapping_mul(XXH_P1); - hash = hash.wrapping_add(XXH_P4); - rest = &rest[8..]; - } - if rest.len() >= 4 { - let word = u32::from_le_bytes([rest[0], rest[1], rest[2], rest[3]]) as u64; - hash = (hash ^ word.wrapping_mul(XXH_P1)).rotate_left(23).wrapping_mul(XXH_P2); - hash = hash.wrapping_add(XXH_P3); - rest = &rest[4..]; - } - for &byte in rest { - hash = (hash ^ (byte as u64).wrapping_mul(XXH_P5)).rotate_left(11).wrapping_mul(XXH_P1); - } - - hash ^= hash >> 33; - hash = hash.wrapping_mul(XXH_P2); - hash ^= hash >> 29; - hash = hash.wrapping_mul(XXH_P3); - hash ^= hash >> 32; - hash -} - -fn le64(buf: &[u8], off: usize) -> u64 { - u64::from_le_bytes(buf[off..off + 8].try_into().expect("an 8-byte window")) -} - -#[cfg(test)] -mod tests { - use super::*; - - /// The check values the three algorithms' own specifications publish, and - /// the seedings upstream picks out of them. - #[test] - fn published_check_values() { - assert_eq!(crc32c_update(u32::MAX, b"123456789") ^ u32::MAX, 0xE306_9283); - assert_eq!(crc64_ecma_update(0, b"123456789"), 0x6c40_df5f_0b49_7347); - assert_eq!(CsumType::Crc64Nonzero.digest(b"123456789"), (0x62ec_59e3_f1a4_f00a, 0)); - assert_eq!(xxh64(b"", 0), 0xEF46_DB37_51D8_E999); - } - - /// XXH64's three tails — the 32-byte stripes, the 8- and 4-byte words and - /// the loose bytes — are separate branches, so a length past each is what - /// covers them. - #[test] - fn xxh64_covers_every_tail() { - let data: Vec = (0u8..=200).cycle().take(201).collect(); - for len in [0usize, 1, 3, 4, 7, 8, 31, 32, 33, 63, 64, 200] { - let a = xxh64(&data[..len], 0); - let b = xxh64(&data[..len], 0); - assert_eq!(a, b, "xxh64 is not a function at length {len}"); - } - assert_ne!(xxh64(&data[..32], 0), xxh64(&data[..33], 0)); - } - - /// Every checksum type puts its digest in `lo` and leaves `hi` zero, and - /// the two encrypted types are refused by name rather than numbered. - #[test] - fn encryption_csum_types_are_refused_by_name() { - for raw in [3u64, 4] { - let err = CsumType::from_disk(raw).expect_err("an encrypted volume must be refused"); - assert!( - matches!(err, UpstreamError::Refused(reason) if reason.contains("encryption")), - "{err:?} does not name encryption" - ); - } - assert!(CsumType::from_disk(8).is_err(), "an undefined checksum type must be refused"); - assert_eq!(CsumType::Crc32c.digest(b"abc").1, 0); - } -} diff --git a/lib/bcachefs/src/upstream/dirent_vectors.rs b/lib/bcachefs/src/upstream/dirent_vectors.rs deleted file mode 100644 index b5cd7e3768f..00000000000 --- a/lib/bcachefs/src/upstream/dirent_vectors.rs +++ /dev/null @@ -1,29 +0,0 @@ -pub const DIRENT_CASES: &[(&str, &[u8], Decoded)] = &[ - ("D1", &[0x00,0x00,0x00,0x80,0x00,0x00,0x00,0x00,0x04,0x6c,0x6f,0x73,0x74,0x2b,0x66,0x6f,0x75,0x6e,0x64,0x00,0x00,0x00,0x00,0x00], Ok(("lost+found", 0x80000000, 4))), - ("D2", &[0x01,0x00,0x00,0x80,0x00,0x00,0x00,0x00,0x04,0x44,0x6f,0x63,0x75,0x6d,0x65,0x6e,0x74,0x73,0x00,0x00,0x00,0x00,0x00,0x00], Ok(("Documents", 0x80000001, 4))), - ("D3", &[0x02,0x00,0x00,0x80,0x00,0x00,0x00,0x00,0x04,0x64,0x65,0x65,0x70,0x00,0x00,0x00], Ok(("deep", 0x80000002, 4))), - ("D4", &[0x04,0x00,0x00,0x80,0x00,0x00,0x00,0x00,0x0a,0x6c,0x69,0x6e,0x6b,0x00,0x00,0x00], Ok(("link", 0x80000004, 10))), - ("D5", &[0x03,0x00,0x00,0x80,0x00,0x00,0x00,0x00,0x08,0x73,0x65,0x71,0x2e,0x74,0x78,0x74], Ok(("seq.txt", 0x80000003, 8))), - ("D6", &[0x00,0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x65,0x6d,0x70,0x74,0x79,0x00,0x00], Ok(("empty", 0x1000, 4))), - ("D7", &[0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x08,0x61,0x62,0x63,0x64,0x65,0x66,0x67], Ok(("abcdefg", 0x7, 8))), - ("D8", &[0x0b,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x08,0x61,0x00,0x00,0x00,0x00,0x00,0x00], Ok(("a", 0xb, 8))), - ("D9", &[0x0c,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x08,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f], Ok(("abcdefghijklmno", 0xc, 8))), - ("D10", &[0x0d,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x08,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x00,0x00,0x00,0x00,0x00,0x00,0x00], Ok(("abcdefgh", 0xd, 8))), - ("D11", &[0x0e,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x08,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x00,0x00,0x00,0x00,0x00,0x00,0x00], Ok(("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqr", 0xe, 8))), - ("D12", &[0x0e,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x08,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x00,0x00,0x00,0x00,0x00,0x00], Err("dirent's name is empty or past BCH_NAME_MAX")), - ("D13", &[0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], Err("dirent is shorter than its header")), - ("D14", &[0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], Err("dirent is shorter than its header")), - ("D15", &[0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x00,0x00,0x00,0x00,0x00,0x00], Err("dirent's name is empty or past BCH_NAME_MAX")), - ("D16", &[0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], Err("dirent's last word is all padding")), - ("D17", &[0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x08,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], Err("dirent's last word is all padding")), - ("D18", &[0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x08,0x61,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], Err("dirent's last word is all padding")), - ("D19", &[0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x88,0x78,0x00,0x00,0x00,0x00,0x00,0x00], Err("dirent is casefolded")), - ("D20", &[0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x10,0x73,0x75,0x62,0x00,0x00,0x00,0x00], Err("dirent points at a subvolume")), - ("D21", &[0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x08,0x61,0x2f,0x62,0x00,0x00,0x00,0x00], Err("dirent's name holds a separator or a NUL")), - ("D22", &[0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x08,0x61,0x00,0x62,0x00,0x00,0x00,0x00], Err("dirent's name holds a separator or a NUL")), - ("D23", &[0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x08,0xff,0xfe,0x00,0x00,0x00,0x00,0x00], Err("dirent's name is not valid UTF-8")), - ("D24", &[0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x68,0x78,0x00,0x00,0x00,0x00,0x00,0x00], Ok(("x", 0x3, 8))), - ("D25", &[0x15,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x08,0xc3,0xa9,0x00,0x00,0x00,0x00,0x00], Ok(("é", 0x15, 8))), - ("D26", &[0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x08,0x61,0x62,0x63,0x64,0x65,0x66,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], Err("dirent's last word is all padding")), - ("D27", &[0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x90,0x78,0x00,0x00,0x00,0x00,0x00,0x00], Err("dirent is casefolded")), -]; diff --git a/lib/bcachefs/src/upstream/fs.rs b/lib/bcachefs/src/upstream/fs.rs deleted file mode 100644 index beaa6aaaa5e..00000000000 --- a/lib/bcachefs/src/upstream/fs.rs +++ /dev/null @@ -1,710 +0,0 @@ -//! What a file is: the inode, dirent and extent values, and the read-only -//! volume API over them. - -use alloc::string::String; -use alloc::vec; -use alloc::vec::Vec; - -use crate::block_io::{BlockBuf, BlockIO, BlockNum, BLOCK_SIZE}; - -use super::bkey::{ - Bpos, Key, TYPE_DIRENT, TYPE_ERROR, TYPE_EXTENT, TYPE_INLINE_DATA, TYPE_INODE_V3, - TYPE_RESERVATION, TYPE_SUBVOLUME, -}; -use super::btree::{Btree, BTREE_DIRENTS, BTREE_EXTENTS, BTREE_INODES, BTREE_SUBVOLUMES}; -use super::csum::CsumType; -use super::raw::{bits, Raw}; -use super::sb::{Superblock, EXTENT_TYPES_MAX, SECTOR}; -use super::UpstreamError; - -/// `BCACHEFS_ROOT_SUBVOL` and `BCACHEFS_ROOT_INO`. -const ROOT_SUBVOL: u64 = 1; -const ROOT_INO: u64 = 4096; -/// The snapshot id a new volume gives the root subvolume and every key under -/// it, and the only one this reader serves a key from. -const NO_SNAPSHOT: u32 = u32::MAX; -/// `BCH_NAME_MAX`. -pub const NAME_MAX: usize = 512; -/// `INODEv3_FIELDS_START_CUR`: `bch_inode_v3`'s fixed part, in u64s. -const INODE_FIELDS_START_CUR: u64 = 6; -const INODE_FIELDS_AT: usize = 48; - -/// The `d_type` values a dirent carries, which are the POSIX ones plus -/// `DT_SUBVOL`. -const DT_DIR: u8 = 4; -const DT_REG: u8 = 8; -const DT_LNK: u8 = 10; -const DT_SUBVOL: u8 = 16; -/// `offsetof(struct bch_dirent, d_name)`. -const DIRENT_NAME_AT: usize = 9; - -/// What a directory entry says a name is. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FileKind { - Dir, - Regular, - Symlink, - /// A type this reader does not open: a device node, a fifo, a subvolume. - Other(u8), -} - -impl FileKind { - fn from_dirent(d_type: u8) -> Self { - match d_type { - DT_DIR => Self::Dir, - DT_REG => Self::Regular, - DT_LNK => Self::Symlink, - other => Self::Other(other), - } - } - - fn from_mode(mode: u16) -> Self { - match mode & 0o170_000 { - 0o040_000 => Self::Dir, - 0o100_000 => Self::Regular, - 0o120_000 => Self::Symlink, - _ => Self::Other((mode >> 12) as u8), - } - } -} - -/// An inode's fixed fields. The varint tail is not decoded: nothing the read -/// path answers needs a field past `bi_size`, and a varint parsed for nobody -/// is a bound nobody checks. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Attrs { - pub inum: u64, - pub size: u64, - pub sectors: u64, - pub mode: u16, - pub kind: FileKind, -} - -/// A bcachefs volume, opened read-only. -pub struct Volume { - io: IO, - sb: Superblock, - root: u64, - entry_u64s: [u8; EXTENT_TYPES_MAX], - /// The largest byte offset any file on this volume can have data at. - device_bytes: u64, -} - -impl Volume { - /// Open a volume: validate the superblock, then take the root subvolume's - /// root inode. - /// - /// **Snapshots are refused here rather than filtered later.** Every key a - /// volume with none carries sits at one snapshot id; serving a file means - /// deciding which of several versions of a key is visible, and this reader - /// does not make that decision, so a root subvolume at any other id is - /// refused by name. - pub fn open(io: IO) -> Result { - let sb = super::sb::read(&io)?; - let entry_u64s = sb.extent_entry_u64s()?; - let root = { - let subvols = Btree::open(&io, &sb, BTREE_SUBVOLUMES)?; - let (val, key) = subvols - .get(Bpos::new(0, ROOT_SUBVOL, 0))? - .ok_or(UpstreamError::Refused("volume has no root subvolume"))?; - if key.kind != TYPE_SUBVOLUME { - return Err(UpstreamError::Refused("root subvolume's key is not a subvolume")); - } - let raw = Raw::new(&val, "root subvolume's value ends early"); - if raw.u32(4)? != NO_SNAPSHOT { - return Err(UpstreamError::Refused("volume has snapshots")); - } - raw.u64(8)? - }; - if root != ROOT_INO { - return Err(UpstreamError::Refused("root subvolume does not name the root inode")); - } - let device_bytes = io - .block_count() - .checked_mul(BLOCK_SIZE as u64) - .ok_or(UpstreamError::Refused("device size in bytes overflows"))?; - Ok(Self { io, sb, root, entry_u64s, device_bytes }) - } - - pub fn superblock(&self) -> &Superblock { - &self.sb - } - - pub fn root(&self) -> u64 { - self.root - } - - /// The one snapshot every key on a volume without snapshots sits at. - fn pos(&self, inode: u64, offset: u64) -> Bpos { - Bpos::new(inode, offset, NO_SNAPSHOT) - } - - /// **A scanned range spans every snapshot id, so each key it hands back is - /// checked rather than assumed.** `Volume::open` refuses a volume whose - /// root subvolume is not at [`NO_SNAPSHOT`]; this is the same refusal for - /// a key that claims otherwise, so no version of a key is silently chosen. - fn one_snapshot(key: &Key) -> Result<(), UpstreamError> { - if key.pos.snapshot != NO_SNAPSHOT { - return Err(UpstreamError::Refused("volume holds a key at a snapshot other than its root subvolume's")); - } - Ok(()) - } - - /// The inode `inum`, or `None` when the btree holds no such key. - pub fn stat(&self, inum: u64) -> Result, UpstreamError> { - let inodes = Btree::open(&self.io, &self.sb, BTREE_INODES)?; - let Some((val, key)) = inodes.get(self.pos(0, inum))? else { - return Ok(None); - }; - if key.kind != TYPE_INODE_V3 { - return Err(UpstreamError::Refused("inode is a version this reader does not decode")); - } - let raw = Raw::new(&val, "inode's value ends early"); - let flags = raw.u64(16)?; - if bits(flags, 31, 36) != INODE_FIELDS_START_CUR { - return Err(UpstreamError::Refused("inode's fixed part is not the length this version defines")); - } - if val.len() < INODE_FIELDS_AT { - return Err(UpstreamError::Refused("inode is shorter than its fixed part")); - } - let mode = bits(flags, 36, 52) as u16; - Ok(Some(Attrs { - inum, - sectors: raw.u64(24)?, - size: raw.u64(32)?, - mode, - kind: FileKind::from_mode(mode), - })) - } - - /// Hand every entry of directory `dir` to `visit`, in the btree's order, - /// stopping early when it answers `false`. - /// - /// A directory's entries occupy one contiguous key range — the hash is the - /// key's offset — so this reads that range and nothing else. - pub fn readdir( - &self, - dir: u64, - visit: &mut dyn FnMut(&str, u64, FileKind) -> bool, - ) -> Result<(), UpstreamError> { - let dirents = Btree::open(&self.io, &self.sb, BTREE_DIRENTS)?; - let mut failure = None; - dirents.range(self.pos(dir, 0), self.pos(dir, u64::MAX), &mut |node, _, key| { - Self::one_snapshot(key)?; - if key.kind != TYPE_DIRENT { - return Err(UpstreamError::Refused("dirents btree holds a key that is not a dirent")); - } - let val = node.value(key)?; - let dirent = match Dirent::read(val.bytes()) { - Ok(dirent) => dirent, - Err(err) => { - failure = Some(err.into()); - return Ok(false); - } - }; - Ok(visit(dirent.name, dirent.inum, dirent.kind)) - })?; - match failure { - Some(err) => Err(err), - None => Ok(()), - } - } - - /// The entry `name` of directory `dir`. - pub fn lookup(&self, dir: u64, name: &str) -> Result, UpstreamError> { - if name.is_empty() || name.len() > NAME_MAX { - return Err(UpstreamError::Refused("a name of no length, or past BCH_NAME_MAX")); - } - let mut found = None; - self.readdir(dir, &mut |entry, inum, kind| { - if entry == name { - found = Some((inum, kind)); - return false; - } - true - })?; - Ok(found) - } - - /// Resolve an absolute path to an inode, following no symlink. - pub fn resolve(&self, path: &str) -> Result, UpstreamError> { - if !path.starts_with('/') { - return Err(UpstreamError::Refused("a path this reader resolves is absolute")); - } - let mut at = (self.root, FileKind::Dir); - for part in path.split('/').filter(|p| !p.is_empty() && *p != ".") { - if at.1 != FileKind::Dir { - return Ok(None); - } - match self.lookup(at.0, part)? { - Some(next) => at = next, - None => return Ok(None), - } - } - Ok(Some(at)) - } - - /// Read the whole of file `inum`, up to the size its inode declares. - /// - /// A range no extent covers is a hole and reads as zeros, which is what - /// makes the inode's size — not the extents — the length of the answer. - /// - /// **`bi_size` is a `u64` off the disk and it sizes this buffer**, which is - /// the allocator-sizing defect this crate's `alloc_probe` exists to catch. - /// The device is the bound: a file cannot be longer than the disk it is on, - /// whatever its inode says, so an inode claiming more is refused before a - /// byte of it is allocated. - pub fn read(&self, inum: u64) -> Result, UpstreamError> { - let attrs = self - .stat(inum)? - .ok_or(UpstreamError::Refused("a file whose inode the btree does not hold"))?; - if attrs.sectors > self.device_bytes / SECTOR as u64 { - return Err(UpstreamError::Refused("inode claims more sectors than the device has")); - } - let mut out = output_buffer(attrs.size, self.device_bytes)?; - - let extents = Btree::open(&self.io, &self.sb, BTREE_EXTENTS)?; - let mut failure = None; - extents.range(self.pos(inum, 0), self.pos(inum, u64::MAX), &mut |node, _, key| { - Self::one_snapshot(key)?; - let val = node.value(key)?; - if let Err(err) = self.place(key, &val, &mut out) { - failure = Some(err); - return Ok(false); - } - Ok(true) - })?; - match failure { - Some(err) => Err(err), - None => Ok(out), - } - } - - /// The target of symlink `inum`: its contents, which is where bcachefs - /// keeps it. - pub fn read_link(&self, inum: u64) -> Result { - let bytes = self.read(inum)?; - let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len()); - String::from_utf8(bytes[..end].to_vec()) - .map_err(|_| UpstreamError::Refused("symlink target is not valid UTF-8")) - } - - /// Copy one extent key's live bytes into `out` at the file offset it covers. - fn place(&self, key: &Key, val: &Raw<'_>, out: &mut [u8]) -> Result<(), UpstreamError> { - // A key's position is the *end* of what it covers, in sectors. - let end_sectors = key.pos.offset; - let start_sectors = end_sectors - .checked_sub(key.size as u64) - .ok_or(UpstreamError::Refused("extent ends before it starts"))?; - // A key's offset is a number the btree chose, and `1 << 60` sectors is - // inside the scanned range and outside a `u64` of bytes. - let at = start_sectors - .checked_mul(SECTOR as u64) - .ok_or(UpstreamError::Refused("extent starts past any byte offset"))?; - let at = usize::try_from(at).map_err(|_| TOO_LARGE)?; - if at >= out.len() { - return Ok(()); - } - let want = out.len() - at; - - match key.kind { - TYPE_INLINE_DATA => { - let take = want.min(val.bytes().len()); - out[at..at + take].copy_from_slice(&val.bytes()[..take]); - Ok(()) - } - // A reservation is allocated-but-unwritten and reads as zeros, - // which is what `out` already holds. - TYPE_RESERVATION => Ok(()), - TYPE_ERROR => Err(UpstreamError::Refused("file holds an extent marked unrecoverable")), - TYPE_EXTENT => { - let bytes = self.read_extent(key, val)?; - let take = want.min(bytes.len()); - out[at..at + take].copy_from_slice(&bytes[..take]); - Ok(()) - } - _ => Err(UpstreamError::Refused("extents btree holds a value type this reader does not read")), - } - } - - /// The live bytes of one `KEY_TYPE_extent`, checksum verified. - /// - /// The checksum covers the extent as it was *written*, so a trimmed extent - /// is read whole, verified, and then sliced — a checksum over the live part - /// alone would verify nothing, because nobody ever computed one. - fn read_extent(&self, key: &Key, val: &Raw<'_>) -> Result, UpstreamError> { - let (ptr, crc) = find_ptr(val, &self.entry_u64s)?; - self.read_ptr(&ptr, crc, key.size) - } - - fn read_ptr(&self, entry: &Raw<'_>, crc: Crc, live_sectors: u32) -> Result, UpstreamError> { - let word = entry.u64(0)?; - if bits(word, 1, 2) != 0 { - return Err(UpstreamError::Refused("extent's first pointer is a cache copy")); - } - if bits(word, 3, 4) != 0 { - return Err(UpstreamError::Refused("extent is unwritten")); - } - if bits(word, 48, 56) as u8 != self.sb.dev_idx() { - return Err(UpstreamError::Refused("extent points at a device this filesystem has not got")); - } - if crc.compression != 0 { - return Err(UpstreamError::Refused("extent is compressed")); - } - if crc.offset as u64 + live_sectors as u64 > crc.stored_sectors as u64 { - return Err(UpstreamError::Refused("extent's live range is not inside the extent")); - } - - let start = bits(word, 4, 48); - let whole = self.read_sectors(start, crc.stored_sectors)?; - if !crc.csum_type.verify(&whole, crc.csum) { - return Err(UpstreamError::Refused("extent's data does not match its checksum")); - } - let from = crc.offset as usize * SECTOR; - let len = live_sectors as usize * SECTOR; - Ok(whole[from..from + len].to_vec()) - } - - fn read_sectors(&self, start: u64, sectors: u32) -> Result, UpstreamError> { - let per_block = (BLOCK_SIZE / SECTOR) as u64; - if !start.is_multiple_of(per_block) || !(sectors as u64).is_multiple_of(per_block) { - return Err(UpstreamError::Refused("extent is not block aligned")); - } - let first = start / per_block; - let blocks = sectors as u64 / per_block; - if first.checked_add(blocks).is_none_or(|end| end > self.io.block_count()) { - return Err(UpstreamError::Refused("extent runs past the end of the device")); - } - let mut out = vec![0u8; blocks as usize * BLOCK_SIZE]; - let mut buf = BlockBuf::zeroed(); - for i in 0..blocks as usize { - let block = BlockNum::new(first + i as u64); - self.io.read_block(block, &mut buf).map_err(|e| UpstreamError::Device(block, e))?; - out[i * BLOCK_SIZE..(i + 1) * BLOCK_SIZE].copy_from_slice(buf.as_bytes()); - } - Ok(out) - } -} - -const TOO_LARGE: UpstreamError = - UpstreamError::Refused("file is larger than this machine can address"); - -const ENTRY_PTR: u32 = 0; -const ENTRY_CRC32: u32 = 1; -const ENTRY_CRC64: u32 = 2; -const ENTRY_CRC128: u32 = 3; -const ENTRY_STRIPE_PTR: u32 = 4; - -/// The buffer a file's contents are read into, sized by its inode. -/// -/// **The bound and the allocation are one function** so nothing can size a -/// `Vec` from `bi_size` without passing the device it is on: a file cannot be -/// longer than the disk holding it, whatever its inode claims. -fn output_buffer(size: u64, device_bytes: u64) -> Result, UpstreamError> { - if size > device_bytes { - return Err(UpstreamError::Refused("inode claims a file longer than the device it is on")); - } - Ok(vec![0u8; usize::try_from(size).map_err(|_| TOO_LARGE)?]) -} - -/// Walk an extent's interleaved entries to its first device pointer, carrying -/// whatever checksum entry preceded it. -/// -/// **Each entry's length comes from the volume's own table, and one word of -/// error here is arbitrary device blocks served as file contents.** An entry -/// can precede the checksum and the pointer: a `flags` entry is written first -/// in the value, so a value is `[flags, crc32, ptr]` and a walk that oversizes -/// the first entry lands somewhere other than the pointer. -fn find_ptr<'a>( - val: &Raw<'a>, - sizes: &[u8; EXTENT_TYPES_MAX], -) -> Result<(Raw<'a>, Crc), UpstreamError> { - let mut crc: Option = None; - let mut at = 0usize; - while at < val.bytes().len() { - let word = val.u64(at)?; - if word == 0 { - return Err(UpstreamError::Refused("extent holds an entry of no type")); - } - let kind = word.trailing_zeros(); - let u64s = entry_u64s(sizes, kind)?; - let entry = val.sub(at, u64s * 8, "extent entry runs past its value")?; - match kind { - // **A bset with no checksum is refused, so an extent with none is - // too.** A pointer with no crc entry before it is data nobody - // computed a checksum over, on a volume whose superblock says it - // checksums its data — which `check_supported` has already required. - ENTRY_PTR => { - return match crc { - Some(crc) => Ok((entry, crc)), - None => Err(UpstreamError::Refused("extent carries no checksum entry")), - } - } - ENTRY_CRC32 => crc = Some(Crc::crc32(&entry)?), - ENTRY_CRC64 => crc = Some(Crc::crc64(&entry)?), - ENTRY_CRC128 => { - return Err(UpstreamError::Refused("extent carries a 128-bit checksum, which is encryption")) - } - ENTRY_STRIPE_PTR => { - return Err(UpstreamError::Refused("extent points into an erasure-coded stripe")) - } - // Rebalance, flags and reconcile entries say nothing about where - // the bytes are; their length is the whole of what matters. - _ => {} - } - at += u64s * 8; - } - Err(UpstreamError::Refused("extent has no device pointer")) -} - -/// How far the next entry of an extent's value is, from the table the volume's -/// own superblock states. A type it sizes at zero is one nothing can step over. -fn entry_u64s(sizes: &[u8; EXTENT_TYPES_MAX], kind: u32) -> Result { - match sizes.get(kind as usize).copied() { - Some(0) | None => { - Err(UpstreamError::Refused("extent holds an entry type this volume gives no size for")) - } - Some(u64s) => Ok(u64s as usize), - } -} - -/// The checksum and geometry one `bch_extent_crc*` entry states about the -/// pointers that follow it. -#[derive(Clone, Copy)] -struct Crc { - /// Sectors as written, which is what the checksum covers. - stored_sectors: u32, - /// Sectors into the written extent where the live data starts. - offset: u32, - compression: u64, - csum_type: CsumType, - csum: (u64, u64), -} - -impl Crc { - /// A checksum entry that states no checksum verifies everything, so it is - /// refused where a bset's `none` is. - fn checked(self) -> Result { - if self.csum_type == CsumType::None { - return Err(UpstreamError::Refused("extent's checksum entry names no checksum")); - } - Ok(self) - } - - /// `bch_extent_crc32`; its sizes are stored biased by one. - fn crc32(entry: &Raw<'_>) -> Result { - let w = entry.u32(0)? as u64; - Self { - stored_sectors: bits(w, 9, 16) as u32 + 1, - offset: bits(w, 16, 23) as u32, - compression: bits(w, 28, 32), - csum_type: CsumType::from_disk(bits(w, 24, 28))?, - csum: (entry.u32(4)? as u64, 0), - } - .checked() - } - - /// `bch_extent_crc64`; its 64-bit checksum is split across two fields. - fn crc64(entry: &Raw<'_>) -> Result { - let w = entry.u64(0)?; - Self { - stored_sectors: bits(w, 12, 21) as u32 + 1, - offset: bits(w, 21, 30) as u32, - compression: bits(w, 44, 48), - csum_type: CsumType::from_disk(bits(w, 40, 44))?, - csum: (entry.u64(8)? | (bits(w, 48, 64) << 48), 0), - } - .checked() - } -} - -/// Each way a `bch_dirent` value is refused. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum DirentError { - /// Not whole words, or no room for `d_inum`, `d_type` and a name byte. - Short, - /// `d_casefold`: the bytes after `d_type` are laid out differently. - Casefolded, - Subvolume, - /// Empty, or longer than `BCH_NAME_MAX`. - NameLength, - SeparatorOrNul, - NotUtf8, - /// The last word is all NUL: no writer pads a name by a whole word. - PaddingWord, -} - -impl From for UpstreamError { - fn from(err: DirentError) -> Self { - UpstreamError::Refused(match err { - DirentError::Short => "dirent is shorter than its header", - DirentError::Casefolded => "dirent is casefolded", - DirentError::Subvolume => "dirent points at a subvolume", - DirentError::NameLength => "dirent's name is empty or past BCH_NAME_MAX", - DirentError::SeparatorOrNul => "dirent's name holds a separator or a NUL", - DirentError::NotUtf8 => "dirent's name is not valid UTF-8", - DirentError::PaddingWord => "dirent's last word is all padding", - }) - } -} - -/// One directory entry, borrowed from its value. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct Dirent<'a> { - name: &'a str, - inum: u64, - kind: FileKind, -} - -const D_TYPE_MASK: u8 = 0x1F; -const D_CASEFOLD: u8 = 0x80; - -impl<'a> Dirent<'a> { - /// Decode a `bch_dirent` value. - /// - /// The name's length is not stored: a writer pads the name with 0 to 7 - /// NULs to a whole word, so the name is what lies between `d_name` and the - /// NULs that end the last word. A NUL before that word is not padding. - /// A last word of nothing but NULs is refused: no writer pads by a whole - /// word, so such a value is not one a writer made. - fn read(val: &'a [u8]) -> Result { - let (Some((&inum, &[type_byte, _, ..])), true) = - (val.split_first_chunk::<8>(), val.len().is_multiple_of(8)) - else { - return Err(DirentError::Short); - }; - if type_byte & D_CASEFOLD != 0 { - return Err(DirentError::Casefolded); - } - let d_type = type_byte & D_TYPE_MASK; - if d_type == DT_SUBVOL { - return Err(DirentError::Subvolume); - } - - let padding = val.iter().rev().take(8).take_while(|&&byte| byte == 0).count(); - if padding == 8 { - return Err(DirentError::PaddingWord); - } - // At most 7 off a value of at least 16 bytes, so `d_name` is inside it. - let name = &val[DIRENT_NAME_AT..val.len() - padding]; - if name.is_empty() || name.len() > NAME_MAX { - return Err(DirentError::NameLength); - } - if name.iter().any(|&byte| byte == b'/' || byte == 0) { - return Err(DirentError::SeparatorOrNul); - } - let name = core::str::from_utf8(name).map_err(|_| DirentError::NotUtf8)?; - Ok(Self { name, inum: u64::from_le_bytes(inum), kind: FileKind::from_dirent(d_type) }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn decode(bytes: &[u8]) -> Result<(&str, u64, FileKind), UpstreamError> { - Dirent::read(bytes).map(|dirent| (dirent.name, dirent.inum, dirent.kind)).map_err(UpstreamError::from) - } - - mod vectors { - /// `(name, d_inum, d_type)`, or the refusal. - type Decoded = Result<(&'static str, u64, u8), &'static str>; - include!("dirent_vectors.rs"); - } - - /// Every dirent vector, names that fill their words and names padded by - /// one to seven NULs, and each refusal in the order it is checked. - #[test] - fn dirent_vectors() { - for &(id, bytes, want) in vectors::DIRENT_CASES { - let want = want - .map(|(name, inum, d_type)| (name, inum, FileKind::from_dirent(d_type))) - .map_err(UpstreamError::Refused); - assert_eq!(decode(bytes), want, "{id}"); - } - } - - /// A `bch_extent_crc32`'s sizes are stored biased by one, so a reader that - /// forgets the bias reads one sector short of every extent. - #[test] - fn a_crc_entry_states_the_extent_it_covers() { - // csum_type crc32c, compression none, uncompressed 80 sectors, - // compressed 80, offset 0 — the second extent of the fixture's seq.txt. - let word: u32 = 0b10 | (79 << 2) | (79 << 9) | (5 << 24); - let mut entry = word.to_le_bytes().to_vec(); - entry.extend_from_slice(&0xc522_c42fu32.to_le_bytes()); - let crc = Crc::crc32(&Raw::new(&entry, "crc32")).expect("a crc32 entry"); - assert_eq!(crc.stored_sectors, 80); - assert_eq!(crc.offset, 0); - assert_eq!(crc.compression, 0); - assert_eq!(crc.csum_type, CsumType::Crc32c); - assert_eq!(crc.csum, (0xc522_c42f, 0)); - } - - /// An inode claiming 256 TiB is refused having asked the allocator for - /// nothing like it. - #[test] - fn a_file_longer_than_its_device_is_refused_without_allocating_it() { - let device = 1u64 << 20; - let _ = crate::alloc_probe::take_peak(); - let refused = output_buffer(1 << 48, device); - let peak = crate::alloc_probe::take_peak(); - assert_eq!( - refused.err(), - Some(UpstreamError::Refused("inode claims a file longer than the device it is on")) - ); - assert!(peak < 1 << 16, "refusing a 256 TiB file asked the allocator for {peak} bytes"); - - assert_eq!(output_buffer(0, device).map(|b| b.len()), Ok(0)); - assert_eq!(output_buffer(device, device).map(|b| b.len()), Ok(device as usize)); - assert!(output_buffer(device + 1, device).is_err()); - } - - /// The sizes `sizeof` gives at the pinned commit, and the refusal for a - /// type the volume gives no size for. - #[test] - fn an_entry_this_volume_cannot_size_is_refused() { - let sizes = super::super::sb::EXTENT_ENTRY_U64S_KNOWN_FOR_TESTS; - for (kind, want) in [(0u32, 1usize), (1, 1), (2, 2), (3, 3), (4, 1), (5, 1), (6, 1), (7, 1), (8, 1)] { - assert_eq!(entry_u64s(&sizes, kind), Ok(want), "entry type {kind}"); - } - assert!(entry_u64s(&sizes, 9).is_err(), "a type the table sizes at zero must be refused"); - assert!(entry_u64s(&sizes, 99).is_err(), "a type past the table must be refused"); - } - - /// An entry ahead of the checksum and the pointer, sized right and sized - /// one word out. - /// - /// A `flags` entry is written first in an extent's value, so - /// `[flags, crc32, ptr]` is a value a volume holds; one word of size error - /// on it puts the walk on the checksum entry and the pointer out of reach. - #[test] - fn an_entry_before_the_checksum_does_not_move_the_pointer() { - // The type field's width is the type: `flags` is 7 bits, so 1 << 6. - let flags: u64 = 1 << 6; - let crc32_lo: u32 = 0b10 | (79 << 2) | (79 << 9) | (5 << 24); - let ptr: u64 = 1 | (0xE000u64 << 4); - - let mut value = flags.to_le_bytes().to_vec(); - value.extend_from_slice(&crc32_lo.to_le_bytes()); - value.extend_from_slice(&0xc522_c42fu32.to_le_bytes()); - value.extend_from_slice(&ptr.to_le_bytes()); - let val = Raw::new(&value, "extent"); - - let right = super::super::sb::EXTENT_ENTRY_U64S_KNOWN_FOR_TESTS; - let (entry, crc) = find_ptr(&val, &right).expect("the pointer"); - assert_eq!(entry.u64(0), Ok(ptr), "the walk did not land on the pointer"); - assert_eq!(crc.stored_sectors, 80, "the checksum entry was skipped"); - assert_eq!(crc.csum, (0xc522_c42f, 0)); - - // One word out: the walk steps over the checksum entry and reads the - // pointer with no checksum to verify it against. - let mut wrong = right; - wrong[6] = 2; - assert_eq!( - find_ptr(&val, &wrong).err(), - Some(UpstreamError::Refused("extent carries no checksum entry")), - "a `flags` entry sized one word out still produced a checksummed pointer" - ); - } -} diff --git a/lib/bcachefs/src/upstream/mod.rs b/lib/bcachefs/src/upstream/mod.rs deleted file mode 100644 index f6def9b5709..00000000000 --- a/lib/bcachefs/src/upstream/mod.rs +++ /dev/null @@ -1,37 +0,0 @@ -//! The upstream bcachefs on-disk format, read side. -//! -//! Format source: `fs/bcachefs_format.h` and the `*_format.h` beside it in -//! at commit -//! `d997ad76e906ca9cbe1d4045d3880185e504191d` (tag v1.39.4), whose -//! `bcachefs_metadata_version_current` is `per_dev_fragmentation_lru`, -//! `BCH_VERSION(1, 39)` = 1063 — the version `bcachefs format` writes. -//! -//! **Every number here came off a disk this crate does not own, and a checksum -//! is not authentication — whoever writes the image writes the checksum.** A -//! bound is applied where bytes become typed data and nowhere else, and each -//! refusal names what it refused: [`UpstreamError::Refused`] carries a -//! sentence, never a number a caller has to interpret. Nothing in this module -//! reinterprets disk bytes as a Rust type; every field is read out of a slice -//! at an offset this module states, because the format is packed and a -//! `repr(C)` view of it would be both unaligned and unchecked. -//! -//! This is the read half. It has no allocator, no journal writer and no -//! mutation, so a consumer that links it cannot change a disk. - -pub mod btree; -pub mod csum; -pub mod bkey; -pub mod raw; -pub mod fs; -pub mod node; -pub mod sb; - -/// What a refusal is: a sentence naming the field or feature, so a log line -/// says which byte on which disk stopped the mount. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum UpstreamError { - /// The image says something this reader will not act on. - Refused(&'static str), - /// The device would not answer for the block the reader asked for. - Device(crate::BlockNum, crate::DeviceError), -} diff --git a/lib/bcachefs/src/upstream/node.rs b/lib/bcachefs/src/upstream/node.rs deleted file mode 100644 index 74cb22f384e..00000000000 --- a/lib/bcachefs/src/upstream/node.rs +++ /dev/null @@ -1,500 +0,0 @@ -//! Btree nodes: a node is a log of checksummed bsets, and reading one means -//! validating each in turn and merging what they say. - -use alloc::vec; -use alloc::vec::Vec; - -use crate::block_io::{BlockBuf, BlockIO, BlockNum, BLOCK_SIZE}; - -use super::bkey::{Bpos, BkeyFormat, Key, BPOS_BYTES, FORMAT_BYTES}; -use super::csum::CsumType; -use super::raw::{bits, Raw}; -use super::sb::{Superblock, BSET_MAGIC, SECTOR}; -use super::UpstreamError; - -/// `BTREE_MAX_DEPTH`, and therefore the deepest descent this reader will make. -pub const MAX_DEPTH: u8 = 4; - -const NODE_MAGIC: usize = 16; -const NODE_FLAGS: usize = 24; -const NODE_MIN_KEY: usize = 32; -const NODE_MAX_KEY: usize = NODE_MIN_KEY + BPOS_BYTES; -/// After `max_key` comes the unused `_ptr`, then the format. -const NODE_FORMAT: usize = NODE_MAX_KEY + BPOS_BYTES + 8; -const NODE_BSET: usize = NODE_FORMAT + FORMAT_BYTES; -/// `offsetof(struct btree_node_entry, keys)`. -const ENTRY_BSET: usize = 16; - -const BSET_SEQ: usize = 0; -const BSET_FLAGS: usize = 16; -const BSET_U64S: usize = 22; -const BSET_KEYS: usize = 24; - -/// A pointer at a btree node: `bch_btree_ptr_v2`, the only kind a volume at -/// this metadata version writes. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct BtreePtr { - pub seq: u64, - /// Sectors of the node actually written, or zero meaning "the whole node". - pub sectors_written: u16, - pub min_key: Bpos, - /// Where the node is, in 512-byte sectors from the start of the device. - pub sector: u64, - pub dev: u8, -} - -impl BtreePtr { - /// Decode the value of a `KEY_TYPE_btree_ptr_v2` key. - pub fn read(val: &Raw<'_>) -> Result { - let seq = val.u64(8)?; - let sectors_written = val.u16(16)?; - let min_key = Bpos::read(val, 20)?; - // The first entry is this reader's pointer; a replica list it does not - // implement is refused where replicas are, in the superblock. - let entry = val.u64(40)?; - if entry & 1 == 0 { - return Err(UpstreamError::Refused("btree pointer's first entry is not a device pointer")); - } - Ok(Self { - seq, - sectors_written, - min_key, - sector: bits(entry, 4, 48), - dev: bits(entry, 48, 56) as u8, - }) - } -} - -/// One validated bset's keys, as a byte range inside the node. -struct Bset { - at: usize, - bytes: usize, -} - -/// A btree node this reader has checked: every bset's checksum matched, and -/// every key in it fits the node. -pub struct Node { - bytes: Vec, - pub format: BkeyFormat, - pub level: u8, - pub btree_id: u32, - pub min_key: Bpos, - pub max_key: Bpos, - bsets: Vec, -} - -impl Node { - /// Read the node `ptr` names and validate it against what the caller - /// expects to find there. - /// - /// `seq`, `btree_id` and `level` are checked against the pointer rather - /// than trusted from the node, because a node the allocator has since - /// reused is otherwise a well-formed node of the wrong tree. - pub fn read( - io: &dyn BlockIO, - sb: &Superblock, - ptr: &BtreePtr, - btree_id: u32, - level: u8, - ) -> Result { - if ptr.dev != sb.dev_idx() { - return Err(UpstreamError::Refused("btree pointer names a device this filesystem has not got")); - } - let node_bytes = sb.btree_node_size(); - let byte_off = ptr - .sector - .checked_mul(SECTOR as u64) - .ok_or(UpstreamError::Refused("btree node offset in bytes overflows"))?; - if byte_off % BLOCK_SIZE as u64 != 0 { - return Err(UpstreamError::Refused("btree node is not block aligned")); - } - let first_block = byte_off / BLOCK_SIZE as u64; - let blocks = node_bytes / BLOCK_SIZE as u64; - if first_block.checked_add(blocks).is_none_or(|end| end > io.block_count()) { - return Err(UpstreamError::Refused("btree node runs past the end of the device")); - } - - let mut bytes = vec![0u8; node_bytes as usize]; - let mut buf = BlockBuf::zeroed(); - for i in 0..blocks as usize { - let block = BlockNum::new(first_block + i as u64); - io.read_block(block, &mut buf).map_err(|e| UpstreamError::Device(block, e))?; - bytes[i * BLOCK_SIZE..(i + 1) * BLOCK_SIZE].copy_from_slice(buf.as_bytes()); - } - - Self::parse(bytes, sb, ptr, btree_id, level) - } - - fn parse( - bytes: Vec, - sb: &Superblock, - ptr: &BtreePtr, - btree_id: u32, - level: u8, - ) -> Result { - let node = Raw::new(&bytes, "btree node ends early"); - let uuid_lo = u64::from_le_bytes(sb.uuid()[..8].try_into().expect("eight uuid bytes")); - if node.u64(NODE_MAGIC)? != uuid_lo ^ BSET_MAGIC { - return Err(UpstreamError::Refused("btree node's magic is not this filesystem's")); - } - let flags = node.u64(NODE_FLAGS)?; - let node_id = bits(flags, 0, 4) | (bits(flags, 9, 25) << 4); - if node_id != btree_id as u64 { - return Err(UpstreamError::Refused("btree node belongs to a different btree")); - } - if bits(flags, 4, 8) != level as u64 { - return Err(UpstreamError::Refused("btree node is at a different level than its pointer")); - } - let min_key = Bpos::read(&node, NODE_MIN_KEY)?; - let max_key = Bpos::read(&node, NODE_MAX_KEY)?; - if min_key > max_key { - return Err(UpstreamError::Refused("btree node's key range runs backwards")); - } - // A node's own low bound is held against its parent's: a node that - // starts below where its pointer says covers keys the parent gave to - // its sibling. Upstream instead *overwrites* - // `min_key` where the pointer sets `BTREE_PTR_RANGE_UPDATED`, so a - // volume that has been through topology repair is refused here. - if min_key != ptr.min_key { - return Err(UpstreamError::Refused("btree node's first key is not the one its pointer names")); - } - let format = BkeyFormat::read(&node, NODE_FORMAT)?; - - let node_sectors = bytes.len() / SECTOR; - let limit = match ptr.sectors_written { - 0 => node_sectors, - n => (n as usize).min(node_sectors), - }; - let first_seq = node.u64(NODE_BSET + BSET_SEQ)?; - if first_seq != ptr.seq { - return Err(UpstreamError::Refused("btree node's sequence number is not its pointer's")); - } - - let mut bsets = Vec::new(); - let mut written = 0usize; - while written < limit { - let (head, bset_at, data_at) = if written == 0 { - (0, NODE_BSET, NODE_BSET + BSET_KEYS) - } else { - let head = written * SECTOR; - (head, head + ENTRY_BSET, head + ENTRY_BSET + BSET_KEYS) - }; - let bset = node.sub(bset_at, BSET_KEYS, "btree node ends before a bset header")?; - if written != 0 && bset.u64(BSET_SEQ)? != first_seq { - break; - } - let bset_flags = bset.u32(BSET_FLAGS)? as u64; - if bits(bset_flags, 4, 5) != 0 { - return Err(UpstreamError::Refused("bset was written big-endian")); - } - if written != 0 && bits(bset_flags, 16, 32) != written as u64 { - return Err(UpstreamError::Refused("bset does not sit where it says it does")); - } - let csum_type = CsumType::from_disk(bits(bset_flags, 0, 4))?; - if csum_type == CsumType::None { - return Err(UpstreamError::Refused("bset carries no checksum")); - } - - let key_bytes = (bset.u16(BSET_U64S)? as usize) - .checked_mul(8) - .ok_or(UpstreamError::Refused("bset's key length overflows"))?; - let end = data_at - .checked_add(key_bytes) - .ok_or(UpstreamError::Refused("bset ends past any address"))?; - if end > bytes.len() { - return Err(UpstreamError::Refused("bset runs past the end of its btree node")); - } - let stored = (node.u64(head)?, node.u64(head + 8)?); - if !csum_type.verify(&bytes[head + 16..end], stored) { - return Err(UpstreamError::Refused("bset checksum does not match its bytes")); - } - - bsets.push(Bset { at: data_at, bytes: key_bytes }); - let sectors = (end - head).div_ceil(BLOCK_SIZE) * (BLOCK_SIZE / SECTOR); - if sectors == 0 { - return Err(UpstreamError::Refused("bset occupies no sectors")); - } - written += sectors; - } - - if bsets.is_empty() { - return Err(UpstreamError::Refused("btree node holds no bset at all")); - } - Ok(Self { bytes, format, level, btree_id, min_key, max_key, bsets }) - } - - /// Every key the node holds, newest bset last. - /// - /// The caller merges: a key in a later bset overrides one at the same - /// position in an earlier one, which is what makes a node a log. - pub fn keys(&self) -> Result, UpstreamError> { - let mut out = Vec::new(); - for (generation, bset) in self.bsets.iter().enumerate() { - let mut at = bset.at; - let end = bset.at + bset.bytes; - while at < end { - let window = Raw::new(&self.bytes[at..end], "key runs past the end of its bset"); - let key = Key::read(&window, &self.format)?; - if key.u64s == 0 { - return Err(UpstreamError::Refused("bset holds a key of no length")); - } - // The node's declared range is what the descent placed it by, - // so a key outside it belongs to a sibling that will never be - // asked for it. - if key.pos < self.min_key || key.pos > self.max_key { - return Err(UpstreamError::Refused("btree node holds a key outside the range it declares")); - } - out.push((generation, key.with_base(at))); - at += key.u64s as usize * 8; - } - } - Ok(out) - } - - /// The bytes of a key's value, given the key [`Node::keys`] handed back. - pub fn value(&self, key: &Key) -> Result, UpstreamError> { - let at = key.base + key.val_at; - let len = key.val_bytes(); - Raw::new(&self.bytes, "key's value runs past its node").sub(at, len, "key's value runs past its node") - } -} - -/// `BCH_JSET_ENTRY_btree_root`. -const JSET_ENTRY_BTREE_ROOT: u8 = 1; -const JSET_ENTRY_HEADER: usize = 8; - -/// The btree roots a cleanly-unmounted filesystem is opened from. -/// -/// A volume whose journal still holds updates has no business being read by -/// this half of the crate: replay is a write, so a dirty volume is refused in -/// [`Superblock::clean`] rather than replayed here. -pub fn clean_roots(sb: &Superblock) -> Result, UpstreamError> { - if !sb.is_clean() { - return Err(UpstreamError::Refused("volume was not unmounted cleanly: it needs journal replay")); - } - let entries = sb.clean()?; - let format = BkeyFormat::unpacked(); - let mut roots = Vec::new(); - let mut at = 0usize; - - while at + JSET_ENTRY_HEADER <= entries.bytes().len() { - let u64s = entries.u16(at)? as usize; - let btree_id = entries.u8(at + 2)? as u32; - let level = entries.u8(at + 3)?; - let kind = entries.u8(at + 4)?; - let bytes = JSET_ENTRY_HEADER - .checked_add(u64s.checked_mul(8).ok_or(SHORT_ENTRY)?) - .ok_or(SHORT_ENTRY)?; - if at + bytes > entries.bytes().len() { - return Err(SHORT_ENTRY); - } - if kind == JSET_ENTRY_BTREE_ROOT && u64s != 0 { - if level >= MAX_DEPTH { - return Err(UpstreamError::Refused("btree root is deeper than the format allows")); - } - let payload = entries.sub(at + JSET_ENTRY_HEADER, u64s * 8, "btree root ends early")?; - let key = Key::read(&payload, &format)?; - if key.kind == super::bkey::TYPE_BTREE_PTR_V2 { - let val = payload.sub(key.val_at, key.val_bytes(), "btree root's value ends early")?; - roots.push((btree_id, level, BtreePtr::read(&val)?)); - } - } - if bytes == 0 { - return Err(SHORT_ENTRY); - } - at += bytes; - } - Ok(roots) -} - -const SHORT_ENTRY: UpstreamError = - UpstreamError::Refused("clean section's entry list ends inside an entry"); - -#[cfg(all(test, feature = "std"))] -mod tests { - use super::*; - use crate::upstream::bkey::{BKEY_U64S, KEY_FORMAT_CURRENT, TYPE_DIRENT}; - use crate::upstream::sb::fixture; - - const NODE_SECTORS: usize = 512; - const BTREE: u32 = 2; - const SEQ: u64 = 0x0123_4567_89ab_cdef; - - fn superblock() -> Superblock { - super::super::sb::read(&fixture::device(&fixture::valid())).expect("the crafted superblock") - } - - /// One unpacked key with an eight-byte value, as a bset holds it. - fn key(offset: u64) -> Vec { - let mut out = vec![0u8; BKEY_U64S * 8 + 8]; - out[0] = BKEY_U64S as u8 + 1; - out[1] = KEY_FORMAT_CURRENT; - out[2] = TYPE_DIRENT; - out[20..24].copy_from_slice(&0u32.to_le_bytes()); - out[24..32].copy_from_slice(&offset.to_le_bytes()); - out[32..40].copy_from_slice(&4096u64.to_le_bytes()); - out - } - - /// A node with one bset holding `offsets.len()` keys, checksummed as - /// upstream checksums the first bset of a node. - fn node(sb: &Superblock, offsets: &[u64]) -> Vec { - let mut bytes = vec![0u8; NODE_SECTORS * SECTOR]; - let uuid_lo = u64::from_le_bytes(sb.uuid()[..8].try_into().unwrap()); - bytes[NODE_MAGIC..NODE_MAGIC + 8].copy_from_slice(&(uuid_lo ^ BSET_MAGIC).to_le_bytes()); - let flags = ((BTREE as u64) & 0xF) | (((BTREE as u64) >> 4) << 9); - bytes[NODE_FLAGS..NODE_FLAGS + 8].copy_from_slice(&flags.to_le_bytes()); - // max_key is SPOS_MAX, so the node's range holds every key below. - bytes[NODE_MAX_KEY..NODE_MAX_KEY + 4].copy_from_slice(&u32::MAX.to_le_bytes()); - bytes[NODE_MAX_KEY + 4..NODE_MAX_KEY + 12].copy_from_slice(&u64::MAX.to_le_bytes()); - bytes[NODE_MAX_KEY + 12..NODE_MAX_KEY + 20].copy_from_slice(&u64::MAX.to_le_bytes()); - // The identity key format, so a key can be written unpacked. - bytes[NODE_FORMAT] = BKEY_U64S as u8; - bytes[NODE_FORMAT + 1] = 6; - bytes[NODE_FORMAT + 2..NODE_FORMAT + 8].copy_from_slice(&[64, 64, 32, 32, 32, 64]); - - let mut keys = Vec::new(); - for offset in offsets { - keys.extend_from_slice(&key(*offset)); - } - bytes[NODE_BSET + BSET_SEQ..NODE_BSET + BSET_SEQ + 8].copy_from_slice(&SEQ.to_le_bytes()); - // Checksum type crc32c, which is what `bcachefs format` writes. - bytes[NODE_BSET + BSET_FLAGS..NODE_BSET + BSET_FLAGS + 4].copy_from_slice(&5u32.to_le_bytes()); - let u64s = (keys.len() / 8) as u16; - bytes[NODE_BSET + BSET_U64S..NODE_BSET + BSET_U64S + 2].copy_from_slice(&u64s.to_le_bytes()); - let at = NODE_BSET + BSET_KEYS; - bytes[at..at + keys.len()].copy_from_slice(&keys); - reseal(&mut bytes); - bytes - } - - fn reseal(bytes: &mut [u8]) { - let u64s = u16::from_le_bytes( - bytes[NODE_BSET + BSET_U64S..NODE_BSET + BSET_U64S + 2].try_into().unwrap(), - ) as usize; - let end = (NODE_BSET + BSET_KEYS + u64s * 8).min(bytes.len()); - let (lo, hi) = CsumType::Crc32c.digest(&bytes[16..end]); - bytes[0..8].copy_from_slice(&lo.to_le_bytes()); - bytes[8..16].copy_from_slice(&hi.to_le_bytes()); - } - - fn ptr() -> BtreePtr { - BtreePtr { - seq: SEQ, - sectors_written: NODE_SECTORS as u16, - min_key: Bpos::MIN, - sector: 0, - dev: 0, - } - } - - fn parse(sb: &Superblock, bytes: Vec) -> Result { - Node::parse(bytes, sb, &ptr(), BTREE, 0) - } - - fn refusal(sb: &Superblock, bytes: Vec) -> &'static str { - match parse(sb, bytes) { - Err(UpstreamError::Refused(why)) => why, - Err(other) => panic!("expected a refusal, got {other:?}"), - Ok(_) => panic!("this btree node should not have been accepted"), - } - } - - /// The builder makes a node this reader accepts, which is what makes each - /// mutation below a measurement of the field it moved. - #[test] - fn the_crafted_node_is_accepted() { - let sb = superblock(); - let node = parse(&sb, node(&sb, &[8, 16, 24])).expect("the crafted node"); - let keys = node.keys().expect("its keys"); - assert_eq!(keys.len(), 3); - assert_eq!(keys[0].1.pos, Bpos::new(4096, 8, 0)); - assert_eq!(keys[2].1.pos, Bpos::new(4096, 24, 0)); - } - - /// **`u64s` is the defect class again**: a bset's key length is a number - /// off the disk that decides how far the key walk goes. - #[test] - fn a_bset_longer_than_its_node_is_refused() { - let sb = superblock(); - let mut bytes = node(&sb, &[8]); - bytes[NODE_BSET + BSET_U64S..NODE_BSET + BSET_U64S + 2] - .copy_from_slice(&u16::MAX.to_le_bytes()); - reseal(&mut bytes); - assert_eq!(refusal(&sb, bytes), "bset runs past the end of its btree node"); - } - - /// A node that is well formed but belongs to another tree, another level - /// or another generation is a node the allocator has since reused. - #[test] - fn a_node_that_is_not_the_one_asked_for_is_refused() { - let sb = superblock(); - - let mut wrong_magic = node(&sb, &[8]); - wrong_magic[NODE_MAGIC] ^= 0xFF; - reseal(&mut wrong_magic); - assert_eq!(refusal(&sb, wrong_magic), "btree node's magic is not this filesystem's"); - - let mut wrong_btree = node(&sb, &[8]); - wrong_btree[NODE_FLAGS] = 7; - reseal(&mut wrong_btree); - assert_eq!(refusal(&sb, wrong_btree), "btree node belongs to a different btree"); - - let mut wrong_level = node(&sb, &[8]); - wrong_level[NODE_FLAGS] |= 1 << 4; - reseal(&mut wrong_level); - assert_eq!(refusal(&sb, wrong_level), "btree node is at a different level than its pointer"); - - let mut wrong_seq = node(&sb, &[8]); - wrong_seq[NODE_BSET + BSET_SEQ] ^= 0xFF; - reseal(&mut wrong_seq); - assert_eq!(refusal(&sb, wrong_seq), "btree node's sequence number is not its pointer's"); - } - - /// A bset whose bytes were changed after it was written is refused, and a - /// bset that declares no checksum at all is refused rather than trusted. - #[test] - fn a_bset_this_filesystem_did_not_write_is_refused() { - let sb = superblock(); - - // Moved and not resealed: the checksum is the only thing that objects. - let mut torn = node(&sb, &[8, 16]); - torn[NODE_BSET + BSET_KEYS + 24] ^= 0xFF; - assert_eq!(refusal(&sb, torn), "bset checksum does not match its bytes"); - - let mut unchecked = node(&sb, &[8]); - unchecked[NODE_BSET + BSET_FLAGS] = 0; - reseal(&mut unchecked); - assert_eq!(refusal(&sb, unchecked), "bset carries no checksum"); - - let mut big_endian = node(&sb, &[8]); - big_endian[NODE_BSET + BSET_FLAGS] = 5 | (1 << 4); - reseal(&mut big_endian); - assert_eq!(refusal(&sb, big_endian), "bset was written big-endian"); - } - - /// A key whose length runs past the bset it is in is refused rather than - /// read out of whatever follows it. - #[test] - fn a_key_longer_than_its_bset_is_refused() { - let sb = superblock(); - // The last key of two, one word longer than the bset has left. - let mut ragged = node(&sb, &[8, 16]); - ragged[NODE_BSET + BSET_KEYS + BKEY_U64S * 8 + 8] += 1; - reseal(&mut ragged); - let parsed = parse(&sb, ragged).expect("the node still parses; its keys do not"); - assert_eq!( - parsed.keys().err(), - Some(UpstreamError::Refused("key runs past the end of its bset")) - ); - - let mut zero_length = node(&sb, &[8, 16]); - zero_length[NODE_BSET + BSET_KEYS] = 0; - reseal(&mut zero_length); - let parsed = parse(&sb, zero_length).expect("the node still parses"); - assert_eq!( - parsed.keys().err(), - Some(UpstreamError::Refused("key is shorter than the format it names")) - ); - } -} diff --git a/lib/bcachefs/src/upstream/raw.rs b/lib/bcachefs/src/upstream/raw.rs deleted file mode 100644 index 15ed830a134..00000000000 --- a/lib/bcachefs/src/upstream/raw.rs +++ /dev/null @@ -1,104 +0,0 @@ -//! Little-endian reads out of a byte slice that came off a disk. -//! -//! Every accessor is bounds-checked and returns a refusal rather than -//! panicking: the slice is untrusted input, and the kernel links this. - -use super::UpstreamError; - -/// A window on disk bytes, named so a refusal can say which structure ran short. -#[derive(Clone, Copy)] -pub struct Raw<'a> { - bytes: &'a [u8], - what: &'static str, -} - -impl<'a> Raw<'a> { - pub fn new(bytes: &'a [u8], what: &'static str) -> Self { - Self { bytes, what } - } - - pub fn bytes(&self) -> &'a [u8] { - self.bytes - } - - fn short(&self) -> UpstreamError { - UpstreamError::Refused(self.what) - } - - pub fn slice(&self, off: usize, len: usize) -> Result<&'a [u8], UpstreamError> { - let end = off.checked_add(len).ok_or_else(|| self.short())?; - self.bytes.get(off..end).ok_or_else(|| self.short()) - } - - /// A window on part of this one, carrying its own name for refusals. - pub fn sub(&self, off: usize, len: usize, what: &'static str) -> Result, UpstreamError> { - Ok(Raw::new(self.slice(off, len)?, what)) - } - - pub fn u8(&self, off: usize) -> Result { - Ok(self.slice(off, 1)?[0]) - } - - pub fn u16(&self, off: usize) -> Result { - let b = self.slice(off, 2)?; - Ok(u16::from_le_bytes([b[0], b[1]])) - } - - pub fn u32(&self, off: usize) -> Result { - let b = self.slice(off, 4)?; - Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]])) - } - - pub fn u64(&self, off: usize) -> Result { - let b = self.slice(off, 8)?; - Ok(u64::from_le_bytes(b.try_into().expect("an 8-byte window"))) - } - - pub fn uuid(&self, off: usize) -> Result<[u8; 16], UpstreamError> { - Ok(self.slice(off, 16)?.try_into().expect("a 16-byte window")) - } -} - -/// The `LE64_BITMASK` accessor: bits `[start, end)` of `word`. -pub fn bits(word: u64, start: u32, end: u32) -> u64 { - debug_assert!(start < end && end <= 64); - (word >> start) & !(!0u64 << (end - start)) -} - -#[cfg(test)] -mod tests { - use super::*; - - /// A structure that ends one byte short refuses by its own name instead of - /// panicking, at every width. - #[test] - fn a_short_window_refuses_by_name() { - let buf = [0u8; 7]; - let raw = Raw::new(&buf, "a truncated thing"); - assert_eq!(raw.u64(0), Err(UpstreamError::Refused("a truncated thing"))); - assert_eq!(raw.u32(4), Err(UpstreamError::Refused("a truncated thing"))); - assert_eq!(raw.u16(6), Err(UpstreamError::Refused("a truncated thing"))); - assert_eq!(raw.u8(7), Err(UpstreamError::Refused("a truncated thing"))); - assert_eq!(raw.uuid(0), Err(UpstreamError::Refused("a truncated thing"))); - assert!(raw.u32(3).is_ok()); - } - - /// An offset near `usize::MAX` must not wrap into a valid range. - #[test] - fn an_offset_that_would_wrap_refuses() { - let buf = [0u8; 16]; - let raw = Raw::new(&buf, "wrapping"); - assert!(raw.slice(usize::MAX, 8).is_err()); - assert!(raw.slice(usize::MAX - 4, 8).is_err()); - } - - #[test] - fn bitmask_matches_upstreams_accessor() { - let flags = 0x8111_1100_8020_0107u64; - assert_eq!(bits(flags, 0, 1), 1); - assert_eq!(bits(flags, 1, 2), 1); - assert_eq!(bits(flags, 12, 28), 512); - assert_eq!(bits(flags, 40, 44), 1); - assert_eq!(bits(flags, 63, 64), 1); - } -} diff --git a/lib/bcachefs/src/upstream/sb.rs b/lib/bcachefs/src/upstream/sb.rs deleted file mode 100644 index ee7327da3f0..00000000000 --- a/lib/bcachefs/src/upstream/sb.rs +++ /dev/null @@ -1,781 +0,0 @@ -//! The superblock: where it is, what makes it this device's, and the options -//! and feature bits that decide whether a mount may proceed. - -use alloc::vec; -use alloc::vec::Vec; - -use crate::block_io::{BlockBuf, BlockIO, BlockNum, BLOCK_SIZE}; - -use super::csum::CsumType; -use super::raw::{bits, Raw}; -use super::UpstreamError; - -pub const SECTOR: usize = 512; -/// `BCH_SB_SECTOR` and `BCH_SB_LAYOUT_SECTOR`. -pub const SB_SECTOR: u64 = 8; -pub const LAYOUT_SECTOR: u64 = 7; -/// `BCHFS_MAGIC`, as the sixteen bytes it occupies on disk. -pub const BCHFS_MAGIC: [u8; 16] = [ - 0xc6, 0x85, 0x73, 0xf6, 0x66, 0xce, 0x90, 0xa9, 0xd9, 0x6a, 0x60, 0xcf, 0x80, 0x3d, 0xf7, 0xef, -]; -pub const BSET_MAGIC: u64 = 0x9013_5c78_b99e_07f5; - -/// `bcachefs_metadata_version_max - 1` at the pinned commit: what `bcachefs -/// format` writes today, and the only version this reader claims to know. -pub const VERSION_CURRENT: u16 = 1063; -/// `bcachefs_metadata_version_major_minor`: below it the format predates the -/// single major.minor scheme and nothing here applies. -pub const VERSION_MIN_SUPPORTED: u16 = 1024; - -const SB_VERSION: usize = 16; -const SB_VERSION_MIN: usize = 18; -const SB_MAGIC: usize = 24; -const SB_UUID: usize = 40; -const SB_OFFSET: usize = 104; -const SB_SEQ: usize = 112; -const SB_BLOCK_SIZE: usize = 120; -const SB_DEV_IDX: usize = 122; -const SB_NR_DEVICES: usize = 123; -const SB_U64S: usize = 124; -const SB_FLAGS: usize = 144; -const SB_FEATURES: usize = 208; -const SB_LAYOUT: usize = 240; -/// `offsetof(struct bch_sb, _data)`: the fixed header, layout included. -const SB_FIELDS_START: usize = 752; - -const LAYOUT_BYTES: usize = 512; -const LAYOUT_MAX_SUPERBLOCKS: usize = 61; -/// `BCH_SB_LAYOUT_SIZE_BITS_MAX`: 512 << 16 is 32 MB, the largest a superblock -/// may reserve and therefore the largest this reader will ever allocate for one. -const LAYOUT_SIZE_BITS_MAX: u8 = 16; - -/// Section types this reader looks for; the rest are stepped over. -const FIELD_MEMBERS_V1: u32 = 1; -const FIELD_CLEAN: u32 = 6; -const FIELD_MEMBERS_V2: u32 = 11; -const FIELD_EXTENT_TYPE_U64S: u32 = 16; -/// `bch_sb_field_clean`'s flags, clocks and `journal_seq`, before its entries. -const CLEAN_ENTRIES_AT: usize = 16; - -/// Feature bits `BCH_SB_FEATURES()` numbers, in the two words `features[2]`. -const FEATURE_LZ4: u32 = 0; -const FEATURE_GZIP: u32 = 1; -const FEATURE_ZSTD: u32 = 2; -const FEATURE_EC: u32 = 4; -const FEATURE_INCOMPRESSIBLE: u32 = 10; -const FEATURE_CASEFOLDING: u32 = 20; -const FEATURE_NO_DEFAULT_SB: u32 = 23; - -/// The features a volume `bcachefs format` writes carries, and which this -/// reader therefore has to tolerate. A bit outside this set is refused by -/// number, because a feature nobody has read is a feature nobody can honour. -const FEATURES_KNOWN: u64 = (1 << 5) // journal_seq_blacklist_v3 - | (1 << 6) // reflink - | (1 << 7) // new_siphash - | (1 << 8) // inline_data - | (1 << 9) // new_extent_overwrite - | (1 << 11) // btree_ptr_v2 - | (1 << 12) // extents_above_btree_updates - | (1 << 13) // btree_updates_journalled - | (1 << 14) // reflink_inline_data - | (1 << 15) // new_varint - | (1 << 16) // journal_no_flush - | (1 << 17) // alloc_v2 - | (1 << 18) // extents_across_btree_nodes - | (1 << 19) // incompat_version_field - | (1 << 21) // no_alloc_info - | (1 << 22) // small_image - | (1 << 23); // no_default_sb, refused by name above rather than honoured - -/// One member device, as the members section describes it. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Member { - pub uuid: [u8; 16], - pub nbuckets: u64, - /// Sectors, and the unit every bucket index is multiplied by. - pub bucket_size: u16, - pub first_bucket: u16, -} - -/// A superblock this reader has agreed to mount from. -pub struct Superblock { - bytes: Vec, -} - -impl Superblock { - fn raw(&self) -> Raw<'_> { - Raw::new(&self.bytes, "superblock ends early") - } - - pub fn version(&self) -> u16 { - self.raw().u16(SB_VERSION).unwrap_or(0) - } - - pub fn version_min(&self) -> u16 { - self.raw().u16(SB_VERSION_MIN).unwrap_or(0) - } - - /// The immutable filesystem UUID; the bset and jset magics are derived from - /// its first eight bytes. - pub fn uuid(&self) -> [u8; 16] { - self.raw().uuid(SB_UUID).unwrap_or([0; 16]) - } - - /// Filesystem block size in bytes. - pub fn block_size(&self) -> u32 { - self.raw().u16(SB_BLOCK_SIZE).unwrap_or(0) as u32 * SECTOR as u32 - } - - pub fn dev_idx(&self) -> u8 { - self.raw().u8(SB_DEV_IDX).unwrap_or(0) - } - - pub fn nr_devices(&self) -> u8 { - self.raw().u8(SB_NR_DEVICES).unwrap_or(0) - } - - fn flags(&self, word: usize) -> u64 { - self.raw().u64(SB_FLAGS + word * 8).unwrap_or(0) - } - - fn features(&self, word: usize) -> u64 { - self.raw().u64(SB_FEATURES + word * 8).unwrap_or(0) - } - - pub fn is_clean(&self) -> bool { - bits(self.flags(0), 1, 2) == 1 - } - - /// The btree node size in bytes: every node read is exactly this long. - pub fn btree_node_size(&self) -> u64 { - bits(self.flags(0), 12, 28) * SECTOR as u64 - } - - pub fn metadata_csum(&self) -> Result { - csum_from_opt(bits(self.flags(0), 40, 44)) - } - - pub fn data_csum(&self) -> Result { - csum_from_opt(bits(self.flags(0), 44, 48)) - } - - /// Walk the variable-length sections, handing each `(type, payload)`. - /// - /// A section whose `u64s` does not advance is the end of the list rather - /// than a loop: upstream's terminator is a zero-length field. - fn sections(&self) -> Sections<'_> { - Sections { raw: self.raw(), at: SB_FIELDS_START } - } - - fn section(&self, want: u32) -> Option> { - self.sections().find(|(ty, _)| *ty == want).map(|(_, payload)| payload) - } - - /// This device's member entry, which is where a bucket becomes a sector. - pub fn member(&self, idx: u8) -> Result { - let (section, first, stride) = if let Some(v2) = self.section(FIELD_MEMBERS_V2) { - let stride = v2.u16(0)? as usize; - (v2, 8, stride) - } else if let Some(v1) = self.section(FIELD_MEMBERS_V1) { - (v1, 0, MEMBER_V1_BYTES) - } else { - return Err(UpstreamError::Refused("superblock has no members section")); - }; - if stride < MEMBER_MIN_BYTES { - return Err(UpstreamError::Refused("members section entry is too small to be one")); - } - let at = first + (idx as usize) - .checked_mul(stride) - .ok_or(UpstreamError::Refused("member index times entry size overflows"))?; - let m = section.sub(at, stride, "members section ends before this device")?; - Ok(Member { - uuid: m.uuid(0)?, - nbuckets: m.u64(16)?, - first_bucket: m.u16(24)?, - bucket_size: m.u16(26)?, - }) - } - - /// The clean section's journal entries, which carry the btree roots a - /// cleanly-unmounted filesystem is opened from. - pub fn clean(&self) -> Result, UpstreamError> { - let clean = self - .section(FIELD_CLEAN) - .ok_or(UpstreamError::Refused("volume has no clean section: it needs journal replay"))?; - // A section shorter than its own fixed part is a subtraction that - // panics under the kernel's overflow checks, not a short read. - let entries = clean - .bytes() - .len() - .checked_sub(CLEAN_ENTRIES_AT) - .ok_or(UpstreamError::Refused("clean section is shorter than its own header"))?; - clean.sub(CLEAN_ENTRIES_AT, entries, "clean section ends before its entries") - } - - /// How many u64s each `bch_extent_entry` type occupies. - /// - /// **A size that is wrong by one word makes the walker step onto a pointer - /// and serve arbitrary device blocks as file contents.** The sizes for the - /// types this reader decodes are compiled in, because they are what its - /// own field offsets assume; the superblock's `extent_type_u64s` section - /// supplies the rest, so an entry type written after this was is stepped - /// over rather than guessed at. A section that disagrees with a compiled-in - /// size is refused: one of the two is describing another format. - pub fn extent_entry_u64s(&self) -> Result<[u8; EXTENT_TYPES_MAX], UpstreamError> { - let section = self.section(FIELD_EXTENT_TYPE_U64S).ok_or(UpstreamError::Refused( - "volume has no extent_type_u64s section: this reader cannot size an extent's entries", - ))?; - let mut sizes = EXTENT_ENTRY_U64S_KNOWN; - for (kind, size) in sizes.iter_mut().enumerate() { - let Ok(stated) = section.u8(kind) else { break }; - if stated == 0 { - break; - } - if kind < EXTENT_ENTRY_TYPES_KNOWN && stated != *size { - return Err(UpstreamError::Refused( - "volume sizes an extent entry this reader decodes differently than it does", - )); - } - *size = stated; - } - Ok(sizes) - } - - /// Refuse, by name, every format feature this reader does not implement. - /// - /// A feature bit silently ignored is a mount that returns the wrong bytes, - /// so the unknown half of `features[0]` is refused by number too. - fn check_supported(&self, device_sectors: u64) -> Result<(), UpstreamError> { - let refuse = |why| Err(UpstreamError::Refused(why)); - - if self.version() > VERSION_CURRENT { - return refuse("volume is a newer metadata version than this reader implements"); - } - if self.version_min() < VERSION_MIN_SUPPORTED { - return refuse("volume still holds metadata older than version 1.0"); - } - if bits(self.flags(0), 62, 63) != 0 { - return refuse("volume was written big-endian"); - } - if self.block_size() as usize != BLOCK_SIZE { - return refuse("volume's block size is not the 4096 bytes this crate reads"); - } - if !self.btree_node_size().is_multiple_of(BLOCK_SIZE as u64) || self.btree_node_size() == 0 { - return refuse("btree node size is not a whole number of blocks"); - } - if self.nr_devices() != 1 { - return refuse("volume has more than one member device"); - } - if self.dev_idx() != 0 { - return refuse("volume's superblock is not device 0 of its filesystem"); - } - if bits(self.flags(1), 10, 14) != 0 { - return refuse("volume is encrypted"); - } - if bits(self.flags(1), 4, 8) != 0 || bits(self.flags(4), 56, 60) != 0 { - return refuse("volume has compression enabled"); - } - if bits(self.flags(2), 0, 4) != 0 || bits(self.flags(4), 60, 64) != 0 { - return refuse("volume has background compression enabled"); - } - // Wanted and required, both kinds: a zero is as much "not one replica" - // as a two is, and this reader reads exactly one copy of everything. - for (word, start, end) in [(0, 48, 52), (0, 52, 56), (1, 20, 24), (1, 24, 28)] { - if bits(self.flags(word), start, end) != 1 { - return refuse("volume does not ask for exactly one replica of everything"); - } - } - // A volume that checksums nothing would have this reader verifying - // every extent against a checksum nobody computed. - if self.metadata_csum()? == CsumType::None || self.data_csum()? == CsumType::None { - return refuse("volume checksums its metadata or its data with nothing"); - } - if bits(self.flags(3), 0, 16) != 0 { - return refuse("volume has erasure coding enabled"); - } - if bits(self.flags(6), 22, 23) != 0 { - return refuse("volume is casefolded"); - } - if bits(self.flags(3), 63, 64) != 0 { - return refuse("volume is a multi-device filesystem"); - } - for (bit, why) in [ - (FEATURE_LZ4, "volume carries lz4-compressed data"), - (FEATURE_GZIP, "volume carries gzip-compressed data"), - (FEATURE_ZSTD, "volume carries zstd-compressed data"), - (FEATURE_INCOMPRESSIBLE, "volume carries compression metadata"), - (FEATURE_EC, "volume carries erasure-coded stripes"), - (FEATURE_CASEFOLDING, "volume carries casefolded directories"), - (FEATURE_NO_DEFAULT_SB, "volume keeps no superblock at the sector this reader looks at"), - ] { - if self.features(0) & (1u64 << bit) != 0 { - return refuse(why); - } - } - if self.features(0) & !FEATURES_KNOWN != 0 { - return refuse("volume sets a feature bit this reader has never read"); - } - if self.features(1) != 0 { - return refuse("volume sets a feature bit past the first word"); - } - - let member = self.member(self.dev_idx())?; - if member.bucket_size == 0 { - return refuse("member's bucket size is zero"); - } - let sectors = member - .nbuckets - .checked_mul(member.bucket_size as u64) - .ok_or(UpstreamError::Refused("member's bucket count times bucket size overflows"))?; - if sectors > device_sectors { - return refuse("member claims more sectors than the device has"); - } - if member.first_bucket as u64 >= member.nbuckets { - return refuse("member's first bucket is past its last"); - } - Ok(()) - } -} - -/// The largest entry type this reader will size, one past `BCH_EXTENT_ENTRY_MAX` -/// so the superblock can name a type written after this code was. -pub const EXTENT_TYPES_MAX: usize = 16; -/// `BCH_EXTENT_ENTRY_MAX`: how many of those this reader has read the layout of. -pub const EXTENT_ENTRY_TYPES_KNOWN: usize = 9; -/// Each entry type's length in u64s, the `sizeof` of its format struct at -/// the pinned commit: ptr, crc32, crc64, crc128, stripe_ptr, rebalance_v1, -/// flags, reconcile, reconcile_bp. The last four are each one 64-bit -/// bitfield. -const EXTENT_ENTRY_U64S_KNOWN: [u8; EXTENT_TYPES_MAX] = - [1, 1, 2, 3, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0]; - -#[cfg(test)] -pub(crate) const EXTENT_ENTRY_U64S_KNOWN_FOR_TESTS: [u8; EXTENT_TYPES_MAX] = EXTENT_ENTRY_U64S_KNOWN; - -const MEMBER_V1_BYTES: usize = 56; -/// Everything before `last_mount`: the fields this reader takes. -const MEMBER_MIN_BYTES: usize = 32; - -fn csum_from_opt(opt: u64) -> Result { - match opt { - 0 => Ok(CsumType::None), - 1 => Ok(CsumType::Crc32c), - 2 => Ok(CsumType::Crc64), - 3 => Ok(CsumType::Xxhash), - _ => Err(UpstreamError::Refused("checksum option is not one this format defines")), - } -} - -struct Sections<'a> { - raw: Raw<'a>, - at: usize, -} - -impl<'a> Iterator for Sections<'a> { - type Item = (u32, Raw<'a>); - - fn next(&mut self) -> Option { - let u64s = self.raw.u32(self.at).ok()? as usize; - let ty = self.raw.u32(self.at + 4).ok()?; - if u64s == 0 { - return None; - } - let bytes = u64s.checked_mul(8)?; - let payload = self.raw.sub(self.at + 8, bytes.checked_sub(8)?, "section ends early").ok()?; - self.at = self.at.checked_add(bytes)?; - Some((ty, payload)) - } -} - -/// Read `len` bytes at `byte_off` through a 4096-byte block device. -/// -/// The superblock lives at sector 8 and its backups at sectors the layout -/// chooses, none of which the block size has any say over. -fn read_bytes(io: &dyn BlockIO, byte_off: u64, len: usize) -> Result, UpstreamError> { - let first = byte_off / BLOCK_SIZE as u64; - let skip = (byte_off % BLOCK_SIZE as u64) as usize; - let blocks = (skip + len).div_ceil(BLOCK_SIZE); - let last = first - .checked_add(blocks as u64) - .ok_or(UpstreamError::Refused("a read past the end of any device"))?; - if last > io.block_count() { - return Err(UpstreamError::Refused("structure runs past the end of the device")); - } - - let mut out = vec![0u8; blocks * BLOCK_SIZE]; - let mut buf = BlockBuf::zeroed(); - for i in 0..blocks { - let block = BlockNum::new(first + i as u64); - io.read_block(block, &mut buf).map_err(|e| UpstreamError::Device(block, e))?; - out[i * BLOCK_SIZE..(i + 1) * BLOCK_SIZE].copy_from_slice(buf.as_bytes()); - } - out.drain(..skip); - out.truncate(len); - Ok(out) -} - -/// Read and validate this device's superblock. -/// -/// The primary at sector 8 first; a copy the layout names only when the primary -/// does not parse, because a device that refused the read is a device that is -/// not answering and not a superblock that is wrong. -pub fn read(io: &dyn BlockIO) -> Result { - let device_sectors = io - .block_count() - .checked_mul((BLOCK_SIZE / SECTOR) as u64) - .ok_or(UpstreamError::Refused("device size in sectors overflows"))?; - - let primary = read_one(io, SB_SECTOR, device_sectors); - let Err(primary_err) = primary else { return primary }; - - let layout = read_bytes(io, LAYOUT_SECTOR * SECTOR as u64, LAYOUT_BYTES)?; - for offset in layout_offsets(&layout)? { - if offset == SB_SECTOR { - continue; - } - if let Ok(sb) = read_one(io, offset, device_sectors) { - return Ok(sb); - } - } - Err(primary_err) -} - -/// The backup superblock sectors the standalone layout at sector 7 names. -fn layout_offsets(layout: &[u8]) -> Result, UpstreamError> { - let raw = Raw::new(layout, "superblock layout ends early"); - if raw.uuid(0)? != BCHFS_MAGIC { - return Err(UpstreamError::Refused("no bcachefs superblock layout at sector 7")); - } - if raw.u8(16)? != 0 { - return Err(UpstreamError::Refused("superblock layout is a type this reader has not read")); - } - if raw.u8(17)? > LAYOUT_SIZE_BITS_MAX { - return Err(UpstreamError::Refused("superblock layout reserves more than 32 MB")); - } - let nr = raw.u8(18)? as usize; - if nr == 0 || nr > LAYOUT_MAX_SUPERBLOCKS { - return Err(UpstreamError::Refused("superblock layout names an impossible number of copies")); - } - (0..nr).map(|i| raw.u64(24 + i * 8)).collect() -} - -fn read_one(io: &dyn BlockIO, sector: u64, device_sectors: u64) -> Result { - let byte_off = sector - .checked_mul(SECTOR as u64) - .ok_or(UpstreamError::Refused("superblock offset in bytes overflows"))?; - - // The header alone first: `u64s` decides how much more to read, and it is a - // number the disk chose, so it is bounded before it sizes an allocation. - let head = read_bytes(io, byte_off, SB_FIELDS_START)?; - let raw = Raw::new(&head, "superblock header ends early"); - if raw.uuid(SB_MAGIC)? != BCHFS_MAGIC { - return Err(UpstreamError::Refused("no bcachefs superblock here")); - } - if raw.u64(SB_OFFSET)? != sector { - return Err(UpstreamError::Refused("superblock names a sector other than its own")); - } - - let size_bits = raw.u8(SB_LAYOUT + 17)?; - if size_bits > LAYOUT_SIZE_BITS_MAX { - return Err(UpstreamError::Refused("superblock reserves more than 32 MB")); - } - let reserved = (SECTOR as u64) << size_bits; - let u64s = raw.u32(SB_U64S)? as u64; - let bytes = SB_FIELDS_START as u64 + u64s * 8; - if bytes > reserved { - return Err(UpstreamError::Refused("superblock is longer than the space it reserves")); - } - if byte_off / SECTOR as u64 + bytes.div_ceil(SECTOR as u64) > device_sectors { - return Err(UpstreamError::Refused("superblock runs past the end of the device")); - } - - let bytes = read_bytes(io, byte_off, bytes as usize)?; - let raw = Raw::new(&bytes, "superblock ends early"); - let csum_type = CsumType::from_disk(bits(raw.u64(SB_FLAGS)?, 2, 8))?; - let stored = (raw.u64(0)?, raw.u64(8)?); - if !csum_type.verify(&bytes[SB_VERSION..], stored) { - return Err(UpstreamError::Refused("superblock checksum does not match its bytes")); - } - - let sb = Superblock { bytes }; - sb.check_supported(device_sectors)?; - Ok(sb) -} - -#[cfg(all(test, feature = "std"))] -pub(crate) mod fixture { - use super::*; - use crate::block_io::VecBlockIO; - - pub(crate) const DEVICE_BLOCKS: u64 = 512; - const NBUCKETS: u64 = 512; - const BUCKET_SECTORS: u16 = 8; - - /// The smallest superblock this reader accepts, as bytes, so a test can - /// move one field and name what that costs. - pub(crate) fn valid() -> Vec { - let members = { - let mut section = vec![0u8; 48]; - section[0..4].copy_from_slice(&6u32.to_le_bytes()); - section[4..8].copy_from_slice(&FIELD_MEMBERS_V2.to_le_bytes()); - section[8..10].copy_from_slice(&32u16.to_le_bytes()); - section[32..40].copy_from_slice(&NBUCKETS.to_le_bytes()); - section[40..42].copy_from_slice(&0u16.to_le_bytes()); - section[42..44].copy_from_slice(&BUCKET_SECTORS.to_le_bytes()); - section - }; - let clean = { - let mut section = vec![0u8; 24]; - section[0..4].copy_from_slice(&3u32.to_le_bytes()); - section[4..8].copy_from_slice(&FIELD_CLEAN.to_le_bytes()); - section - }; - let extent_types = { - let mut section = vec![0u8; 24]; - section[0..4].copy_from_slice(&3u32.to_le_bytes()); - section[4..8].copy_from_slice(&FIELD_EXTENT_TYPE_U64S.to_le_bytes()); - section[8..8 + EXTENT_ENTRY_TYPES_KNOWN] - .copy_from_slice(&EXTENT_ENTRY_U64S_KNOWN[..EXTENT_ENTRY_TYPES_KNOWN]); - section - }; - let sections = [members.as_slice(), clean.as_slice(), extent_types.as_slice()].concat(); - - let mut sb = vec![0u8; SB_FIELDS_START + sections.len()]; - sb[SB_VERSION..SB_VERSION + 2].copy_from_slice(&VERSION_CURRENT.to_le_bytes()); - sb[SB_VERSION_MIN..SB_VERSION_MIN + 2].copy_from_slice(&VERSION_CURRENT.to_le_bytes()); - sb[SB_MAGIC..SB_MAGIC + 16].copy_from_slice(&BCHFS_MAGIC); - sb[SB_OFFSET..SB_OFFSET + 8].copy_from_slice(&SB_SECTOR.to_le_bytes()); - sb[SB_BLOCK_SIZE..SB_BLOCK_SIZE + 2].copy_from_slice(&8u16.to_le_bytes()); - sb[SB_NR_DEVICES] = 1; - let u64s = (sections.len() / 8) as u32; - sb[SB_U64S..SB_U64S + 4].copy_from_slice(&u64s.to_le_bytes()); - - // initialized, clean, csum type crc32c_nonzero, 256 KB nodes, one - // replica of each kind wanted, each checksummed crc32c. - let flags0 = 1 | (1 << 1) | (1 << 2) | (512u64 << 12) | (1 << 40) | (1 << 44) - | (1 << 48) | (1 << 52); - sb[SB_FLAGS..SB_FLAGS + 8].copy_from_slice(&flags0.to_le_bytes()); - // One replica of each kind required. - let flags1 = (1u64 << 20) | (1 << 24); - sb[SB_FLAGS + 8..SB_FLAGS + 16].copy_from_slice(&flags1.to_le_bytes()); - - sb[SB_LAYOUT..SB_LAYOUT + 16].copy_from_slice(&BCHFS_MAGIC); - sb[SB_LAYOUT + 17] = 11; - sb[SB_LAYOUT + 18] = 1; - sb[SB_LAYOUT + 24..SB_LAYOUT + 32].copy_from_slice(&SB_SECTOR.to_le_bytes()); - - sb[SB_FIELDS_START..].copy_from_slice(§ions); - reseal(&mut sb); - sb - } - - /// Recompute the checksum after a field has been moved, so a test measures - /// the field's refusal rather than the checksum's. - pub(crate) fn reseal(sb: &mut [u8]) { - let (lo, hi) = CsumType::Crc32cNonzero.digest(&sb[SB_VERSION..]); - sb[0..8].copy_from_slice(&lo.to_le_bytes()); - sb[8..16].copy_from_slice(&hi.to_le_bytes()); - } - - pub(crate) fn device(sb: &[u8]) -> VecBlockIO { - let mut image = vec![0u8; DEVICE_BLOCKS as usize * BLOCK_SIZE]; - let layout_at = LAYOUT_SECTOR as usize * SECTOR; - image[layout_at..layout_at + 16].copy_from_slice(&BCHFS_MAGIC); - image[layout_at + 17] = 11; - image[layout_at + 18] = 1; - image[layout_at + 24..layout_at + 32].copy_from_slice(&SB_SECTOR.to_le_bytes()); - let at = SB_SECTOR as usize * SECTOR; - image[at..at + sb.len()].copy_from_slice(sb); - VecBlockIO::from_vec(image) - } - - /// One field of a superblock, moved. - type Mutation = alloc::boxed::Box)>; - - pub(crate) fn refusal(sb: &[u8]) -> &'static str { - match read(&device(sb)) { - Err(UpstreamError::Refused(why)) => why, - Err(other) => panic!("expected a refusal, got {other:?}"), - Ok(_) => panic!("this superblock should not have been accepted"), - } - } - - /// The builder makes something this reader accepts, which is what makes - /// every mutation below a measurement of the field it moved. - #[test] - fn the_crafted_superblock_is_accepted() { - let sb = read(&device(&valid())).expect("the crafted superblock"); - assert_eq!(sb.version(), VERSION_CURRENT); - assert!(sb.is_clean()); - assert_eq!(sb.block_size(), BLOCK_SIZE as u32); - assert_eq!(sb.btree_node_size(), 256 * 1024); - assert_eq!(sb.metadata_csum(), Ok(CsumType::Crc32c)); - assert_eq!(sb.member(0).expect("the member").nbuckets, NBUCKETS); - } - - /// **`u64s` is the defect class this crate's `alloc_probe` exists for**: a - /// length off the disk sizes the buffer the superblock is read into. A - /// superblock claiming four gigabytes is refused, and the refusal costs - /// nothing like four gigabytes. - #[test] - fn a_huge_u64s_is_refused_without_allocating_it() { - let mut sb = valid(); - sb[SB_U64S..SB_U64S + 4].copy_from_slice(&(1u32 << 29).to_le_bytes()); - reseal(&mut sb); - - let io = device(&sb); - let _ = crate::alloc_probe::take_peak(); - let err = read(&io).err(); - let peak = crate::alloc_probe::take_peak(); - - assert_eq!(err, Some(UpstreamError::Refused("superblock is longer than the space it reserves"))); - assert!(peak < 1 << 20, "refusing a 4 GB superblock asked the allocator for {peak} bytes"); - } - - /// Each of these is a sentence a log line can print, and each is reached - /// only because the checksum was recomputed over the moved field. - #[test] - fn every_unsupported_format_feature_is_refused_by_name() { - let mut cases: Vec<(&str, Mutation)> = Vec::new(); - let flag = |word: usize, shift: u32, value: u64| -> Mutation { - Box::new(move |sb: &mut Vec| { - let at = SB_FLAGS + word * 8; - let mut bits = u64::from_le_bytes(sb[at..at + 8].try_into().unwrap()); - bits |= value << shift; - sb[at..at + 8].copy_from_slice(&bits.to_le_bytes()); - }) - }; - let feature = |bit: u32| -> Mutation { - Box::new(move |sb: &mut Vec| { - let mut bits = u64::from_le_bytes(sb[SB_FEATURES..SB_FEATURES + 8].try_into().unwrap()); - bits |= 1u64 << bit; - sb[SB_FEATURES..SB_FEATURES + 8].copy_from_slice(&bits.to_le_bytes()); - }) - }; - cases.push(("volume is encrypted", flag(1, 10, 1))); - cases.push(("volume has compression enabled", flag(1, 4, 3))); - cases.push(("volume has background compression enabled", flag(2, 0, 3))); - cases.push(("volume does not ask for exactly one replica of everything", flag(0, 53, 1))); - cases.push(("volume has erasure coding enabled", flag(3, 0, 1))); - cases.push(("volume is casefolded", flag(6, 22, 1))); - cases.push(("volume is a multi-device filesystem", flag(3, 63, 1))); - cases.push(("volume was written big-endian", flag(0, 62, 1))); - cases.push(("volume carries lz4-compressed data", feature(FEATURE_LZ4))); - cases.push(("volume carries gzip-compressed data", feature(FEATURE_GZIP))); - cases.push(("volume carries zstd-compressed data", feature(FEATURE_ZSTD))); - cases.push(("volume carries erasure-coded stripes", feature(FEATURE_EC))); - cases.push(("volume carries casefolded directories", feature(FEATURE_CASEFOLDING))); - cases.push(("volume sets a feature bit this reader has never read", feature(24))); - cases.push(( - "volume is a newer metadata version than this reader implements", - Box::new(|sb: &mut Vec| { - sb[SB_VERSION..SB_VERSION + 2].copy_from_slice(&(VERSION_CURRENT + 1).to_le_bytes()) - }), - )); - cases.push(( - "volume has more than one member device", - Box::new(|sb: &mut Vec| sb[SB_NR_DEVICES] = 2), - )); - cases.push(( - "volume's block size is not the 4096 bytes this crate reads", - Box::new(|sb: &mut Vec| sb[SB_BLOCK_SIZE..SB_BLOCK_SIZE + 2].copy_from_slice(&1u16.to_le_bytes())), - )); - - for (want, mutate) in cases { - let mut sb = valid(); - mutate(&mut sb); - reseal(&mut sb); - assert_eq!(refusal(&sb), want, "the mutation for {want:?} was refused for another reason"); - } - } - - /// A superblock's own bytes have to say where it is and hash to what it - /// claims, or a copy of somebody else's disk mounts here. - #[test] - fn a_superblock_that_is_not_this_ones_is_refused() { - let mut wrong_place = valid(); - wrong_place[SB_OFFSET..SB_OFFSET + 8].copy_from_slice(&9u64.to_le_bytes()); - reseal(&mut wrong_place); - assert_eq!(refusal(&wrong_place), "superblock names a sector other than its own"); - - let mut no_magic = valid(); - no_magic[SB_MAGIC] ^= 0xFF; - reseal(&mut no_magic); - assert_eq!(refusal(&no_magic), "no bcachefs superblock here"); - - // Moved and *not* resealed: the checksum is what refuses it. - let mut torn = valid(); - torn[SB_SEQ] ^= 0xFF; - assert_eq!(refusal(&torn), "superblock checksum does not match its bytes"); - } - - /// A member that describes a device other than this one is where an - /// unchecked bucket count becomes a read past the end of the disk. - #[test] - fn a_member_bigger_than_its_device_is_refused() { - let sections = SB_FIELDS_START; - let mut too_big = valid(); - too_big[sections + 32..sections + 40].copy_from_slice(&u64::MAX.to_le_bytes()); - reseal(&mut too_big); - assert_eq!(refusal(&too_big), "member's bucket count times bucket size overflows"); - - let mut over = valid(); - over[sections + 32..sections + 40].copy_from_slice(&(NBUCKETS * 64).to_le_bytes()); - reseal(&mut over); - assert_eq!(refusal(&over), "member claims more sectors than the device has"); - - let mut no_buckets = valid(); - no_buckets[sections + 42..sections + 44].copy_from_slice(&0u16.to_le_bytes()); - reseal(&mut no_buckets); - assert_eq!(refusal(&no_buckets), "member's bucket size is zero"); - } - - /// A clean section shorter than its own fixed part reaches the read path - /// through every mount, and the subtraction that finds its entries panics - /// under the overflow checks the kernel and root are built with. - #[test] - fn a_clean_section_shorter_than_its_header_is_refused() { - let at = SB_FIELDS_START + 48; - for u64s in [1u32, 2] { - let mut short = valid(); - short[at..at + 4].copy_from_slice(&u64s.to_le_bytes()); - reseal(&mut short); - let sb = read(&device(&short)).expect("the superblock itself still parses"); - assert_eq!( - sb.clean().err(), - Some(UpstreamError::Refused("clean section is shorter than its own header")), - "a clean section of {u64s} word(s) was not refused" - ); - } - } - - /// The two readers in this crate answer a bcachefs superblock differently: - /// the interim ToyOS format's refuses the magic, and this one accepts it. - #[test] - fn the_interim_reader_rejects_bcachefs_s_magic() { - let io = device(&valid()); - let refused = crate::Superblock::read(&io).expect_err("the interim reader must refuse this"); - assert!( - matches!(refused, crate::FsError::BadMagic { expected, .. } if expected == *b"BCFS"), - "the interim reader answered {refused:?} rather than refusing the magic" - ); - // And the reverse: the upstream reader is what does accept it. - assert!(read(&io).is_ok(), "the upstream reader must accept what it wrote the fixture for"); - } - - /// A volume that was not unmounted cleanly is refused by name, because - /// replay is a write and this half of the crate does not do writes. - #[test] - fn a_volume_without_a_clean_section_is_refused_by_name() { - let mut dirty = valid(); - // Turn the clean section into a type this reader steps over. - let at = SB_FIELDS_START + 48; - dirty[at + 4..at + 8].copy_from_slice(&99u32.to_le_bytes()); - reseal(&mut dirty); - let sb = read(&device(&dirty)).expect("a superblock with no clean section still parses"); - assert_eq!( - sb.clean().err(), - Some(UpstreamError::Refused("volume has no clean section: it needs journal replay")) - ); - } -} diff --git a/lib/bcachefs/tests/fixtures/encrypted-wide-macs.img.gz b/lib/bcachefs/tests/fixtures/encrypted-wide-macs.img.gz new file mode 100644 index 00000000000..2fd4e87442c Binary files /dev/null and b/lib/bcachefs/tests/fixtures/encrypted-wide-macs.img.gz differ diff --git a/lib/bcachefs/tests/fixtures/encrypted.img.gz b/lib/bcachefs/tests/fixtures/encrypted.img.gz new file mode 100644 index 00000000000..769e490842a Binary files /dev/null and b/lib/bcachefs/tests/fixtures/encrypted.img.gz differ diff --git a/lib/bcachefs/tests/upstream_fixture.rs b/lib/bcachefs/tests/upstream_fixture.rs deleted file mode 100644 index ecb6bbb1866..00000000000 --- a/lib/bcachefs/tests/upstream_fixture.rs +++ /dev/null @@ -1,168 +0,0 @@ -//! The upstream read path, judged against volumes upstream's own tools wrote. -//! -//! **The bytes are the judge, and nothing fetches them.** `bcachefs-tools` -//! formatted and filled each fixture under `fixtures/`, and `bcachefs fsck` -//! called it clean; `NOTICE` records which release did it and the commands -//! that were run. What is committed is the result, so this suite depends on -//! Rust and nothing else — the tools are a development instrument and no part -//! of the tree. -//! -//! The contents are this project's own test bytes, chosen so the host can -//! predict every one of them without asking anything: an eleven-byte file that -//! bcachefs stores inline, `seq 1 40000` spanning several checksummed extents, -//! an empty directory, a nested directory and a symlink. - -use std::io::Read; -use std::path::{Path, PathBuf}; - -use bcachefs::upstream::fs::{FileKind, Volume}; -use bcachefs::{BlockBuf, BlockIO, BlockNum, DeviceError, TransferError}; - -/// The eleven-byte file, short enough that bcachefs keeps it in the btree -/// rather than in an extent. -const HELLO: &str = "hello-toyos"; -const LINK_TARGET: &str = "../a.txt"; -/// The line count of the multi-extent file, and what makes it predictable. -const SEQ_LINES: u32 = 40_000; - -/// A fixture as a read-only block device, decompressed into memory. -struct FixtureIo { - bytes: Vec, -} - -struct HostIoFailed; -impl TransferError for HostIoFailed { - fn refused_before_attempt(&self) -> bool { - false - } -} - -fn failed() -> DeviceError { - DeviceError::classify(&HostIoFailed) -} - -impl BlockIO for FixtureIo { - fn read_block(&self, block: BlockNum, buf: &mut BlockBuf) -> Result<(), DeviceError> { - let at = (block.raw() as usize).checked_mul(4096).ok_or_else(failed)?; - let end = at.checked_add(4096).ok_or_else(failed)?; - buf.as_bytes_mut().copy_from_slice(self.bytes.get(at..end).ok_or_else(failed)?); - Ok(()) - } - - fn write_block(&self, _block: BlockNum, _buf: &BlockBuf) -> Result<(), DeviceError> { - Err(failed()) - } - - fn block_count(&self) -> u64 { - (self.bytes.len() / 4096) as u64 - } -} - -fn fixture(name: &str) -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures").join(name) -} - -fn open(name: &str) -> Volume { - let path = fixture(name); - let gz = std::fs::read(&path).unwrap_or_else(|e| panic!("reading {}: {e}", path.display())); - let mut bytes = Vec::new(); - flate2::read::GzDecoder::new(&gz[..]) - .read_to_end(&mut bytes) - .unwrap_or_else(|e| panic!("decompressing {}: {e}", path.display())); - Volume::open(FixtureIo { bytes }) - .unwrap_or_else(|e| panic!("opening the volume in {}: {e:?}", path.display())) -} - -/// `seq 1 40000`'s output, which the tools wrote into the fixture and this -/// side reproduces without reading it. -fn seq_text() -> Vec { - let mut out = Vec::new(); - for n in 1..=SEQ_LINES { - out.extend_from_slice(n.to_string().as_bytes()); - out.push(b'\n'); - } - out -} - -/// Every entry of a directory, as ` `, sorted. -fn listing(volume: &Volume, dir: u64) -> Vec { - let mut out = Vec::new(); - volume - .readdir(dir, &mut |name, _, kind| { - let letter = match kind { - FileKind::Dir => 'd', - FileKind::Regular => 'f', - FileKind::Symlink => 'l', - FileKind::Other(_) => '?', - }; - out.push(format!("{letter} {name}")); - true - }) - .expect("listing a directory"); - out.sort(); - out -} - -fn resolve(volume: &Volume, path: &str) -> (u64, FileKind) { - volume - .resolve(path) - .unwrap_or_else(|e| panic!("resolving {path}: {e:?}")) - .unwrap_or_else(|| panic!("the fixture holds no {path}")) -} - -/// The whole tree, read back out of a volume upstream wrote. -#[test] -fn the_upstream_volume_reads_back_as_what_upstream_wrote() { - let volume = open("crc32c.img.gz"); - - assert_eq!( - listing(&volume, volume.root()), - ["d Documents", "d empty", "d lost+found"], - "the root is not what the tools put there" - ); - - let (documents, kind) = resolve(&volume, "/Documents"); - assert_eq!(kind, FileKind::Dir); - assert_eq!(listing(&volume, documents), ["d deep", "f a.txt"]); - - let (deep, kind) = resolve(&volume, "/Documents/deep"); - assert_eq!(kind, FileKind::Dir, "the nested directory"); - assert_eq!(listing(&volume, deep), ["f seq.txt", "l link"]); - - // An empty directory reads back as one, rather than as absent. - let (empty, kind) = resolve(&volume, "/empty"); - assert_eq!(kind, FileKind::Dir); - assert!(listing(&volume, empty).is_empty(), "the empty directory listed something"); - - // The inline-data file: short enough that its bytes are in the btree. - let (a_txt, kind) = resolve(&volume, "/Documents/a.txt"); - assert_eq!(kind, FileKind::Regular); - let attrs = volume.stat(a_txt).expect("stat").expect("an inode"); - assert_eq!(attrs.size, HELLO.len() as u64); - assert_eq!(attrs.mode & 0o170_000, 0o100_000, "a.txt is not a regular file"); - assert_eq!(volume.read(a_txt).expect("reading a.txt"), HELLO.as_bytes()); - - // The multi-extent file, byte for byte against a copy this side made. - let (seq_txt, kind) = resolve(&volume, "/Documents/deep/seq.txt"); - assert_eq!(kind, FileKind::Regular); - let want = seq_text(); - let attrs = volume.stat(seq_txt).expect("stat").expect("an inode"); - assert_eq!(attrs.size, want.len() as u64, "seq.txt is not the length it was written at"); - assert_eq!(volume.read(seq_txt).expect("reading seq.txt"), want); - - let (link, kind) = resolve(&volume, "/Documents/deep/link"); - assert_eq!(kind, FileKind::Symlink); - assert_eq!(volume.read_link(link).expect("reading the symlink"), LINK_TARGET); - - // The root directory is the one the format's own constant names. - assert_eq!(volume.root(), 4096); -} - -/// A path the volume does not hold is `None`, not an error and not a guess. -#[test] -fn a_path_the_volume_does_not_hold_reads_back_as_absent() { - let volume = open("crc32c.img.gz"); - for path in ["/nope", "/Documents/nope", "/Documents/a.txt/under-a-file", "/empty/nope"] { - assert_eq!(volume.resolve(path).expect("resolving"), None, "{path} resolved to something"); - } -} diff --git a/lib/crypto/src/chacha.rs b/lib/crypto/src/chacha.rs index 994fe6344e7..e58f007d755 100644 --- a/lib/crypto/src/chacha.rs +++ b/lib/crypto/src/chacha.rs @@ -1,5 +1,7 @@ //! RFC 8439 ChaCha20; the original 64-bit nonce construction serves SSH. //! Rounds and memory addresses are independent of key and message contents. +//! [`xor`] is the keystream alone, for formats that compose ChaCha20 their +//! own way; TLS and SSH use [`crate::aead`] and [`crate::ssh`]. use crate::{Error, poly1305::Poly1305, secret::Secret}; @@ -116,7 +118,9 @@ fn stream(key: &[u8; 32], counter: u64, nonce: &[u8; 8], data: &mut [u8]) { } } -pub(crate) fn xor(key: &[u8; 32], nonce: &[u8; 12], counter: u32, data: &mut [u8]) -> Result<(), Error> { +/// RFC 8439 §2.4: the keystream from block `counter` of (`key`, `nonce`) +/// XORed into `data`. `Length` when the blocks would pass 2^32 - 1. +pub fn xor(key: &[u8; 32], nonce: &[u8; 12], counter: u32, data: &mut [u8]) -> Result<(), Error> { check_ietf(data.len(), counter)?; let (counter, nonce) = ietf(nonce, counter); stream(key, counter, &nonce, data); diff --git a/lib/crypto/src/lib.rs b/lib/crypto/src/lib.rs index 24a2afb9936..8faf7fc988c 100644 --- a/lib/crypto/src/lib.rs +++ b/lib/crypto/src/lib.rs @@ -25,7 +25,7 @@ pub mod aead; mod aes; mod arch; mod block; -mod chacha; +pub mod chacha; mod ct; pub mod ecc; pub mod ed25519; @@ -39,13 +39,14 @@ pub mod mlkem; mod montgomery; mod naf; mod nist; -mod poly1305; +pub mod poly1305; pub mod rsa; #[cfg(feature = "rustls-provider")] pub mod rustls_provider; mod scalar25519; pub mod scrypt; mod secret; +pub mod siphash; pub mod ssh; #[cfg(feature = "wpa2")] pub mod wpa2; diff --git a/lib/crypto/src/poly1305.rs b/lib/crypto/src/poly1305.rs index d36f5c5a516..0d26eaa42c0 100644 --- a/lib/crypto/src/poly1305.rs +++ b/lib/crypto/src/poly1305.rs @@ -3,11 +3,30 @@ //! bits of each 32-bit word (Bernstein, "The Poly1305-AES message-authentication //! code", FSE 2005), so r1·2^128 ≡ s1 = r1 + r1/4 (mod 2^130 - 5) exactly, and //! every product sum below fits in 128 bits without reduction. No key-indexed -//! table or content-dependent branch. +//! table or content-dependent branch. [`tag`] and [`verify`] are Poly1305 +//! alone, for formats that compose it their own way. +use crate::Error; use crate::block::{padded, place, terminated}; use crate::secret::{Secret, erase}; +/// RFC 8439 §2.5's tag of `message` under the one-time `key`. +pub fn tag(key: &[u8; 32], message: &[u8]) -> [u8; 16] { + let mut mac = Poly1305::new(key); + mac.update(message); + mac.finish() +} + +/// `Ok` when `expected`, 1 to 16 bytes, is that many leading bytes of +/// [`tag`], compared in constant time. +pub fn verify(key: &[u8; 32], message: &[u8], expected: &[u8]) -> Result<(), Error> { + let tag = Secret(tag(key, message)); + match tag.0.get(..expected.len()) { + Some(prefix) if !expected.is_empty() && crate::ct::equal(prefix, expected) => Ok(()), + _ => Err(Error::Authentication), + } +} + pub(crate) struct Poly1305 { /// r0, r1 and s1, each below 2^61. r: [u64; 3], @@ -224,13 +243,15 @@ mod tests { 0x85, 0xd6, 0xbe, 0x78, 0x57, 0x55, 0x6d, 0x33, 0x7f, 0x44, 0x52, 0xfe, 0x42, 0xd5, 0x06, 0xa8, 0x01, 0x03, 0x80, 0x8a, 0xfb, 0x0d, 0xb2, 0xfd, 0x4a, 0xbf, 0xf6, 0xaf, 0x41, 0x49, 0xf5, 0x1b, ]; - let mut mac = Poly1305::new(&key); - mac.update(b"Cryptographic Forum Research Group"); - assert_eq!( - mac.finish(), - [ - 0xa8, 0x06, 0x1d, 0xc1, 0x30, 0x51, 0x36, 0xc6, 0xc2, 0x2b, 0x8b, 0xaf, 0x0c, 0x01, 0x27, 0xa9 - ] - ); + let message = b"Cryptographic Forum Research Group"; + let expected = [0xa8, 0x06, 0x1d, 0xc1, 0x30, 0x51, 0x36, 0xc6, 0xc2, 0x2b, 0x8b, 0xaf, 0x0c, 0x01, 0x27, 0xa9]; + assert_eq!(tag(&key, message), expected); + assert_eq!(verify(&key, message, &expected), Ok(())); + assert_eq!(verify(&key, message, &expected[..10]), Ok(()), "a truncated tag is its prefix"); + let mut forged = expected; + forged[9] ^= 1; + assert_eq!(verify(&key, message, &forged[..10]), Err(Error::Authentication)); + assert_eq!(verify(&key, message, &[]), Err(Error::Authentication), "no tag authenticates nothing"); + assert_eq!(verify(&key, message, &[0; 17]), Err(Error::Authentication)); } } diff --git a/lib/crypto/src/siphash.rs b/lib/crypto/src/siphash.rs new file mode 100644 index 00000000000..9d7fce3f476 --- /dev/null +++ b/lib/crypto/src/siphash.rs @@ -0,0 +1,69 @@ +//! SipHash-2-4 (Aumasson and Bernstein, "SipHash: a fast short-input PRF", 2012), a keyed +//! function of short inputs. Key and message reach no branch or address. + +pub type Key = [u8; 16]; + +fn round(v: &mut [u64; 4]) { + v[0] = v[0].wrapping_add(v[1]); + v[1] = v[1].rotate_left(13) ^ v[0]; + v[0] = v[0].rotate_left(32); + v[2] = v[2].wrapping_add(v[3]); + v[3] = v[3].rotate_left(16) ^ v[2]; + v[0] = v[0].wrapping_add(v[3]); + v[3] = v[3].rotate_left(21) ^ v[0]; + v[2] = v[2].wrapping_add(v[1]); + v[1] = v[1].rotate_left(17) ^ v[2]; + v[2] = v[2].rotate_left(32); +} + +fn compress(v: &mut [u64; 4], m: u64) { + v[3] ^= m; + round(v); + round(v); + v[0] ^= m; +} + +pub fn siphash24(key: &Key, data: &[u8]) -> u64 { + let (k0, k1) = key.split_at(8); + let k0 = k0.iter().rev().fold(0u64, |a, &b| a.rotate_left(8) | u64::from(b)); + let k1 = k1.iter().rev().fold(0u64, |a, &b| a.rotate_left(8) | u64::from(b)); + let mut v = [k0 ^ 0x736f_6d65_7073_6575, k1 ^ 0x646f_7261_6e64_6f6d, k0 ^ 0x6c79_6765_6e65_7261, k1 ^ 0x7465_6462_7974_6573]; + let (words, tail) = data.as_chunks::<8>(); + for word in words { + compress(&mut v, u64::from_le_bytes(*word)); + } + let length = u64::from(data.len().to_le_bytes()[0]).rotate_right(8); + compress(&mut v, tail.iter().rev().fold(0u64, |a, &b| a.rotate_left(8) | u64::from(b)) | length); + v[2] ^= 0xff; + for _ in 0..4 { + round(&mut v); + } + v[0] ^ v[1] ^ v[2] ^ v[3] +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn the_papers_vector() { + // Appendix A: key 00..0f, message 00..0e. + let key: Key = core::array::from_fn(|i| i as u8); + let message: [u8; 15] = core::array::from_fn(|i| i as u8); + assert_eq!(siphash24(&key, &message), 0xa129_ca61_49be_45e5); + } + + #[test] + #[allow(deprecated)] + fn every_length_through_three_words_agrees_with_std() { + use core::hash::Hasher; + let key: Key = core::array::from_fn(|i| (i as u8).wrapping_mul(37) ^ 0x5a); + let message: [u8; 25] = core::array::from_fn(|i| (i as u8).wrapping_mul(101).wrapping_add(7)); + for len in 0..=message.len() { + let (k0, k1) = (u64::from_le_bytes(key[..8].try_into().unwrap()), u64::from_le_bytes(key[8..].try_into().unwrap())); + let mut oracle = core::hash::SipHasher::new_with_keys(k0, k1); + oracle.write(&message[..len]); + assert_eq!(siphash24(&key, &message[..len]), oracle.finish(), "{len} bytes"); + } + } +} diff --git a/system/netstack/wire/Cargo.toml b/system/netstack/wire/Cargo.toml index 87075fd18fc..27295039910 100644 --- a/system/netstack/wire/Cargo.toml +++ b/system/netstack/wire/Cargo.toml @@ -5,3 +5,6 @@ edition = "2021" description = "Ethernet, ARP, IPv4, ICMPv4, IGMP, UDP and TCP wire formats, parsed in place and built, and what every net crate shares: the caller's clock, SipHash, IPv4 address classes and the counters macro (no_std, forbid(unsafe_code))." license = "MIT OR Apache-2.0" publish = false + +[dependencies] +toyos-crypto = { path = "../../../lib/crypto" } diff --git a/system/netstack/wire/src/siphash.rs b/system/netstack/wire/src/siphash.rs index d62032a6a70..f5f6b2c19e8 100644 --- a/system/netstack/wire/src/siphash.rs +++ b/system/netstack/wire/src/siphash.rs @@ -1,45 +1,7 @@ -//! SipHash-2-4 (Aumasson and Bernstein, "SipHash: a fast short-input PRF"): the keyed function -//! behind every value a net crate keeps unpredictable, each purpose with its own key. +//! The keyed function behind every value a net crate keeps unpredictable, each purpose with its +//! own key. -pub type Key = [u8; 16]; - -fn round(v: &mut [u64; 4]) { - v[0] = v[0].wrapping_add(v[1]); - v[1] = v[1].rotate_left(13) ^ v[0]; - v[0] = v[0].rotate_left(32); - v[2] = v[2].wrapping_add(v[3]); - v[3] = v[3].rotate_left(16) ^ v[2]; - v[0] = v[0].wrapping_add(v[3]); - v[3] = v[3].rotate_left(21) ^ v[0]; - v[2] = v[2].wrapping_add(v[1]); - v[1] = v[1].rotate_left(17) ^ v[2]; - v[2] = v[2].rotate_left(32); -} - -fn compress(v: &mut [u64; 4], m: u64) { - v[3] ^= m; - round(v); - round(v); - v[0] ^= m; -} - -pub fn siphash24(key: &Key, data: &[u8]) -> u64 { - let (k0, k1) = key.split_at(8); - let k0 = k0.iter().rev().fold(0u64, |a, &b| a.rotate_left(8) | u64::from(b)); - let k1 = k1.iter().rev().fold(0u64, |a, &b| a.rotate_left(8) | u64::from(b)); - let mut v = [k0 ^ 0x736f_6d65_7073_6575, k1 ^ 0x646f_7261_6e64_6f6d, k0 ^ 0x6c79_6765_6e65_7261, k1 ^ 0x7465_6462_7974_6573]; - let (words, tail) = data.as_chunks::<8>(); - for word in words { - compress(&mut v, u64::from_le_bytes(*word)); - } - let length = u64::from(data.len().to_le_bytes()[0]).rotate_right(8); - compress(&mut v, tail.iter().rev().fold(0u64, |a, &b| a.rotate_left(8) | u64::from(b)) | length); - v[2] ^= 0xff; - for _ in 0..4 { - round(&mut v); - } - v[0] ^ v[1] ^ v[2] ^ v[3] -} +pub use toyos_crypto::siphash::{siphash24, Key}; pub fn low32(key: &Key, data: &[u8]) -> u32 { let [a, b, c, d, ..] = siphash24(key, data).to_le_bytes();