use std::collections::BTreeMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;
use super::backend::{Errno, FsBackend};
use super::protocol::{
RemoveMappingIn, RemoveMappingOne, SetupMappingIn, FUSE_SETUPMAPPING_FLAG_READ,
FUSE_SETUPMAPPING_FLAG_WRITE,
};
pub const DAX_PROT_READ: u32 = 1 << 0;
pub const DAX_PROT_WRITE: u32 = 1 << 1;
pub trait HvfMapper: Send + Sync {
fn map(&self, host_va: *mut u8, gpa: u64, len: u64, prot: u32) -> Result<(), Errno>;
fn unmap(&self, gpa: u64, len: u64) -> Result<(), Errno>;
}
pub struct MockHvfMapper {
log: Mutex<Vec<MockCall>>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MockCall {
Map {
host_va: usize,
gpa: u64,
len: u64,
prot: u32,
},
Unmap {
gpa: u64,
len: u64,
},
}
impl MockHvfMapper {
pub fn new() -> Self {
Self {
log: Mutex::new(Vec::new()),
}
}
pub fn calls(&self) -> Vec<MockCall> {
self.log.lock().unwrap().clone()
}
}
impl Default for MockHvfMapper {
fn default() -> Self {
Self::new()
}
}
impl HvfMapper for MockHvfMapper {
fn map(&self, host_va: *mut u8, gpa: u64, len: u64, prot: u32) -> Result<(), Errno> {
self.log.lock().unwrap().push(MockCall::Map {
host_va: host_va as usize,
gpa,
len,
prot,
});
Ok(())
}
fn unmap(&self, gpa: u64, len: u64) -> Result<(), Errno> {
self.log.lock().unwrap().push(MockCall::Unmap { gpa, len });
Ok(())
}
}
#[derive(Clone, Debug)]
struct Slot {
host_va: Option<*mut u8>,
len: u64,
nodeid: u64,
foffset: u64,
prot: u32,
}
unsafe impl Send for Slot {}
unsafe impl Sync for Slot {}
pub struct DaxSession {
dax_base_gpa: u64,
dax_window_len: u64,
cap_bytes: u64,
current_mapped_bytes: AtomicU64,
backend: std::sync::Arc<dyn FsBackend>,
mapper: std::sync::Arc<dyn HvfMapper>,
active: Mutex<BTreeMap<u64, Slot>>,
}
impl DaxSession {
pub fn new(
dax_base_gpa: u64,
dax_window_len: u64,
backend: std::sync::Arc<dyn FsBackend>,
mapper: std::sync::Arc<dyn HvfMapper>,
) -> Self {
Self::with_cap(
dax_base_gpa,
dax_window_len,
dax_window_len,
backend,
mapper,
)
}
pub fn with_cap(
dax_base_gpa: u64,
dax_window_len: u64,
cap_bytes: u64,
backend: std::sync::Arc<dyn FsBackend>,
mapper: std::sync::Arc<dyn HvfMapper>,
) -> Self {
Self {
dax_base_gpa,
dax_window_len,
cap_bytes,
current_mapped_bytes: AtomicU64::new(0),
backend,
mapper,
active: Mutex::new(BTreeMap::new()),
}
}
pub fn setup(&self, nodeid: u64, req: &SetupMappingIn) -> Result<(), Errno> {
log_dax(|| {
format!(
"SETUPMAPPING: nodeid={nodeid} fh={} foffset={:#x} len={:#x} flags={:#x} moffset={:#x}",
req.fh, req.foffset, req.len, req.flags, req.moffset
)
});
if (req.moffset & 0x3FFF) != 0 || (req.len & 0x3FFF) != 0 {
return Err(super::backend::EINVAL);
}
if req
.moffset
.checked_add(req.len)
.is_none_or(|end| end > self.dax_window_len)
{
return Err(super::backend::EINVAL);
}
let mut current = self.current_mapped_bytes.load(Ordering::Acquire);
loop {
let proposed = match current.checked_add(req.len) {
Some(v) if v <= self.cap_bytes => v,
_ => return Err(super::backend::ENOSPC),
};
match self.current_mapped_bytes.compare_exchange_weak(
current,
proposed,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => break,
Err(observed) => current = observed,
}
}
let mut prot = 0u32;
if req.flags & FUSE_SETUPMAPPING_FLAG_READ != 0 {
prot |= DAX_PROT_READ;
}
if req.flags & FUSE_SETUPMAPPING_FLAG_WRITE != 0 {
prot |= DAX_PROT_WRITE;
}
if prot == 0 {
self.current_mapped_bytes
.fetch_sub(req.len, Ordering::AcqRel);
return Err(super::backend::EINVAL);
}
let host_va = match self
.backend
.dax_map(nodeid, req.fh, req.foffset, req.len, prot)
{
Ok(va) => va,
Err(e) => {
log_dax(|| format!("backend.dax_map FAILED: errno={e}"));
self.current_mapped_bytes
.fetch_sub(req.len, Ordering::AcqRel);
return Err(e);
}
};
let gpa = self.dax_base_gpa + req.moffset;
log_dax(|| {
format!(
"hv_vm_map: host_va={:#x} gpa={gpa:#x} len={:#x} prot={prot:#x}",
host_va as usize, req.len
)
});
self.mapper
.map(host_va, gpa, req.len, prot)
.inspect_err(|&e| {
log_dax(|| format!("mapper.map FAILED: errno={e}"));
let _ = self.backend.dax_unmap(nodeid, host_va, req.len);
self.current_mapped_bytes
.fetch_sub(req.len, Ordering::AcqRel);
})?;
let mut active = self.active.lock().unwrap();
if active.contains_key(&req.moffset) {
let _ = self.mapper.unmap(gpa, req.len);
let _ = self.backend.dax_unmap(nodeid, host_va, req.len);
self.current_mapped_bytes
.fetch_sub(req.len, Ordering::AcqRel);
return Err(super::backend::EEXIST);
}
active.insert(
req.moffset,
Slot {
host_va: Some(host_va),
len: req.len,
nodeid,
foffset: req.foffset,
prot,
},
);
Ok(())
}
pub fn remove(&self, nodeid: u64, entry: &RemoveMappingOne) -> Result<(), Errno> {
let mut active = self.active.lock().unwrap();
let slot = active
.remove(&entry.moffset)
.ok_or(super::backend::EINVAL)?;
if slot.len != entry.len {
active.insert(entry.moffset, slot);
return Err(super::backend::EINVAL);
}
let gpa = self.dax_base_gpa + entry.moffset;
let released_len = slot.len;
drop(active);
let mapper_res = if slot.host_va.is_some() {
self.mapper.unmap(gpa, entry.len)
} else {
Ok(())
};
let backend_res = if let Some(host_va) = slot.host_va {
self.backend.dax_unmap(nodeid, host_va, slot.len)
} else {
Ok(())
};
self.current_mapped_bytes
.fetch_sub(released_len, Ordering::AcqRel);
mapper_res.and(backend_res)
}
pub fn current_mapped_bytes(&self) -> u64 {
self.current_mapped_bytes.load(Ordering::Acquire)
}
pub fn active_slot_count(&self) -> usize {
self.active.lock().unwrap().len()
}
pub fn rebind_all(&self) -> Result<usize, Errno> {
let gpas: Vec<u64> = {
let active = self.active.lock().unwrap();
active
.iter()
.filter(|(_, s)| s.host_va.is_none())
.map(|(moffset, _)| self.dax_base_gpa + moffset)
.collect()
};
let mut bound = 0usize;
for gpa in gpas {
if self.handle_stage2_fault(gpa)? {
bound += 1;
}
}
Ok(bound)
}
pub fn snapshot_state(&self) -> Vec<u8> {
let active = self.active.lock().unwrap();
let entry_count = active.len() as u64;
let mut out = Vec::with_capacity(8 + 4 + 8 * 5 + (active.len() * 48));
out.extend_from_slice(b"DAXS");
out.extend_from_slice(&1u32.to_le_bytes());
out.extend_from_slice(&self.dax_base_gpa.to_le_bytes());
out.extend_from_slice(&self.dax_window_len.to_le_bytes());
out.extend_from_slice(&self.cap_bytes.to_le_bytes());
out.extend_from_slice(
&self
.current_mapped_bytes
.load(Ordering::Acquire)
.to_le_bytes(),
);
out.extend_from_slice(&entry_count.to_le_bytes());
for (moffset, slot) in active.iter() {
out.extend_from_slice(&moffset.to_le_bytes());
out.extend_from_slice(&slot.len.to_le_bytes());
out.extend_from_slice(&slot.nodeid.to_le_bytes());
out.extend_from_slice(&slot.foffset.to_le_bytes());
out.extend_from_slice(&slot.prot.to_le_bytes());
out.extend_from_slice(&0u32.to_le_bytes());
}
out
}
pub fn restore_state(&self, blob: &[u8]) -> Result<(), std::io::Error> {
fn take<'a>(b: &'a [u8], p: &mut usize, n: usize) -> Result<&'a [u8], std::io::Error> {
if *p + n > b.len() {
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"dax snapshot truncated",
));
}
let s = &b[*p..*p + n];
*p += n;
Ok(s)
}
fn read_u32(b: &[u8], p: &mut usize) -> Result<u32, std::io::Error> {
let s = take(b, p, 4)?;
Ok(u32::from_le_bytes([s[0], s[1], s[2], s[3]]))
}
fn read_u64(b: &[u8], p: &mut usize) -> Result<u64, std::io::Error> {
let s = take(b, p, 8)?;
Ok(u64::from_le_bytes([
s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7],
]))
}
let mut p = 0usize;
let magic = take(blob, &mut p, 4)?;
if magic != b"DAXS" {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"dax snapshot bad magic",
));
}
let version = read_u32(blob, &mut p)?;
if version != 1 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("dax snapshot version {version} unsupported"),
));
}
let snap_base = read_u64(blob, &mut p)?;
let snap_window = read_u64(blob, &mut p)?;
if snap_base != self.dax_base_gpa || snap_window != self.dax_window_len {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"dax snapshot window mismatch: snap=({:#x}, {:#x}) self=({:#x}, {:#x})",
snap_base, snap_window, self.dax_base_gpa, self.dax_window_len
),
));
}
let _cap_bytes = read_u64(blob, &mut p)?;
let _stored_current = read_u64(blob, &mut p)?;
let entry_count = read_u64(blob, &mut p)?;
let mut new_entries: BTreeMap<u64, Slot> = BTreeMap::new();
let mut sum_len: u64 = 0;
for _ in 0..entry_count {
let moffset = read_u64(blob, &mut p)?;
let len = read_u64(blob, &mut p)?;
let nodeid = read_u64(blob, &mut p)?;
let foffset = read_u64(blob, &mut p)?;
let prot = read_u32(blob, &mut p)?;
let _reserved = read_u32(blob, &mut p)?;
if moffset
.checked_add(len)
.is_none_or(|end| end > self.dax_window_len)
{
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"dax snapshot entry out of window: moffset={:#x} len={:#x} window={:#x}",
moffset, len, self.dax_window_len
),
));
}
sum_len = sum_len.saturating_add(len);
new_entries.insert(
moffset,
Slot {
host_va: None,
len,
nodeid,
foffset,
prot,
},
);
}
let mut active = self.active.lock().unwrap();
active.clear();
for (k, v) in new_entries {
active.insert(k, v);
}
drop(active);
self.current_mapped_bytes.store(sum_len, Ordering::Release);
Ok(())
}
pub fn handle_stage2_fault(&self, gpa: u64) -> Result<bool, Errno> {
if gpa < self.dax_base_gpa {
return Ok(false);
}
let moffset_fault = gpa - self.dax_base_gpa;
if moffset_fault >= self.dax_window_len {
return Ok(false);
}
let active = self.active.lock().unwrap();
let (&slot_moffset, slot) = match active.range(..=moffset_fault).next_back() {
Some(kv) => kv,
None => return Ok(false),
};
if moffset_fault >= slot_moffset + slot.len {
return Ok(false);
}
if slot.host_va.is_some() {
return Ok(true);
}
let nodeid = slot.nodeid;
let foffset = slot.foffset;
let len = slot.len;
let prot = slot.prot;
let slot_gpa_start = self.dax_base_gpa + slot_moffset;
drop(active);
log_dax(|| {
format!(
"restore-fault: re-map nodeid={nodeid} foffset={foffset:#x} \
len={len:#x} prot={prot:#x} gpa={slot_gpa_start:#x}",
)
});
let host_va = self.backend.dax_map(nodeid, u64::MAX, foffset, len, prot)?;
if let Err(e) = self.mapper.map(host_va, slot_gpa_start, len, prot) {
let _ = self.backend.dax_unmap(nodeid, host_va, len);
return Err(e);
}
let mut active = self.active.lock().unwrap();
if let Some(s) = active.get_mut(&slot_moffset) {
if s.host_va.is_some() {
drop(active);
let _ = self.mapper.unmap(slot_gpa_start, len);
let _ = self.backend.dax_unmap(nodeid, host_va, len);
return Ok(true);
}
s.host_va = Some(host_va);
} else {
drop(active);
let _ = self.mapper.unmap(slot_gpa_start, len);
let _ = self.backend.dax_unmap(nodeid, host_va, len);
return Ok(true);
}
Ok(true)
}
}
fn log_dax<F: FnOnce() -> String>(make_msg: F) {
use std::io::Write;
let Some(target) = crate::trace::fuse_target() else {
return;
};
let s = make_msg();
let target = target.to_string_lossy().into_owned();
if target == "1" || target == "stderr" {
eprintln!("[dax] {s}");
return;
}
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&target)
{
let _ = writeln!(f, "[dax] {s}");
}
}
pub fn parse_remove_payload(payload: &[u8]) -> Result<Vec<RemoveMappingOne>, Errno> {
if payload.len() < core::mem::size_of::<RemoveMappingIn>() {
return Err(super::backend::EINVAL);
}
let hdr: RemoveMappingIn =
unsafe { core::ptr::read_unaligned(payload.as_ptr() as *const RemoveMappingIn) };
let count = hdr.count as usize;
let one_size = core::mem::size_of::<RemoveMappingOne>();
let needed = core::mem::size_of::<RemoveMappingIn>() + count * one_size;
if payload.len() < needed {
return Err(super::backend::EINVAL);
}
let mut out = Vec::with_capacity(count);
let base = payload.as_ptr();
for i in 0..count {
let off = core::mem::size_of::<RemoveMappingIn>() + i * one_size;
let one: RemoveMappingOne =
unsafe { core::ptr::read_unaligned(base.add(off) as *const RemoveMappingOne) };
out.push(one);
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fuse::backend::MemoryFs;
use std::sync::Arc;
struct StubDaxBackend {
inner: MemoryFs,
next_va: Mutex<usize>,
}
impl StubDaxBackend {
fn new() -> Self {
Self {
inner: MemoryFs::new(),
next_va: Mutex::new(0x40_0000_0000usize),
}
}
}
impl FsBackend for StubDaxBackend {
fn lookup(
&self,
p: u64,
n: &std::ffi::OsStr,
) -> Result<crate::fuse::backend::Entry, Errno> {
self.inner.lookup(p, n)
}
fn forget(&self, n: u64, nl: u64) {
self.inner.forget(n, nl)
}
fn getattr(&self, n: u64, f: Option<u64>) -> Result<crate::fuse::protocol::Attr, Errno> {
self.inner.getattr(n, f)
}
fn open(&self, n: u64, f: u32) -> Result<u64, Errno> {
self.inner.open(n, f)
}
fn read(&self, n: u64, h: u64, o: u64, s: u32) -> Result<Vec<u8>, Errno> {
self.inner.read(n, h, o, s)
}
fn release(&self, n: u64, h: u64) -> Result<(), Errno> {
self.inner.release(n, h)
}
fn opendir(&self, n: u64, f: u32) -> Result<u64, Errno> {
self.inner.opendir(n, f)
}
fn readdir(
&self,
n: u64,
h: u64,
o: u64,
s: u32,
) -> Result<Vec<crate::fuse::backend::DirEntry>, Errno> {
self.inner.readdir(n, h, o, s)
}
fn releasedir(&self, n: u64, h: u64) -> Result<(), Errno> {
self.inner.releasedir(n, h)
}
fn statfs(&self, n: u64) -> Result<crate::fuse::backend::StatFs, Errno> {
self.inner.statfs(n)
}
fn dax_map(
&self,
_nodeid: u64,
_fh: u64,
_foffset: u64,
len: u64,
_prot: u32,
) -> Result<*mut u8, Errno> {
let mut g = self.next_va.lock().unwrap();
let va = *g;
*g += len as usize;
Ok(va as *mut u8)
}
fn dax_unmap(&self, _nodeid: u64, _host_va: *mut u8, _len: u64) -> Result<(), Errno> {
Ok(())
}
}
fn make_session() -> (DaxSession, Arc<MockHvfMapper>) {
let backend = Arc::new(StubDaxBackend::new());
let mapper = Arc::new(MockHvfMapper::new());
let session = DaxSession::new(0x80_0000_0000, 0x4_0000_0000, backend, mapper.clone());
(session, mapper)
}
#[test]
fn setup_records_mapping_and_calls_mapper() {
let (sess, m) = make_session();
let req = SetupMappingIn {
fh: 1,
foffset: 0,
len: 0x4000,
flags: FUSE_SETUPMAPPING_FLAG_READ,
moffset: 0,
};
sess.setup(7, &req).unwrap();
assert_eq!(sess.active_slot_count(), 1);
let calls = m.calls();
assert_eq!(calls.len(), 1);
match &calls[0] {
MockCall::Map { gpa, len, prot, .. } => {
assert_eq!(*gpa, 0x80_0000_0000);
assert_eq!(*len, 0x4000);
assert_eq!(*prot, DAX_PROT_READ);
}
_ => panic!("expected Map"),
}
}
#[test]
fn remove_undoes_a_prior_setup() {
let (sess, m) = make_session();
let setup = SetupMappingIn {
fh: 1,
foffset: 0,
len: 0x4000,
flags: FUSE_SETUPMAPPING_FLAG_READ,
moffset: 0x1_0000,
};
sess.setup(7, &setup).unwrap();
let rem = RemoveMappingOne {
moffset: 0x1_0000,
len: 0x4000,
};
sess.remove(7, &rem).unwrap();
assert_eq!(sess.active_slot_count(), 0);
let calls = m.calls();
assert_eq!(calls.len(), 2);
match &calls[1] {
MockCall::Unmap { gpa, len } => {
assert_eq!(*gpa, 0x80_0000_0000 + 0x1_0000);
assert_eq!(*len, 0x4000);
}
_ => panic!("expected Unmap"),
}
}
#[test]
fn setup_rejects_unaligned() {
let (sess, _) = make_session();
let req = SetupMappingIn {
fh: 1,
foffset: 0,
len: 0x4000,
flags: FUSE_SETUPMAPPING_FLAG_READ,
moffset: 0x100, };
assert_eq!(
sess.setup(7, &req).unwrap_err(),
super::super::backend::EINVAL
);
}
#[test]
fn setup_rejects_out_of_window() {
let (sess, _) = make_session();
let req = SetupMappingIn {
fh: 1,
foffset: 0,
len: 0x4000,
flags: FUSE_SETUPMAPPING_FLAG_READ,
moffset: 0x4_0000_0000 - 0x4000 + 0x4000,
};
assert_eq!(
sess.setup(7, &req).unwrap_err(),
super::super::backend::EINVAL
);
}
#[test]
fn setup_rejects_moffset_len_overflow() {
let (sess, _) = make_session();
let req = SetupMappingIn {
fh: 1,
foffset: 0,
len: 0x8000,
flags: FUSE_SETUPMAPPING_FLAG_READ,
moffset: u64::MAX & !0x3FFF,
};
assert_eq!(
sess.setup(7, &req).unwrap_err(),
super::super::backend::EINVAL
);
assert_eq!(sess.current_mapped_bytes.load(Ordering::SeqCst), 0);
}
#[test]
fn setup_rejects_overlapping_existing_moffset() {
let (sess, _) = make_session();
let req = SetupMappingIn {
fh: 1,
foffset: 0,
len: 0x4000,
flags: FUSE_SETUPMAPPING_FLAG_READ,
moffset: 0,
};
sess.setup(7, &req).unwrap();
assert_eq!(
sess.setup(7, &req).unwrap_err(),
super::super::backend::EEXIST
);
}
#[test]
fn setup_rejects_zero_perms() {
let (sess, _) = make_session();
let req = SetupMappingIn {
fh: 1,
foffset: 0,
len: 0x4000,
flags: 0, moffset: 0,
};
assert_eq!(
sess.setup(7, &req).unwrap_err(),
super::super::backend::EINVAL
);
}
#[test]
fn setup_rejects_when_cap_would_be_exceeded() {
let backend = Arc::new(StubDaxBackend::new());
let mapper = Arc::new(MockHvfMapper::new());
let session = DaxSession::with_cap(
0x80_0000_0000,
0x4_0000_0000,
0x8000, backend,
mapper.clone(),
);
let mk = |moff: u64| SetupMappingIn {
fh: 1,
foffset: 0,
len: 0x4000,
flags: FUSE_SETUPMAPPING_FLAG_READ,
moffset: moff,
};
session.setup(7, &mk(0)).unwrap();
session.setup(7, &mk(0x4000)).unwrap();
let err = session.setup(7, &mk(0x8000)).unwrap_err();
assert_eq!(err, super::super::backend::ENOSPC);
assert_eq!(session.current_mapped_bytes(), 0x8000);
}
#[test]
fn remove_releases_cap_reservation() {
let backend = Arc::new(StubDaxBackend::new());
let mapper = Arc::new(MockHvfMapper::new());
let session = DaxSession::with_cap(
0x80_0000_0000,
0x4_0000_0000,
0x4000, backend,
mapper.clone(),
);
let req = SetupMappingIn {
fh: 1,
foffset: 0,
len: 0x4000,
flags: FUSE_SETUPMAPPING_FLAG_READ,
moffset: 0,
};
session.setup(7, &req).unwrap();
assert_eq!(
session.setup(7, &req.clone()).unwrap_err(),
super::super::backend::ENOSPC
);
let rem = RemoveMappingOne {
moffset: 0,
len: 0x4000,
};
session.remove(7, &rem).unwrap();
assert_eq!(session.current_mapped_bytes(), 0);
session.setup(7, &req).unwrap();
}
#[test]
fn snapshot_restore_round_trip_preserves_entries() {
let (sess, _m) = make_session();
let req = |moff: u64, foff: u64, len: u64, flags: u64| SetupMappingIn {
fh: 1,
foffset: foff,
len,
flags,
moffset: moff,
};
sess.setup(7, &req(0x0_0000, 0, 0x4000, FUSE_SETUPMAPPING_FLAG_READ))
.unwrap();
sess.setup(
11,
&req(
0x8000,
0x1_0000,
0x4000,
FUSE_SETUPMAPPING_FLAG_READ | FUSE_SETUPMAPPING_FLAG_WRITE,
),
)
.unwrap();
sess.setup(
13,
&req(0x10_0000, 0x4000, 0x8000, FUSE_SETUPMAPPING_FLAG_WRITE),
)
.unwrap();
assert_eq!(sess.active_slot_count(), 3);
let pre_bytes = sess.current_mapped_bytes();
let blob = sess.snapshot_state();
assert!(
blob.len() >= 48 + 3 * 40,
"blob.len()={} expected >= {}",
blob.len(),
48 + 3 * 40
);
assert_eq!(&blob[..4], b"DAXS");
let (sess2, m2) = make_session();
sess2.restore_state(&blob).expect("restore should succeed");
assert_eq!(sess2.active_slot_count(), 3);
assert_eq!(sess2.current_mapped_bytes(), pre_bytes);
assert!(m2.calls().is_empty(), "restore must not call mapper.map");
let active = sess2.active.lock().unwrap();
for (moff, slot) in active.iter() {
assert!(
slot.host_va.is_none(),
"restored slot at moffset={moff:#x} must start with host_va=None"
);
}
}
#[test]
fn restored_entries_have_no_host_va_until_fault() {
let (sess, _) = make_session();
let req = SetupMappingIn {
fh: 1,
foffset: 0x1000,
len: 0x4000,
flags: FUSE_SETUPMAPPING_FLAG_READ,
moffset: 0x2_0000,
};
sess.setup(42, &req).unwrap();
let blob = sess.snapshot_state();
let (sess2, _) = make_session();
sess2.restore_state(&blob).unwrap();
let active = sess2.active.lock().unwrap();
let slot = active.get(&0x2_0000).expect("entry should be restored");
assert!(slot.host_va.is_none());
assert_eq!(slot.len, 0x4000);
assert_eq!(slot.nodeid, 42);
assert_eq!(slot.foffset, 0x1000);
assert_eq!(slot.prot, DAX_PROT_READ);
}
#[test]
fn handle_stage2_fault_rehydrates_restored_slot() {
let (sess, _) = make_session();
let req = SetupMappingIn {
fh: 1,
foffset: 0x5000,
len: 0x4000,
flags: FUSE_SETUPMAPPING_FLAG_READ | FUSE_SETUPMAPPING_FLAG_WRITE,
moffset: 0x4_0000,
};
sess.setup(99, &req).unwrap();
let blob = sess.snapshot_state();
let (sess2, m2) = make_session();
sess2.restore_state(&blob).unwrap();
assert!(m2.calls().is_empty());
let fault_gpa = 0x80_0000_0000 + 0x4_0000 + 0x1234;
let handled = sess2.handle_stage2_fault(fault_gpa).unwrap();
assert!(handled, "fault inside a restored slot must be claimed");
let calls = m2.calls();
assert_eq!(calls.len(), 1);
match &calls[0] {
MockCall::Map { gpa, len, prot, .. } => {
assert_eq!(*gpa, 0x80_0000_0000 + 0x4_0000);
assert_eq!(*len, 0x4000);
assert_eq!(*prot, DAX_PROT_READ | DAX_PROT_WRITE);
}
_ => panic!("expected Map"),
}
let active = sess2.active.lock().unwrap();
assert!(active.get(&0x4_0000).unwrap().host_va.is_some());
}
#[test]
fn rebind_all_eagerly_binds_every_restored_slot() {
let (sess, _) = make_session();
for (nodeid, moffset) in [(11u64, 0x0u64), (22u64, 0x4_0000u64)] {
sess.setup(
nodeid,
&SetupMappingIn {
fh: 1,
foffset: 0,
len: 0x4000,
flags: FUSE_SETUPMAPPING_FLAG_READ,
moffset,
},
)
.unwrap();
}
let blob = sess.snapshot_state();
let (sess2, m2) = make_session();
sess2.restore_state(&blob).unwrap();
assert!(m2.calls().is_empty(), "restore must not map eagerly");
{
let active = sess2.active.lock().unwrap();
assert_eq!(active.len(), 2);
assert!(active.values().all(|s| s.host_va.is_none()));
}
let bound = sess2.rebind_all().unwrap();
assert_eq!(bound, 2);
assert_eq!(
m2.calls()
.iter()
.filter(|c| matches!(c, MockCall::Map { .. }))
.count(),
2,
"both slots must be mapped into the guest"
);
let active = sess2.active.lock().unwrap();
assert!(active.values().all(|s| s.host_va.is_some()));
drop(active);
assert_eq!(sess2.rebind_all().unwrap(), 0);
}
#[test]
fn handle_stage2_fault_out_of_window_returns_false_without_lock() {
let (sess, _) = make_session();
assert!(!sess.handle_stage2_fault(0x10000).unwrap());
assert!(!sess
.handle_stage2_fault(0x80_0000_0000 + 0x4_0000_0000 + 0x1000)
.unwrap());
}
#[test]
fn handle_stage2_fault_in_window_but_no_slot_returns_false() {
let (sess, m) = make_session();
let req = SetupMappingIn {
fh: 1,
foffset: 0,
len: 0x4000,
flags: FUSE_SETUPMAPPING_FLAG_READ,
moffset: 0x4000,
};
sess.setup(7, &req).unwrap();
let blob = sess.snapshot_state();
let (sess2, m2) = make_session();
sess2.restore_state(&blob).unwrap();
let fault_gpa = 0x80_0000_0000 + 0x10_0000;
assert!(!sess2.handle_stage2_fault(fault_gpa).unwrap());
assert!(m2.calls().is_empty());
assert!(m.calls().iter().any(|c| matches!(c, MockCall::Map { .. })));
}
#[test]
fn handle_stage2_fault_on_already_bound_slot_is_noop() {
let (sess, m) = make_session();
let req = SetupMappingIn {
fh: 1,
foffset: 0,
len: 0x4000,
flags: FUSE_SETUPMAPPING_FLAG_READ,
moffset: 0,
};
sess.setup(7, &req).unwrap();
let calls_before = m.calls().len();
assert!(sess.handle_stage2_fault(0x80_0000_0000 + 0x1234).unwrap());
let calls_after = m.calls().len();
assert_eq!(
calls_before, calls_after,
"already-bound slot must NOT trigger extra mapper calls"
);
}
#[test]
fn restore_state_rejects_malformed_blobs() {
let (sess, _) = make_session();
assert!(sess.restore_state(&[1, 2, 3]).is_err());
let mut bad = Vec::from(*b"WRONG_MAGIC");
bad.extend_from_slice(&1u32.to_le_bytes());
assert!(sess.restore_state(&bad).is_err());
let mut bad = Vec::from(*b"DAXS");
bad.extend_from_slice(&999u32.to_le_bytes());
bad.extend_from_slice(&0u64.to_le_bytes());
bad.extend_from_slice(&0u64.to_le_bytes());
bad.extend_from_slice(&0u64.to_le_bytes());
bad.extend_from_slice(&0u64.to_le_bytes());
bad.extend_from_slice(&0u64.to_le_bytes());
assert!(sess.restore_state(&bad).is_err());
let mut wrong_window = Vec::from(*b"DAXS");
wrong_window.extend_from_slice(&1u32.to_le_bytes());
wrong_window.extend_from_slice(&0xDEAD_BEEFu64.to_le_bytes()); wrong_window.extend_from_slice(&sess.dax_window_len.to_le_bytes());
wrong_window.extend_from_slice(&0u64.to_le_bytes()); wrong_window.extend_from_slice(&0u64.to_le_bytes()); wrong_window.extend_from_slice(&0u64.to_le_bytes()); assert!(sess.restore_state(&wrong_window).is_err());
let mut trunc = Vec::from(*b"DAXS");
trunc.extend_from_slice(&1u32.to_le_bytes());
trunc.extend_from_slice(&sess.dax_base_gpa.to_le_bytes());
trunc.extend_from_slice(&sess.dax_window_len.to_le_bytes());
trunc.extend_from_slice(&0u64.to_le_bytes());
trunc.extend_from_slice(&0u64.to_le_bytes());
trunc.extend_from_slice(&3u64.to_le_bytes()); assert!(sess.restore_state(&trunc).is_err());
}
#[test]
fn restore_state_is_idempotent() {
let (sess, _) = make_session();
let req = SetupMappingIn {
fh: 1,
foffset: 0,
len: 0x4000,
flags: FUSE_SETUPMAPPING_FLAG_READ,
moffset: 0,
};
sess.setup(7, &req).unwrap();
let blob = sess.snapshot_state();
let (sess2, _) = make_session();
sess2.restore_state(&blob).unwrap();
let after_first = sess2.active_slot_count();
sess2.restore_state(&blob).unwrap();
let after_second = sess2.active_slot_count();
assert_eq!(after_first, after_second);
assert_eq!(after_second, 1);
assert_eq!(sess2.current_mapped_bytes(), 0x4000);
}
#[test]
fn parse_remove_payload_round_trip() {
let entries = vec![
RemoveMappingOne {
moffset: 0x4000,
len: 0x4000,
},
RemoveMappingOne {
moffset: 0x8000,
len: 0x8000,
},
];
let mut buf = Vec::new();
let hdr = RemoveMappingIn {
count: entries.len() as u32,
_pad: 0,
};
buf.extend_from_slice(unsafe {
std::slice::from_raw_parts(
&hdr as *const RemoveMappingIn as *const u8,
core::mem::size_of::<RemoveMappingIn>(),
)
});
for e in &entries {
buf.extend_from_slice(unsafe {
std::slice::from_raw_parts(
e as *const RemoveMappingOne as *const u8,
core::mem::size_of::<RemoveMappingOne>(),
)
});
}
let parsed = parse_remove_payload(&buf).unwrap();
assert_eq!(parsed.len(), 2);
assert_eq!(parsed[0].moffset, 0x4000);
assert_eq!(parsed[1].moffset, 0x8000);
}
}