Skip to content

Commit 6cbf7c5

Browse files
committed
feat : Added 4 kb block hashes with a ranged read that only verifies the blocks it touch
1 parent b837aa3 commit 6cbf7c5

1 file changed

Lines changed: 129 additions & 28 deletions

File tree

src/vfs.rs

Lines changed: 129 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ use alloc::vec::Vec;
22
use alloc::string::String;
33
use crate::provenance::provenance_hash;
44
use crate::println;
5-
use crate::task::CryptoVerificationJob;
5+
6+
/// Provenance block granularity (standard 4 KiB, matching fs-verity/dm-verity).
7+
pub const BLOCK_SIZE: usize = 4096;
68

79
#[derive(Debug, Clone)]
810
pub enum FileType {
@@ -15,26 +17,72 @@ pub struct FileNode {
1517
pub name: String,
1618
pub file_type: FileType,
1719
pub data: Vec<u8>,
20+
/// Whole-file BLAKE3 (baseline path; goes stale after a partial write_block).
1821
pub provenance_hash: [u8; 32],
22+
/// Per-BLOCK_SIZE BLAKE3 leaves; a range read verifies only touched blocks.
23+
pub block_hashes: Vec<[u8; 32]>,
24+
/// BLAKE3 Merkle root over the leaves, recomputed lazily when dirty.
25+
merkle_root: [u8; 32],
26+
root_dirty: bool,
27+
}
28+
29+
/// One BLAKE3 hash per BLOCK_SIZE chunk (last chunk may be short).
30+
fn compute_block_hashes(data: &[u8]) -> Vec<[u8; 32]> {
31+
data.chunks(BLOCK_SIZE).map(|c| provenance_hash(c)).collect()
32+
}
33+
34+
/// BLAKE3 Merkle root over the leaves: pairs hashed bottom-up, a lone node is
35+
/// promoted unchanged. Empty input -> zero root.
36+
fn compute_merkle_root(leaves: &[[u8; 32]]) -> [u8; 32] {
37+
if leaves.is_empty() {
38+
return [0u8; 32];
39+
}
40+
let mut level: Vec<[u8; 32]> = leaves.to_vec();
41+
while level.len() > 1 {
42+
let mut next: Vec<[u8; 32]> = Vec::with_capacity((level.len() + 1) / 2);
43+
let mut i = 0;
44+
while i < level.len() {
45+
if i + 1 < level.len() {
46+
let mut buf = [0u8; 64];
47+
buf[..32].copy_from_slice(&level[i]);
48+
buf[32..].copy_from_slice(&level[i + 1]);
49+
next.push(provenance_hash(&buf));
50+
} else {
51+
next.push(level[i]);
52+
}
53+
i += 2;
54+
}
55+
level = next;
56+
}
57+
level[0]
1958
}
2059

2160
impl FileNode {
2261
pub fn new_file(name: &str, data: &[u8]) -> Self {
2362
let hash = provenance_hash(data);
63+
let block_hashes = compute_block_hashes(data);
64+
let merkle_root = compute_merkle_root(&block_hashes);
2465
FileNode {
2566
name: String::from(name),
2667
file_type: FileType::Regular,
2768
data: Vec::from(data),
2869
provenance_hash: hash,
70+
block_hashes,
71+
merkle_root,
72+
root_dirty: false,
2973
}
3074
}
3175

3276
pub fn new_file_untrusted(name: &str, data: &[u8]) -> Self {
77+
let nblocks = (data.len() + BLOCK_SIZE - 1) / BLOCK_SIZE;
3378
FileNode {
3479
name: String::from(name),
3580
file_type: FileType::Regular,
3681
data: Vec::from(data),
3782
provenance_hash: [0u8; 32], // deliberately invalid — always fails verify()
83+
block_hashes: alloc::vec![[0u8; 32]; nblocks], // also fails verify_range()
84+
merkle_root: [0u8; 32],
85+
root_dirty: false,
3886
}
3987
}
4088

@@ -44,6 +92,9 @@ impl FileNode {
4492
file_type: FileType::Directory,
4593
data: Vec::new(),
4694
provenance_hash: [0u8; 32],
95+
block_hashes: Vec::new(),
96+
merkle_root: [0u8; 32],
97+
root_dirty: false,
4798
}
4899
}
49100

@@ -53,6 +104,54 @@ impl FileNode {
53104
&self.provenance_hash
54105
)
55106
}
107+
108+
/// Block-level: verify only the leaves overlapping [offset, offset+len).
109+
pub fn verify_range(&self, offset: usize, len: usize) -> bool {
110+
if len == 0 {
111+
return true;
112+
}
113+
let end = core::cmp::min(offset + len, self.data.len());
114+
if offset >= end {
115+
return false;
116+
}
117+
let start_block = offset / BLOCK_SIZE;
118+
let end_block = (end - 1) / BLOCK_SIZE;
119+
for b in start_block..=end_block {
120+
let bstart = b * BLOCK_SIZE;
121+
let bend = core::cmp::min(bstart + BLOCK_SIZE, self.data.len());
122+
let live = provenance_hash(&self.data[bstart..bend]);
123+
match self.block_hashes.get(b) {
124+
Some(stored) if crate::provenance::constant_time_eq(&live, stored) => {}
125+
_ => return false,
126+
}
127+
}
128+
true
129+
}
130+
131+
/// Lazily return the Merkle root, recomputing from leaves only if dirty.
132+
pub fn merkle_root(&mut self) -> [u8; 32] {
133+
if self.root_dirty {
134+
self.merkle_root = compute_merkle_root(&self.block_hashes);
135+
self.root_dirty = false;
136+
}
137+
self.merkle_root
138+
}
139+
140+
/// Overwrite one block: updates that leaf and marks the root dirty, so a
141+
/// single-block write costs one block hash instead of a full-file rehash.
142+
/// The whole-file `provenance_hash` (baseline only) is left stale.
143+
pub fn write_block(&mut self, idx: usize, new_data: &[u8]) -> bool {
144+
let bstart = idx * BLOCK_SIZE;
145+
if bstart >= self.data.len() || idx >= self.block_hashes.len() {
146+
return false;
147+
}
148+
let bend = core::cmp::min(bstart + BLOCK_SIZE, self.data.len());
149+
let n = core::cmp::min(new_data.len(), bend - bstart);
150+
self.data[bstart..bstart + n].copy_from_slice(&new_data[..n]);
151+
self.block_hashes[idx] = provenance_hash(&self.data[bstart..bend]);
152+
self.root_dirty = true;
153+
true
154+
}
56155
}
57156

