Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions build/ci.rs
Original file line number Diff line number Diff line change
Expand Up @@ -754,9 +754,9 @@ pub(crate) const CONTROLS: &[Control] = &[
red(KERNEL_SIM, "placement-ignores-staleness", Some("policy"), &[
Fails("a_stopped_cpu_stops_taking_work"),
]),
// The block protocol's three: a completion lost to a session's end, a
// completion given twice after a reset, and a loss nothing is written
// again after.
// The block protocol's four: a completion lost to a session's end, a
// completion given twice after a reset, a loss nothing is written again
// after, and a forced write the device was not told to force.
red(BLOCKRING, "mutate-session-end-forgets", None, &[
Fails("model::tests::every_request_is_answered_exactly_once"),
]),
Expand All @@ -766,6 +766,9 @@ pub(crate) const CONTROLS: &[Control] = &[
red(BLOCKRING, "mutate-no-reissue-after-loss", None, &[
Fails("model::tests::what_a_flush_calls_durable_is_on_the_medium"),
]),
red(BLOCKRING, "mutate-fua-goes-out-unforced", None, &[
Fails("model::tests::a_forced_write_answered_done_stays_on_the_medium"),
]),
// The link's one: a full ring's want swapped `Relaxed`, a wake lost.
red(LINK, "mutate-want-relaxed", Some("ends_loom"), &[Fails("a_full_ring_is_never_left_asleep_on_room_it_has")]),
red(SDK, "transport-publish-relaxed", Some("transport_loom"), &[
Expand Down
4 changes: 4 additions & 0 deletions docs/src/architecture/storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ All four layers run in userland.
block service.
- A partition has one holder at a time, every request is bounded to it before
the device sees it, and a flush answers for its own writer's writes.
- A write may be asked onto the medium by itself, without a flush of the
whole device; a filesystem's journal commits with one. A device that cannot
force a single write is written and then flushed instead, and its driver
says so when it brings the disk up.
- Caching belongs to file servers, never to the block layer.
- Partition editing and storage initialisation are userland operations shared
by disk tools and the installer. Editing the table, reading payloads and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,35 @@ comparison of ToyOS's resets with Linux's restart
(`issues/a-reset-can-leave-a-usb-stick-hung-until-its-power-is-removed.md`),
and that does not discharge this exit.

**A stick's forced writes have never met a real stick.** This is not a
judgement the boot stick lost: it is new with the forced write. usbd asks
MODE SENSE(6) as a disk first comes up, reads DPOFUA from its header, and
sends a durable write as WRITE(10) with FUA only where the stick set it
(`system/usbd/xhci/src/msc.rs`, `lib/scsi`). A host model and QEMU's stick
are all that have answered either command. What a real stick can do that
they do not:

- **Break on the MODE SENSE.** The command climbs the recovery ladder like
any broken command, and since it runs at every first bring-up, a stick
that hangs on it is never served.
- **Set DPOFUA and refuse a write with FUA.** The write is answered not
done; the journal issues its entry again twice more and then the mount stops
writing. Nothing acknowledged is lost, and the volume takes no more
writes until it is mounted again.
- **Set DPOFUA and ignore FUA.** The write is answered done from the
stick's cache. The commit is acknowledged without being on the medium, a
power cut loses it, and usbd counts no flush owed for it, so a stick that
leaves then is counted as having lost nothing. Nothing on the host can
see this.

**Exit**: a metal row drives a USB stick that is not the boot disk — the
scratch disk (`issues/the-scratch-disk-adopt-scratch-makes-is-not-served.md`)
— and judges usbd serving it, a reset under a write to it, and a tone beside
that write.
that write. And for forced writes: on a real stick, usbd's bring-up line
shows the MODE SENSE answered with no Bulk-Only break before it; and on a
real device that sets DPOFUA, carrying a bcachefs volume, a commit's
WRITE(10) with FUA is answered done. Whether such a device honours FUA is
shown only by cutting its power after an acknowledged commit; that is
evidence to record if it is ever taken, and no part of this exit.

**Owner**: the orchestrator, which runs metal.
18 changes: 16 additions & 2 deletions lib/bcachefs/src/engine/mkfs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -353,12 +353,26 @@ fn fields(geometry: &Geometry, member: &Member, crypt: Option<&Crypt>, roots: &[
out
}

pub(crate) async fn write(dev: &impl BlockDev, sector: u64, bytes: &[u8]) -> Result<()> {
/// The device block `sector` is, and `bytes` as whole blocks, zero past
/// their end.
fn blocks(sector: u64, bytes: &[u8]) -> (u64, Vec<[u8; BLOCK]>) {
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?;
(at / BLOCK as u64, blocks)
}

pub(crate) async fn write(dev: &impl BlockDev, sector: u64, bytes: &[u8]) -> Result<()> {
let (lba, blocks) = blocks(sector, bytes);
dev.write(lba, &blocks).await?;
Ok(())
}

/// [`write`], on the medium once it returns ([`BlockDev::write_durable`]).
pub(crate) async fn write_durable(dev: &impl BlockDev, sector: u64, bytes: &[u8]) -> Result<()> {
let (lba, blocks) = blocks(sector, bytes);
dev.write_durable(lba, &blocks).await?;
Ok(())
}

Expand Down
21 changes: 11 additions & 10 deletions lib/bcachefs/src/engine/mount/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1441,10 +1441,14 @@ impl<D: BlockDev> Mount<D> {
}

/// Writes every closed entry through `upto`, in order, each after a
/// flush and followed by one (A2.12), so each is durable once written and
/// so is every node write that completed before it. An entry naming
/// itself has its header sector written last, after a flush. A failed
/// write is reissued with the same bytes, or the mount stops writing.
/// flush and itself durable as it is written (A2.12), so each is durable
/// once written and so is every node write that completed before it. An
/// entry naming itself has its header sector written last, once the rest
/// is durable. A durable write is never over a write no flush has
/// covered: an attempt that failed may have left the entry's blocks
/// written and unflushed, and each attempt's own flush covers them
/// before it writes again. A failed write is reissued with the same
/// bytes, or the mount stops writing.
async fn write_entries(&self, upto: u64) -> Result<()> {
self.journal_queue.wait(|| (!self.journal_held.get()).then(|| self.journal_held.set(true))).await;
let out = async {
Expand All @@ -1468,14 +1472,11 @@ impl<D: BlockDev> Mount<D> {
if header_last {
let mut headless = bytes.clone();
headless[..SECTOR].fill(0);
mkfs::write(&self.dev, sector, &headless).await?;
self.dev.flush().await?;
mkfs::write(&self.dev, sector, &bytes[..BLOCK]).await?;
mkfs::write_durable(&self.dev, sector, &headless).await?;
mkfs::write_durable(&self.dev, sector, &bytes[..BLOCK]).await
} else {
mkfs::write(&self.dev, sector, &bytes).await?;
mkfs::write_durable(&self.dev, sector, &bytes).await
}
self.dev.flush().await?;
Ok(())
})
.await?;
let mut st = self.st.borrow_mut();
Expand Down
139 changes: 138 additions & 1 deletion lib/bcachefs/src/engine/tests/crash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ use super::mount::{random, MEMORY, NOW, OWNER};
use super::*;

/// A device that, after each write and flush it passes on, hands the model
/// to `after`.
/// to `after`: a durable write both while it is still torn and once it is on
/// the medium.
pub(super) struct Watched<'a> {
pub model: &'a Model,
pub after: Box<dyn Fn(&Model) + 'a>,
Expand All @@ -42,6 +43,14 @@ impl BlockDev for Watched<'_> {
(self.after)(self.model);
Ok(())
}

async fn write_durable(&self, lba: u64, data: &[Block]) -> core::result::Result<(), toyos_blockdev::Error> {
self.model.write(lba, data).await?;
(self.after)(self.model);
self.model.forced();
(self.after)(self.model);
Ok(())
}
}

const MIN_LIMITS: toyos_crypto::scrypt::Limits = toyos_crypto::scrypt::Limits { memory: 1 << 20, work: 1 << 20 };
Expand Down Expand Up @@ -240,6 +249,134 @@ pub(super) fn ops_survive_a_crash_on(image: Vec<u8>, passphrase: Option<&[u8]>,
reused
}

/// What a mount asked of its device, in order.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Asked {
Write,
Flush,
/// A durable write at this block.
Durable(u64),
}

/// A device that records what it is asked, and takes a durable write
/// forced, or, with `forces` off, as a write it leaves in its cache. It
/// refuses its next `refusing` durable writes, each left in its cache.
struct Recorded<'a> {
model: &'a Model,
forces: bool,
refusing: Cell<u32>,
asked: core::cell::RefCell<Vec<Asked>>,
}

impl BlockDev for Recorded<'_> {
fn blocks(&self) -> u64 {
self.model.blocks()
}

async fn read(&self, lba: u64, out: &mut [Block]) -> core::result::Result<(), toyos_blockdev::Error> {
self.model.read(lba, out).await
}

async fn write(&self, lba: u64, data: &[Block]) -> core::result::Result<(), toyos_blockdev::Error> {
self.asked.borrow_mut().push(Asked::Write);
self.model.write(lba, data).await
}

async fn flush(&self) -> core::result::Result<(), toyos_blockdev::Error> {
self.asked.borrow_mut().push(Asked::Flush);
self.model.flush().await
}

async fn write_durable(&self, lba: u64, data: &[Block]) -> core::result::Result<(), toyos_blockdev::Error> {
self.asked.borrow_mut().push(Asked::Durable(lba));
if self.refusing.get() > 0 {
self.refusing.set(self.refusing.get() - 1);
self.model.write(lba, data).await?;
return Err(toyos_blockdev::Error::Device);
}
match self.forces {
true => self.model.write_durable(lba, data).await,
false => self.model.write(lba, data).await,
}
}
}

/// A commit of one create asks the device for a flush and then its journal
/// entry, durable as written, and nothing after: with a file's data written
/// before it, the same two after the data. Every crash once the commit has
/// returned holds the create. The control: on a device that takes the entry
/// without forcing it, some crash after the commit returned has lost it.
#[test]
fn a_commit_is_one_flush_and_its_entry_forced_and_is_lost_where_the_entry_is_not() {
for forces in [true, false] {
let model = Model::new(formatted(64 << 20, None).into_inner(), 512);
let dev = Recorded { model: &model, forces, refusing: Cell::new(0), asked: core::cell::RefCell::default() };
let mount = block_on_ready(Mount::open(&dev, MEMORY, random())).unwrap();
let root = mount.root();
block_on_ready(async {
// The first entry after a clean state names itself, and goes
// header last.
mount.create(root, b"first", New::File, OWNER, NOW).await.unwrap();
mount.commit().await.unwrap();
dev.asked.borrow_mut().clear();
mount.create(root, b"second", New::File, OWNER, NOW).await.unwrap();
mount.commit().await.unwrap();
});
let asked = dev.asked.take();
let [Asked::Flush, Asked::Durable(entry)] = asked[..] else { panic!("a commit asked {asked:?}") };

let held = model.check(1 << 12, 0xf0a_0001, |image| {
let dev = Overlay::new(image);
let mount = block_on_ready(Mount::open(&dev, MEMORY, random())).map_err(|e| format!("mount: {e:?}"))?;
match block_on_ready(mount.lookup(mount.root(), b"second")) {
Ok(Some(_)) => Ok(()),
other => Err(format!("the committed create is {other:?}")),
}
});
match (forces, held) {
(true, Ok(_)) => {}
(true, Err(lost)) => panic!("{lost}"),
(false, Err(_)) => continue,
(false, Ok(search)) => panic!("an entry left in the cache was held through {search:?}"),
}

block_on_ready(async {
let file = mount.lookup(root, b"second").await.unwrap().unwrap().ino;
assert_eq!(mount.write(file, 0, &[7; BLOCK], NOW).await.unwrap(), BLOCK);
mount.commit().await.unwrap();
});
let asked = dev.asked.take();
let [Asked::Write, Asked::Flush, Asked::Durable(next)] = asked[..] else { panic!("a commit after data asked {asked:?}") };
assert!(next > entry, "the second entry at block {next} is not after the first at {entry}");
block_on_ready(mount.unmount()).unwrap();
}
}

/// A durable write the device refused may be in its cache, so the attempt
/// after it flushes before it writes the entry again: the model refuses a
/// forced write over what no flush has covered, and the commit is asked as
/// flush, entry, flush, entry.
#[test]
fn an_entry_written_again_after_a_refusal_follows_a_flush_of_its_own() {
let model = Model::new(formatted(64 << 20, None).into_inner(), 512);
let dev = Recorded { model: &model, forces: true, refusing: Cell::new(0), asked: core::cell::RefCell::default() };
let mount = block_on_ready(Mount::open(&dev, MEMORY, random())).unwrap();
block_on_ready(async {
mount.create(mount.root(), b"first", New::File, OWNER, NOW).await.unwrap();
mount.commit().await.unwrap();
dev.asked.borrow_mut().clear();
dev.refusing.set(1);
mount.create(mount.root(), b"second", New::File, OWNER, NOW).await.unwrap();
mount.commit().await.unwrap();
});
let asked = dev.asked.take();
let [Asked::Flush, Asked::Durable(first), Asked::Flush, Asked::Durable(again)] = asked[..] else {
panic!("a commit whose entry was refused once asked {asked:?}")
};
assert_eq!(first, again);
block_on_ready(mount.unmount()).unwrap();
}

fn creates(n: usize) -> Vec<Op> {
(0..n).map(|i| Op::Create(format!("c{i}").into_bytes())).collect()
}
Expand Down
15 changes: 15 additions & 0 deletions lib/blockdev/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,15 @@ pub trait BlockDev {
/// Every write that completed before this was asked, on the medium.
async fn flush(&self) -> Result<(), Error>;

/// `data` at `lba`, on the medium once complete; nothing else is promised
/// durable. It must not overlap a write of this device's no flush has
/// covered. A device that cannot force one write to its medium writes it
/// and then flushes, which is this.
async fn write_durable(&self, lba: u64, data: &[Block]) -> Result<(), Error> {
self.write(lba, data).await?;
self.flush().await
}

/// [`read`](Self::read) of a file's data rather than the filesystem's own
/// structures: a device that caches the structures may pass this by.
async fn read_data(&self, lba: u64, out: &mut [Block]) -> Result<(), Error> {
Expand Down Expand Up @@ -105,6 +114,9 @@ impl<D: BlockDev + ?Sized> BlockDev for &D {
async fn flush(&self) -> Result<(), Error> {
(**self).flush().await
}
async fn write_durable(&self, lba: u64, data: &[Block]) -> Result<(), Error> {
(**self).write_durable(lba, data).await
}
async fn read_data(&self, lba: u64, out: &mut [Block]) -> Result<(), Error> {
(**self).read_data(lba, out).await
}
Expand All @@ -126,6 +138,9 @@ impl<D: BlockDev + ?Sized> BlockDev for Rc<D> {
async fn flush(&self) -> Result<(), Error> {
(**self).flush().await
}
async fn write_durable(&self, lba: u64, data: &[Block]) -> Result<(), Error> {
(**self).write_durable(lba, data).await
}
async fn read_data(&self, lba: u64, out: &mut [Block]) -> Result<(), Error> {
(**self).read_data(lba, out).await
}
Expand Down
Loading
Loading