use crate::block_io::BlockDevice;
use crate::error::{Error, Result};
use crate::fs::Filesystem;
use crate::inode::Inode;
use crate::jbd2::{self, JournalSuperblock, JBD2_MAGIC_NUMBER};
use crate::transaction::Transaction;
pub struct JournalWriter {
healthy: bool,
hold_needs_recovery: bool,
physical_map: Vec<u64>,
block_size: u32,
jsb: JournalSuperblock,
blocks_count: u64,
}
fn journal_len_fits(
max_len: u32,
inode_size: u64,
block_size: u32,
device_bytes: u64,
) -> Result<()> {
let block_size = u64::from(block_size.max(1));
if u64::from(max_len) > inode_size / block_size {
return Err(Error::Corrupt(
"journal declares more blocks than its inode holds",
));
}
if u64::from(max_len) > device_bytes / block_size {
return Err(Error::Corrupt(
"journal declares more blocks than the device holds",
));
}
Ok(())
}
impl JournalWriter {
pub fn open(fs: &Filesystem) -> Result<Option<Self>> {
let Some(jsb) = jbd2::read_superblock(fs)? else {
return Ok(None);
};
let raw = fs.read_inode_raw(fs.sb.journal_inode)?;
let jinode = Inode::parse(&raw)?;
let bs = fs.sb.block_size();
if u64::from(jsb.max_len) > fs.sb.blocks_count {
return Err(Error::Corrupt(
"journal declares more blocks than the filesystem holds",
));
}
journal_len_fits(jsb.max_len, jinode.size, bs, fs.dev.size_bytes())?;
let descriptor_bytes = fs.sb.block_group_count() * u64::from(fs.sb.desc_size);
let group0_metadata_end = u64::from(fs.sb.first_data_block)
+ 1
+ descriptor_bytes.div_ceil(u64::from(bs))
+ u64::from(fs.sb.reserved_gdt_blocks);
let mut physical_map = Vec::with_capacity(jsb.max_len as usize);
for logical in 0..jsb.max_len as u64 {
let phys = crate::indirect::map_logical_any(
&jinode.block,
jinode.flags,
fs.dev.as_ref(),
bs,
logical,
)?
.ok_or(Error::Corrupt(
"journal_writer: journal inode has unmapped logical block",
))?;
if phys < group0_metadata_end || phys >= fs.sb.blocks_count {
return Err(Error::Corrupt(
"journal_writer: journal inode maps a block onto the superblock, group 0's \
descriptor table, or past the end of the filesystem",
));
}
physical_map.push(phys);
}
Ok(Some(Self {
healthy: true,
hold_needs_recovery: false,
blocks_count: fs.sb.blocks_count,
physical_map,
block_size: bs,
jsb,
}))
}
pub fn begin(&self) -> Transaction {
Transaction::begin(
self.jsb.sequence,
self.block_size,
self.jsb.uses_64bit(),
self.jsb.uses_csum_v3(),
)
}
pub fn max_blocks_per_transaction(&self) -> usize {
(self.jsb.max_len as usize).saturating_sub(1)
}
pub fn commit(&mut self, dev: &dyn BlockDevice, tx: &Transaction) -> Result<()> {
if !self.healthy {
return Err(Error::Corrupt("journal writer failed; reopen for recovery"));
}
self.commit_inner(dev, tx)
}
pub(crate) fn holding_needs_recovery(mut self) -> Self {
self.hold_needs_recovery = true;
self
}
pub fn is_healthy(&self) -> bool {
self.healthy
}
fn commit_inner(&mut self, dev: &dyn BlockDevice, tx: &Transaction) -> Result<()> {
if !dev.is_writable() {
return Err(Error::ReadOnly);
}
if tx.sequence != self.jsb.sequence {
return Err(Error::Corrupt(
"journal_writer: transaction sequence does not match writer state",
));
}
if tx.block_size != self.block_size {
return Err(Error::InvalidArgument(
"journal_writer: transaction block size does not match the volume",
));
}
if tx
.writes
.iter()
.any(|w| w.bytes.len() != self.block_size as usize)
{
return Err(Error::InvalidArgument(
"journal_writer: a journaled write is not exactly one block",
));
}
let sb_block = self.sb_block();
let patched;
let tx = if tx.writes.iter().any(|w| w.fs_block == sb_block) {
let mut copy = tx.clone();
let off = self.sb_offset_in_block();
for w in copy.writes.iter_mut().filter(|w| w.fs_block == sb_block) {
crate::journal_apply::set_needs_recovery_in(&mut w.bytes[off..off + 1024], true);
}
patched = copy;
&patched
} else {
tx
};
let blocks = tx.commit_for(&self.jsb)?;
if blocks.is_empty() {
return Ok(()); }
if blocks.len() > self.max_blocks_per_transaction() {
return Err(Error::Corrupt(
"journal_writer: transaction too large for journal",
));
}
self.healthy = false;
let txn_first_jblock = 1usize;
for (i, block) in blocks.iter().enumerate() {
let jblock_idx = txn_first_jblock + i;
let phys = self.physical_map[jblock_idx];
let at = crate::journal_apply::byte_offset_in(
phys,
self.blocks_count,
self.block_size,
dev.size_bytes(),
)?;
dev.write_at(at, block)?;
}
crate::journal_apply::write_needs_recovery(dev, true)?;
dev.flush()?;
self.jsb.start = txn_first_jblock as u32;
self.write_jsb(dev)?;
dev.flush()?;
for w in &tx.writes {
let at = crate::journal_apply::byte_offset_in(
w.fs_block,
self.blocks_count,
self.block_size,
dev.size_bytes(),
)?;
dev.write_at(at, &w.bytes)?;
}
dev.flush()?;
self.jsb.start = 0;
self.jsb.sequence = self.jsb.sequence.wrapping_add(1);
self.write_jsb(dev)?;
if !self.hold_needs_recovery {
crate::journal_apply::write_needs_recovery(dev, false)?;
}
dev.flush()?;
self.healthy = true;
Ok(())
}
fn sb_block(&self) -> u64 {
crate::superblock::SUPERBLOCK_OFFSET / u64::from(self.block_size)
}
fn sb_offset_in_block(&self) -> usize {
(crate::superblock::SUPERBLOCK_OFFSET % u64::from(self.block_size)) as usize
}
fn write_jsb(&self, dev: &dyn BlockDevice) -> Result<()> {
let bs_u64 = self.block_size as u64;
let phys = self.physical_map[0];
let mut buf = vec![0u8; self.block_size as usize];
dev.read_at(phys * bs_u64, &mut buf)?;
let magic = u32::from_be_bytes(buf[0x00..0x04].try_into().unwrap());
if magic != JBD2_MAGIC_NUMBER {
return Err(Error::Corrupt(
"journal_writer: jsb block lost its magic between mount and commit",
));
}
let block_type = u32::from_be_bytes(buf[0x04..0x08].try_into().unwrap());
if block_type != self.jsb.block_type {
return Err(Error::Corrupt(
"journal_writer: jsb block_type changed since mount",
));
}
buf[0x08..0x0C].copy_from_slice(&self.jsb.sequence.to_be_bytes());
buf[0x18..0x1C].copy_from_slice(&self.jsb.sequence.to_be_bytes());
buf[0x1C..0x20].copy_from_slice(&self.jsb.start.to_be_bytes());
if self.jsb.uses_csum_v2_or_v3() {
const JSB_LEN: usize = 1024; buf[0xFC..0x100].copy_from_slice(&0u32.to_be_bytes());
let csum = crate::checksum::linux_crc32c(!0, &buf[..JSB_LEN]);
buf[0xFC..0x100].copy_from_slice(&csum.to_be_bytes());
}
dev.write_at(phys * bs_u64, &buf)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_journal_must_fit_its_inode_and_the_device() {
assert!(journal_len_fits(8, 8 * 4096, 4096, 16 * 4096).is_ok());
assert!(journal_len_fits(9, 8 * 4096, 4096, 16 * 4096).is_err());
assert!(journal_len_fits(17, u64::MAX, 4096, 16 * 4096).is_err());
assert!(journal_len_fits(16, u64::MAX, 4096, 16 * 4096).is_ok());
assert!(journal_len_fits(u32::MAX, u64::MAX, 4096, 64 << 20).is_err());
}
mod journal_presence {
use super::*;
use crate::features::{Compat, FsFlavor};
use std::sync::{Arc, Mutex};
const BS: u32 = 1024;
const VOL: u64 = 8 * 1024 * 1024;
struct MemDev {
bytes: Mutex<Vec<u8>>,
}
impl MemDev {
fn new() -> Arc<Self> {
Arc::new(Self {
bytes: Mutex::new(vec![0u8; VOL as usize]),
})
}
}
impl crate::block_io::BlockDevice for MemDev {
fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
let b = self.bytes.lock().unwrap();
let (start, end) = (offset as usize, offset as usize + buf.len());
if end > b.len() {
return Err(Error::Corrupt("MemDev: read past end"));
}
buf.copy_from_slice(&b[start..end]);
Ok(())
}
fn write_at(&self, offset: u64, buf: &[u8]) -> Result<()> {
let mut b = self.bytes.lock().unwrap();
let (start, end) = (offset as usize, offset as usize + buf.len());
if end > b.len() {
return Err(Error::Corrupt("MemDev: write past end"));
}
b[start..end].copy_from_slice(buf);
Ok(())
}
fn size_bytes(&self) -> u64 {
VOL
}
fn flush(&self) -> Result<()> {
Ok(())
}
fn is_writable(&self) -> bool {
true
}
}
fn formatted(flavor: FsFlavor) -> Filesystem {
let dev = MemDev::new();
crate::mkfs::format_filesystem_with_flavor(dev.as_ref(), None, None, VOL, BS, flavor)
.expect("format");
Filesystem::mount(dev).expect("mount")
}
#[test]
fn open_returns_none_when_the_volume_has_no_journal() {
let fs = formatted(FsFlavor::Ext4);
assert_eq!(
fs.sb.feature_compat & Compat::HAS_JOURNAL.bits(),
0,
"this volume was formatted without a journal and says it has one"
);
assert_eq!(fs.sb.journal_inode, 0, "and names no journal inode");
let writer = JournalWriter::open(&fs).expect("open");
assert!(
writer.is_none(),
"a volume with no journal handed back a journal writer, so every write \
through it would go to blocks that are not a journal"
);
}
#[test]
fn open_returns_a_writer_when_the_volume_has_one() {
let fs = formatted(FsFlavor::Ext3);
assert_ne!(
fs.sb.feature_compat & Compat::HAS_JOURNAL.bits(),
0,
"this volume was formatted with a journal and says it has none"
);
assert_ne!(fs.sb.journal_inode, 0, "and it names the journal inode");
let writer = JournalWriter::open(&fs).expect("open");
assert!(
writer.is_some(),
"a volume with a journal handed back nothing, so a caller would fall \
back to unjournaled writes on a filesystem that has a log"
);
}
#[test]
fn a_refusal_before_any_write_leaves_the_writer_usable() {
let fs = formatted(FsFlavor::Ext3);
let mut writer = JournalWriter::open(&fs).expect("open").expect("writer");
let block = fs.sb.blocks_count - 1;
let mut too_large = writer.begin();
for i in 0..=writer.max_blocks_per_transaction() as u64 {
too_large
.add_write(block - i, vec![0u8; BS as usize])
.expect("add_write");
}
assert!(writer.commit(fs.dev.as_ref(), &too_large).is_err());
assert!(
writer.is_healthy(),
"a transaction refused before it wrote anything retired the writer"
);
let mut small = writer.begin();
small
.add_write(block, vec![0u8; BS as usize])
.expect("add_write");
writer
.commit(fs.dev.as_ref(), &small)
.expect("the next transaction commits");
}
fn writer_on_ext3() -> (Arc<MemDev>, Filesystem, JournalWriter) {
let dev = MemDev::new();
crate::mkfs::format_filesystem_with_flavor(
dev.as_ref(),
None,
None,
VOL,
BS,
FsFlavor::Ext3,
)
.expect("format");
let fs = Filesystem::mount(dev.clone()).expect("mount");
let writer = JournalWriter::open(&fs).expect("open").expect("writer");
(dev, fs, writer)
}
fn assert_refused_untouched(tx: &Transaction, what: &str) {
let (dev, fs, mut writer) = writer_on_ext3();
let mut tx = tx.clone();
tx.sequence = writer.begin().sequence;
let before = dev.bytes.lock().unwrap().clone();
let got = writer.commit(fs.dev.as_ref(), &tx);
assert!(got.is_err(), "{what}: committed ({got:?})");
assert!(
*dev.bytes.lock().unwrap() == before,
"{what}: the refused commit wrote to the device"
);
assert!(
writer.is_healthy(),
"{what}: the refusal retired the writer"
);
}
#[test]
fn a_write_longer_than_a_block_is_refused_before_any_write() {
let mut tx = Transaction::begin(0, BS, false, false);
tx.writes.push(crate::transaction::JournaledBlock {
fs_block: 100,
bytes: vec![0xA5; 2 * BS as usize],
});
assert_refused_untouched(&tx, "a 2x-block entry");
}
#[test]
fn a_short_superblock_write_is_refused_not_a_panic() {
let mut tx = Transaction::begin(0, BS, false, false);
tx.writes.push(crate::transaction::JournaledBlock {
fs_block: 1, bytes: vec![0u8; 16],
});
assert_refused_untouched(&tx, "a 16-byte superblock entry");
}
#[test]
fn a_transaction_for_another_block_size_is_refused() {
let mut tx = Transaction::begin(0, 2 * BS, false, false);
tx.add_write(100, vec![0xA5; 2 * BS as usize])
.expect("add_write at the transaction's own size");
assert_refused_untouched(&tx, "a 2 KiB transaction on a 1 KiB volume");
}
struct CutDevice {
inner: Arc<MemDev>,
budget: Mutex<usize>,
}
impl crate::block_io::BlockDevice for CutDevice {
fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
self.inner.read_at(offset, buf)
}
fn write_at(&self, offset: u64, buf: &[u8]) -> Result<()> {
let mut left = self.budget.lock().unwrap();
if *left == 0 {
return Ok(());
}
*left -= 1;
self.inner.write_at(offset, buf)
}
fn size_bytes(&self) -> u64 {
VOL
}
fn flush(&self) -> Result<()> {
Ok(())
}
fn is_writable(&self) -> bool {
true
}
}
#[test]
fn a_block_starting_with_the_magic_is_logged_escaped_and_replayed_whole() {
let dev = MemDev::new();
crate::mkfs::format_filesystem_with_flavor(
dev.as_ref(),
None,
None,
VOL,
BS,
FsFlavor::Ext3,
)
.expect("format");
let fs = Filesystem::mount(dev.clone()).expect("mount");
let mut writer = JournalWriter::open(&fs).expect("open").expect("writer");
let target = fs.sb.blocks_count - 1;
let mut payload: Vec<u8> = (0..BS as usize).map(|j| 0x5A ^ (j % 251) as u8).collect();
payload[0..4].copy_from_slice(&JBD2_MAGIC_NUMBER.to_be_bytes());
let mut tx = writer.begin();
tx.add_write(target, payload.clone()).expect("add_write");
let cut = CutDevice {
inner: dev.clone(),
budget: Mutex::new(3 + 2),
};
writer.commit(&cut, &tx).expect("commit");
let logged_at = writer.physical_map[2];
drop(fs);
let read = |block: u64| {
let mut b = vec![0u8; BS as usize];
dev.read_at(block * BS as u64, &mut b).unwrap();
b
};
let logged = read(logged_at);
assert_eq!(
&logged[0..4],
&[0u8; 4],
"the logged copy starts with the journal magic, unescaped"
);
assert!(logged[4..] == payload[4..]);
assert_eq!(read(target), vec![0u8; BS as usize], "cut too late");
let fs = Filesystem::mount(dev.clone()).expect("remount");
assert!(
crate::jbd2::read_superblock(&fs)
.expect("journal superblock")
.expect("a journal")
.is_clean(),
"the remount did not replay the journal"
);
drop(fs);
assert!(
read(target) == payload,
"replay did not restore the block, magic and all"
);
}
}
mod needs_host {
use super::*;
use crate::block_io::FileDevice;
use std::fs;
use std::sync::Arc;
fn copy_to_tmp(name: &str, tag: &str) -> String {
use std::sync::atomic::{AtomicU32, Ordering};
static COUNTER: AtomicU32 = AtomicU32::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let src = fs_ext4_test_support::fixture(env!("CARGO_MANIFEST_DIR"), name);
let dst =
fs_ext4_test_support::temp_path!("fs_ext4_jw_{}_{tag}_{n}.img", std::process::id());
fs::copy(&src, &dst).unwrap_or_else(|e| panic!("copy {src} to {dst}: {e}"));
dst
}
#[test]
fn empty_transaction_is_no_op() {
let path = copy_to_tmp("ext4-basic.img", "empty_tx");
let dev = FileDevice::open_rw(&path).expect("open rw");
let fs = Filesystem::mount(Arc::new(dev)).expect("mount");
let mut jw = JournalWriter::open(&fs).expect("open writer").expect(
"ext4-basic.img is built with has_journal (test-disks/guest-build-images.sh)",
);
let initial_seq = jw.jsb.sequence;
let tx = jw.begin();
jw.commit(fs.dev.as_ref(), &tx).expect("commit");
assert_eq!(
jw.jsb.sequence,
initial_seq.wrapping_add(1),
"sequence should advance even for a no-write commit"
);
assert_eq!(jw.jsb.start, 0, "should be clean after commit");
fs::remove_file(path).ok();
}
}
}