58157
pub struct VirtualFS {
@@ -85,6 +184,35 @@ impl VirtualFS {
85184
Some(file.data.as_slice())
86185
}
87186

187+
/// Block-level verified ranged read: verifies only the blocks overlapping
188+
/// the range. Cost scales with bytes read, not total file size.
189+
pub fn read_range(&self, name: &str, offset: usize, len: usize) -> Option<&[u8]> {
190+
let file = self.files.iter().find(|f| f.name == name)?;
191+
let end = core::cmp::min(offset + len, file.data.len());
192+
if offset >= end {
193+
return None;
194+
}
195+
if !file.verify_range(offset, len) {
196+
println!("[AXIOM KERNEL] READ BLOCKED: \"{}\" block {} provenance violation",
197+
name, offset / BLOCK_SIZE);
198+
return None;
199+
}
200+
Some(&file.data[offset..end])
201+
}
202+
203+
/// Overwrite one block of a file (exercises the lazy Merkle path).
204+
pub fn write_block(&mut self, name: &str, idx: usize, data: &[u8]) -> bool {
205+
match self.files.iter_mut().find(|f| f.name == name) {
206+
Some(f) => f.write_block(idx, data),
207+
None => false,
208+
}
209+
}
210+
211+
/// Current Merkle root for a file (recomputed lazily if dirty).
212+
pub fn merkle_root(&mut self, name: &str) -> Option<[u8; 32]> {
213+
self.files.iter_mut().find(|f| f.name == name).map(|f| f.merkle_root())
214+
}
215+
88216
pub fn verify(&self, name: &str) -> Option<bool> {
89217
self.files.iter()
90218
.find(|f| f.name == name)
@@ -106,31 +234,4 @@ impl VirtualFS {
106234
}
107235
}
108236
}
109-
}
110-
111-
// Atomic State Machine Definitions for Asynchronous Verification
112-
pub const STATE_PENDING: u8 = 0;
113-
pub const STATE_VERIFIED: u8 = 1;
114-
pub const STATE_CORRUPTED: u8 = 2;
115-
116-
pub struct CachedVfsBlock {
117-
pub block_id: u64,
118-
pub data: [u8; 4096],
119-
pub lineage_token: u64,
120-
pub status: core::sync::atomic::AtomicU8,
121-
}
122-
123-
pub fn read_block_async(block: &mut CachedVfsBlock) {
124-
block.status.store(STATE_PENDING, core::sync::atomic::Ordering::Relaxed);
125-
126-
let job = CryptoVerificationJob {
127-
block_ptr: block as *mut CachedVfsBlock,
128-
expected_hash: [0u8; 32],
129-
};
130-
131-
if let Some(queue) = crate::task::VERIFICATION_QUEUE.get() {
132-
let _ = queue.push(job);
133-
}
134-
135-
crate::task::yield_current_thread();
136237
}

0 commit comments

Comments
 (0)