ext2: Support indirect/double-indirect blocks

To allow files larger than (4*12)KiB, support indirect blocks and
double-indirect blocks so it now supports ~4GB files.

BUG=b:342937441
TEST=cargo test in /ext2

Change-Id: I0b8fd421e85d0a5e203876aa260077448f740737
Reviewed-on: https://chromium-review.googlesource.com/c/crosvm/crosvm/+/5569436
Reviewed-by: Junichi Uekawa <uekawa@chromium.org>
Commit-Queue: Keiichi Watanabe <keiichiw@chromium.org>
Reviewed-by: Takaya Saeki <takayas@chromium.org>
This commit is contained in:
Keiichi Watanabe 2024-05-27 11:40:12 +09:00 committed by crosvm LUCI
parent c9f4ad0da9
commit 80dc747e0d
3 changed files with 167 additions and 23 deletions

View file

@ -133,8 +133,10 @@ struct FileMappingInfo {
start_block: BlockId,
/// The file to be mmap'd.
file: File,
/// The size of the file to be mmap'd.
file_size: usize,
/// The length of the mapping.
length: usize,
/// Offset in the file to start the mapping.
file_offset: usize,
}
/// A struct to represent an ext2 filesystem.
@ -273,7 +275,7 @@ impl<'a> Ext2<'a> {
if !self.dentries.contains_key(&parent) {
let block_id = self.allocate_block()?;
let inode = self.get_inode_mut(parent)?;
inode.block.set_block_id(0, &block_id);
inode.block.set_direct_blocks(&[block_id])?;
inode.blocks = block_size as u32 / 512;
self.dentries.insert(
parent,
@ -410,6 +412,38 @@ impl<'a> Ext2<'a> {
Ok(())
}
fn fill_indirect_block(
&mut self,
arena: &'a Arena<'a>,
indirect_table: BlockId,
file: &File,
file_size: usize,
file_offset: usize,
) -> Result<usize> {
let block_size = self.block_size() as usize;
// We use a block as a table of indirect blocks.
// So, the maximum number of blocks supported by single indirect blocks is limited by the
// maximum number of entries in one block, which is (block_size / 4) where 4 is the size of
// int.
let max_num_blocks = block_size / 4;
let max_data_len = max_num_blocks * block_size;
let length = std::cmp::min(file_size - file_offset, max_data_len);
let block_num = length.div_ceil(block_size);
let blocks = self.allocate_contiguous_blocks(block_num as u16)?;
let slice = arena.allocate_slice(indirect_table, 0, 4 * block_num)?;
slice.copy_from_slice(blocks.as_bytes());
self.fd_mappings.push(FileMappingInfo {
start_block: blocks[0],
length,
file: file.try_clone()?,
file_offset,
});
Ok(length)
}
fn add_file(
&mut self,
arena: &'a Arena<'a>,
@ -426,26 +460,74 @@ impl<'a> Ext2<'a> {
let block_size = self.block_size() as usize;
let mut block = InodeBlock::default();
let block_num = file_size.div_ceil(block_size);
if block_num > 12 {
// TODO(b/342937441): Support indirect blocks.
bail!("indirect data block are not yet supported");
}
if block_num > 0 {
let mut written = 0;
let mut used_blocks = 0;
if file_size > 0 {
let block_num = std::cmp::min(
file_size.div_ceil(block_size),
InodeBlock::NUM_DIRECT_BLOCKS,
);
let blocks = self.allocate_contiguous_blocks(block_num as u16)?;
let length = std::cmp::min(file_size, block_size * InodeBlock::NUM_DIRECT_BLOCKS);
self.fd_mappings.push(FileMappingInfo {
start_block: blocks[0],
file_size,
file,
length,
file: file.try_clone()?,
file_offset: 0,
});
block.copy_from_slice(0, blocks.as_bytes());
block.set_direct_blocks(&blocks)?;
written = length;
used_blocks = block_num;
}
// Indirect data block
if written < file_size {
let indirect_table = self.allocate_block()?;
block.set_indirect_block_table(&indirect_table)?;
used_blocks += 1;
let length =
self.fill_indirect_block(arena, indirect_table, &file, file_size, written)?;
written += length;
used_blocks += length.div_ceil(block_size);
}
// Double-indirect data block
// Supporting double-indirect data block allows storing ~4GB files if 4GB block size is
// used.
if written < file_size {
let d_indirect_table = self.allocate_block()?;
block.set_double_indirect_block_table(&d_indirect_table)?;
used_blocks += 1;
let mut indirect_blocks: Vec<BlockId> = vec![];
// Iterate (block_size / 4) times, as each block id is 4-byte.
for _ in 0..block_size / 4 {
if written >= file_size {
break;
}
let indirect_table = self.allocate_block()?;
indirect_blocks.push(indirect_table);
used_blocks += 1;
let length =
self.fill_indirect_block(arena, indirect_table, &file, file_size, written)?;
written += length;
used_blocks += length.div_ceil(block_size);
}
let d_table = arena.allocate_slice(d_indirect_table, 0, indirect_blocks.len() * 4)?;
d_table.copy_from_slice(indirect_blocks.as_bytes());
}
if written < file_size {
unimplemented!("Triple-indirect block is not supported");
}
// The spec says that the `blocks` field is a "32-bit value representing the total number
// of 512-bytes blocks". This `512` is a fixed number regardless of the actual block size,
// which is usuaully 4KB.
let blocks = block_num as u32 * (block_size as u32 / 512);
let blocks = used_blocks as u32 * (block_size as u32 / 512);
let size = file_size as u32;
let inode = Inode::from_metadata(
arena,
@ -533,15 +615,16 @@ pub fn create_ext2_region(cfg: &Config, src_dir: Option<&Path>) -> Result<Memory
let mut mmap_arena = MemoryMappingArena::from(mem);
for FileMappingInfo {
start_block,
file_size,
file,
length,
file_offset,
} in file_mappings
{
mmap_arena.add_fd_mapping(
u32::from(start_block) as usize * BLOCK_SIZE,
file_size,
length,
&file,
0, /* fd_offset */
file_offset as u64, /* fd_offset */
Protection::read(),
)?;
}

View file

@ -93,14 +93,45 @@ impl Default for InodeBlock {
}
impl InodeBlock {
/// Copies the given slice to `self.0.[offset..]`.
pub fn copy_from_slice(&mut self, offset: usize, src: &[u8]) {
self.0[offset..offset + src.len()].copy_from_slice(src)
}
// Each inode contains 12 direct pointers (0-11), one singly indirect pointer (12), one
// doubly indirect block pointer (13), and one triply indirect pointer (14).
pub const NUM_DIRECT_BLOCKS: usize = 12;
const INDIRECT_BLOCK_TABLE_ID: usize = Self::NUM_DIRECT_BLOCKS;
const DOUBLE_INDIRECT_BLOCK_TABLE_ID: usize = 13;
/// Set a block id at the given index.
pub fn set_block_id(&mut self, index: usize, block_id: &BlockId) {
self.copy_from_slice(index * 4, block_id.as_bytes())
fn set_block_id(&mut self, index: usize, block_id: &BlockId) -> Result<()> {
let offset = index * std::mem::size_of::<BlockId>();
let bytes = block_id.as_bytes();
if self.0.len() < offset + bytes.len() {
bail!("index out of bounds when setting block_id to InodeBlock: index={index}, block_id: {:?}", block_id);
}
self.0[offset..offset + bytes.len()].copy_from_slice(bytes);
Ok(())
}
/// Set an array of direct block IDs.
pub fn set_direct_blocks(&mut self, block_ids: &[BlockId]) -> Result<()> {
let bytes = block_ids.as_bytes();
if bytes.len() > self.0.len() {
bail!(
"length of direct blocks is {} bytes, but it must not exceed {}",
bytes.len(),
self.0.len()
);
}
self.0[..bytes.len()].copy_from_slice(bytes);
Ok(())
}
/// Set a block id to be used as the indirect block table.
pub fn set_indirect_block_table(&mut self, block_id: &BlockId) -> Result<()> {
self.set_block_id(Self::INDIRECT_BLOCK_TABLE_ID, block_id)
}
/// Set a block id to be used as the double indirect block table.
pub fn set_double_indirect_block_table(&mut self, block_id: &BlockId) -> Result<()> {
self.set_block_id(Self::DOUBLE_INDIRECT_BLOCK_TABLE_ID, block_id)
}
}

View file

@ -9,6 +9,8 @@ use std::fs::create_dir;
use std::fs::File;
use std::fs::OpenOptions;
use std::io::BufWriter;
use std::io::Seek;
use std::io::SeekFrom;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
@ -289,3 +291,31 @@ fn test_max_file_name() {
assert_eq_dirs(&td, &dir, &disk);
}
#[test]
fn test_mkfs_indirect_block() {
// testdata
// ├── big.txt (80KiB), which requires indirect blocks
// └── huge.txt (8MiB), which requires doubly indirect blocks
let td = tempdir().unwrap();
let dir = td.path().join("testdata");
std::fs::create_dir(&dir).unwrap();
let mut big = std::fs::File::create(dir.join("big.txt")).unwrap();
big.seek(SeekFrom::Start(80 * 1024)).unwrap();
big.write_all(&[0]).unwrap();
let mut huge = std::fs::File::create(dir.join("huge.txt")).unwrap();
huge.seek(SeekFrom::Start(8 * 1024 * 1024)).unwrap();
huge.write_all(&[0]).unwrap();
let disk = mkfs(
&td,
&Config {
blocks_per_group: 4096,
inodes_per_group: 4096,
},
Some(&dir),
);
assert_eq_dirs(&td, &dir, &disk);
}