add code to check if space in file is lost

This commit is contained in:
soruh 2023-08-03 04:12:22 +02:00
parent 97d5a05671
commit 42a2aef527
3 changed files with 332 additions and 12 deletions

View File

@ -6,14 +6,14 @@ use zerocopy::{AsBytes, FromBytes, FromZeroes, Unaligned};
use crate::{Db, FilePointer, FileRange, PagePointer, RawFilePointer, PAGE_SIZE, U16, U32, U64};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum SlabKind {
pub enum SlabKind {
SingleBytes,
RelativeFreeList,
AbsoluteFreeList,
}
impl SlabKind {
fn for_size(size: u32) -> Self {
pub fn for_size(size: u32) -> Self {
if size == 1 {
Self::SingleBytes
} else if size < size_of::<RawFilePointer>() as u32 {
@ -313,7 +313,7 @@ impl GeneralPurposeAllocator {
}
}
fn div_round_up(a: u64, b: u64) -> u64 {
pub(crate) fn div_round_up(a: u64, b: u64) -> u64 {
(a + b - 1) / b
}
@ -353,7 +353,10 @@ impl<'db, R> Iterator for SlabListIterator<'db, R> {
impl SlabListHeader {
pub fn capacity(&self) -> u32 {
(self.size.get() - size_of::<SlabListHeader>() as u32) / size_of::<Slab>() as u32
(self.size() - size_of::<SlabListHeader>() as u32) / size_of::<Slab>() as u32
}
pub fn size(&self) -> u32 {
self.size.get()
}
}
@ -364,7 +367,7 @@ impl SlabListPointer {
(!ptr.0.is_null()).then_some(ptr)
}
fn read_header<R>(self, db: &Db<R>) -> SlabListHeader {
pub fn read_header<R>(self, db: &Db<R>) -> SlabListHeader {
unsafe { db.read(self.0) }
}
@ -458,9 +461,9 @@ pub struct Slab {
#[derive(Clone, Copy, FromBytes, FromZeroes, AsBytes, Unaligned)]
#[repr(C)]
struct RelativeFreeListHeader {
next_page: PagePointer,
first: U16,
pub struct RelativeFreeListHeader {
pub next_page: PagePointer,
pub first: U16,
}
impl RelativeFreeListHeader {
@ -580,10 +583,14 @@ impl SlabPointer {
}
}
pub fn set_head<R>(&self, db: &mut Db<R>, next: RawFilePointer) {
fn set_head<R>(&self, db: &mut Db<R>, next: RawFilePointer) {
self.modify(db).head = next;
}
pub fn head<R>(&self, db: &Db<R>) -> RawFilePointer {
self.read(db).head
}
pub fn allocate_page<R>(&self, db: &mut Db<R>) -> RawFilePointer {
let Slab { head, size } = self.read(db);

View File

@ -196,6 +196,9 @@ impl PagePointer {
fn null() -> Self {
Self::nth(0)
}
fn is_null(self) -> bool {
self == Self::null()
}
}
#[derive(Clone, Copy, FromBytes, FromZeroes, AsBytes, Unaligned, PartialEq, Eq)]
@ -286,6 +289,12 @@ pub struct Reader<R> {
state: Arc<AtomicArc<Snapshot<R>>>,
}
impl<R> Reader<R> {
fn get(&self) -> Arc<Snapshot<R>> {
self.state.get()
}
}
pub struct Db<R> {
file: File,
map: MmapMut,
@ -616,9 +625,12 @@ impl<R> Db<R> {
#[cfg(test)]
mod tests {
use crate::allocator::{div_round_up, RelativeFreeListHeader, SlabKind};
use super::*;
use mapped::ReaderTrait;
use std::io::Write;
use std::ops::Shl;
use std::process::Stdio;
#[derive(Debug, Clone, Copy)]
@ -829,10 +841,13 @@ mod tests {
let mut snapshots = VecDeque::new();
for i in 0..20 {
dbg!(i);
db.transaction(|transaction| {
let root = transaction.root();
let root = if root.is_null() {
let root = if !root.is_null() {
root
} else {
let (root, data) = transaction.allocate::<DataHeader>();
*data = DataHeader {
@ -840,8 +855,6 @@ mod tests {
list: FilePointer::null(),
};
root
} else {
root
};
@ -879,6 +892,23 @@ mod tests {
root
});
validate_db(&db, |snaphot, coverage| {
coverage.set_allocated(snaphot.root.range());
let data = snaphot.read(snaphot.root);
let mut next = data.list;
while !next.is_null() {
coverage.set_allocated(next.range());
next = snaphot.read(next).next;
}
for SnapshotAndFreeList { to_free, .. } in &db.snapshots {
for &range in to_free {
coverage.set_range(range, CoverageKind::Free);
}
}
});
snapshots.push_back(db.create_reader().state.get());
if snapshots.len() > 10 {
drop(snapshots.pop_front());
@ -886,6 +916,8 @@ mod tests {
}
}
// TODO: allocate some variably sized strings
for (i, snapshot) in snapshots.iter().enumerate() {
let root = snapshot.read(snapshot.root);
@ -919,6 +951,283 @@ mod tests {
// hexdump(db.map.as_bytes());
}
#[repr(u8)]
#[derive(Clone, Copy)]
enum CoverageKind {
Unaccounted = 0b00,
Allocated = 0b01,
Free = 0b10,
Metadata = 0b11,
}
impl CoverageKind {
fn color(self) -> &'static str {
match self {
CoverageKind::Unaccounted => "31",
CoverageKind::Allocated => "32",
CoverageKind::Free => "34",
CoverageKind::Metadata => "35",
}
}
}
impl CoverageKind {
fn from_bits(a: bool, b: bool) -> Self {
let res = match (a, b) {
(false, false) => Self::Unaccounted,
(false, true) => Self::Allocated,
(true, false) => Self::Free,
(true, true) => Self::Metadata,
};
assert_eq!(res as u8, ((a as u8) << 1) + b as u8);
res
}
fn to_bits(self) -> (bool, bool) {
(self as u8 & 0b10 != 0, self as u8 & 0b01 != 0)
}
}
struct CoverageMap {
data_0: Vec<u8>,
data_1: Vec<u8>,
empty_bits: u8,
}
impl CoverageMap {
fn new(len: usize) -> Self {
let bits = div_round_up(len as u64, 8) as usize;
Self {
data_0: vec![0; bits],
data_1: vec![0; bits],
empty_bits: (8 - len % 8) as u8,
}
}
#[must_use]
fn set(&mut self, i: usize, kind: CoverageKind) -> bool {
let i_byte = i / 8;
let i_bit = i % 8;
let mask = 1 << i_bit;
let (set_0, set_1) = kind.to_bits();
if i_byte >= self.data_0.len() {
return false;
}
let is_set = self.data_0[i_byte] & mask != 0 || self.data_1[i_byte] & mask != 0;
if is_set {
return false;
}
self.data_0[i_byte] |= mask * set_0 as u8;
self.data_1[i_byte] |= mask * set_1 as u8;
true
}
#[must_use]
fn try_set_range(&mut self, range: FileRange, kind: CoverageKind) -> bool {
range.as_range().all(|i| self.set(i, kind))
}
fn set_allocated(&mut self, range: FileRange) {
self.set_range(range, CoverageKind::Allocated);
}
fn set_range(&mut self, range: FileRange, kind: CoverageKind) {
assert!(
self.try_set_range(range, kind),
"possible allocator corruption"
)
}
fn all_covered(&self) -> bool {
let len = self.data_0.len();
for (i, (&byte_0, &byte_1)) in self.data_0.iter().zip(self.data_1.iter()).enumerate() {
let byte = byte_0 | byte_1;
if i == len - 1 {
if byte != u8::MAX.overflowing_shl(self.empty_bits as u32).0 {
return false;
}
} else if byte != u8::MAX {
return false;
}
}
true
}
fn set_color(res: &mut String, color: &str) {
res.push_str("\x1b[");
res.push_str(color);
res.push('m');
}
fn print(&self) -> String {
let mut res = String::new();
let mut prev = "";
for (i, (&byte_0, &byte_1)) in self.data_0.iter().zip(self.data_1.iter()).enumerate() {
let byte = byte_0 | byte_1;
fn all_equal(bits: u8) -> bool {
bits == 0 || bits == u8::MAX
}
let kind = if all_equal(byte_0) && all_equal(byte_1) {
Some(CoverageKind::from_bits(byte_0 & 1 == 1, byte_1 & 1 == 1))
} else {
None
};
if i != 0 {
if i as u64 % (PAGE_SIZE / 8 / 8) == 0 {
res.push('\n');
}
if i as u64 % (PAGE_SIZE / 8) == 0 {
Self::set_color(&mut res, "");
prev = "";
res.push_str(&"-".repeat((PAGE_SIZE / 8 / 8) as usize));
res.push('\n');
}
}
let color = kind.map(CoverageKind::color).unwrap_or("33");
if color != prev {
Self::set_color(&mut res, color);
}
prev = color;
res.push(char::from_u32(0x2800 + byte as u32).unwrap());
}
Self::set_color(&mut res, "");
res.push('\n');
res
}
fn assert_covered(&self) {
if !self.all_covered() {
panic!("Space in the file was lost\n{}", self.print());
}
}
}
#[test]
fn coverage_map_works() {
let mut coverage = CoverageMap::new(40);
assert!(!coverage.all_covered());
assert!(coverage.try_set_range(RawFilePointer::null().range(20), CoverageKind::Metadata));
assert!(!coverage.all_covered());
assert!(coverage.try_set_range((RawFilePointer::null() + 20).range(20), CoverageKind::Free));
assert!(coverage.all_covered());
assert!(!coverage.try_set_range(
(RawFilePointer::null() + 40).range(8),
CoverageKind::Allocated
));
assert!(!coverage.try_set_range(
(RawFilePointer::null() + 50).range(10),
CoverageKind::Allocated
));
}
fn validate_db<R>(db: &Db<R>, f: impl FnOnce(&Snapshot<R>, &mut CoverageMap)) {
let mut coverage = CoverageMap::new(db.map.len());
let snapshot = &*db.state.get();
coverage.set_range(Db::<R>::header_ptr().range(), CoverageKind::Metadata);
// general purpose
{
let head = Db::<R>::header_ptr().allocator_state_ptr().general_ptr();
let mut next = *snapshot.read(head);
while !next.is_null() {
let size = GeneralPurposeAllocator::size(db, next);
coverage.set_range(next.into_raw().range(size), CoverageKind::Free);
next = *snapshot.read(next.next_ptr());
}
}
// slabs
{
let slabs = *snapshot.read(Db::<R>::header_ptr().allocator_state_ptr().slabs_ptr());
let mut next = Some(slabs);
while let Some(slabs) = next {
coverage.set_range(
slabs
.0
.into_raw()
.range(slabs.read_header(db).size() as u64),
CoverageKind::Metadata,
);
next = slabs.next(db);
for slab in slabs.iter(db) {
let size = slab.size(db);
let head = slab.head(db);
match SlabKind::for_size(size) {
SlabKind::SingleBytes => todo!(),
SlabKind::RelativeFreeList => {
let (mut page, offset) = head.page_offset();
while !page.is_null() {
let header =
FilePointer::<RelativeFreeListHeader>::new(page.start());
coverage.set_range(header.range(), CoverageKind::Metadata);
let header = snapshot.read(header);
page = header.next_page;
let mut next = header.first.get();
while next != 0 {
let next_ptr = FilePointer::<U16>::new(
RawFilePointer::from_page_and_offset(page, next),
);
coverage.set_range(
next_ptr.into_raw().range(size as u64),
CoverageKind::Free,
);
next = snapshot.read(next_ptr).get();
}
}
todo!();
}
SlabKind::AbsoluteFreeList => {
let mut next = head;
while !next.is_null() {
let next_ptr = FilePointer::<RawFilePointer>::new(next);
coverage.set_range(
next_ptr.into_raw().range(size as u64),
CoverageKind::Free,
);
next = *snapshot.read(next_ptr);
}
}
}
}
}
}
f(snapshot, &mut coverage);
print!("{}", coverage.print());
if !coverage.all_covered() {
panic!("space in the file was lost...");
}
}
fn hexdump(bytes: &[u8]) {
let mut child = std::process::Command::new("hexdump")
.arg("-C")

View File

@ -48,6 +48,10 @@ impl<'t, R> TransactionHandle<'t, R> {
fn read_ptr_raw(&self, range: FileRange) -> FileRange {
if let Some(&replaced) = self.replaced.get(&range.start) {
assert_eq!(replaced.from, range);
// TODO: replacing this with unwrap_or(range)
// will access the original region, which can't have actually been
// allocated, but was logically freed.
replaced.to.expect("use after free")
} else if let Some(&new) = self.new.get(&range.start) {
assert_eq!(new, range);