supermachine 0.5.0

Run any OCI/Docker image as a hardware-isolated microVM on macOS HVF (Linux KVM and Windows WHP in progress). Single library API, zero flags for the common case, sub-100 ms cold-restore from snapshot.
// Server-initiated FUSE notifications.
//
// The guest's virtio-fs driver caches file metadata + dentries. When
// the host changes a file out-of-band (writes from a different
// process, atomic rename via mv, etc.), the guest's cache becomes
// stale. Send FUSE_NOTIFY_INVAL_INODE / INVAL_ENTRY to drop the
// caches.
//
// For DAX-mapped reads, Apple Silicon's unified cache handles
// in-place modifications transparently (host write → host page-cache
// page is dirtied → guest reads see new content). Notifications
// are still needed for:
//   - File replaced via atomic rename (new inode, mmap holds OLD)
//   - File truncated (mmap'd pages past new EOF)
//   - File deleted
//   - Directory entries added/removed (LOOKUP cache)
//
// LIMITATION (Linux 6.12 virtio-fs guest driver): the current upstream
// virtio-fs implementation does NOT request notification buffers on
// the hipriority queue. virtio-fs has FORGET batching but no path
// for server→guest invalidation messages. Our host-side machinery
// is correct + forward-compatible — when a kernel patch lands to
// add `virtio_fs_setup_notify_vq`-style buffer provisioning, our
// `push_notification` already does the right thing. Until then the
// guest falls back to the 1-second `attr_valid` cache TTL, which
// is acceptable for the typical dev-loop pattern (editor save →
// next compile cycle takes longer than 1s anyway).

use super::protocol::{
    NotifyInvalEntryOut, NotifyInvalInodeOut, OutHeader, FUSE_NOTIFY_INVAL_ENTRY,
    FUSE_NOTIFY_INVAL_INODE,
};

/// Server-initiated notification sender. Backends (PosixFs, future
/// FsBackend impls) use this to tell the guest "your cache for this
/// inode is stale".
///
/// Implemented by `VirtioFs` (pushes to the hipriority queue).
/// Tests use `MockNotifier` which records calls.
pub trait Notifier: Send + Sync {
    /// FUSE_NOTIFY_INVAL_INODE — invalidate cached data for `nodeid`
    /// over `[off, off+len)`. `len = -1` invalidates the whole inode.
    /// `off = -1` AND `len = 0` invalidates only the inode's attrs.
    fn invalidate_inode(&self, nodeid: u64, off: i64, len: i64);

    /// FUSE_NOTIFY_INVAL_ENTRY — invalidate cached dentry for
    /// `(parent_nodeid, name)`. Used when a host change adds/removes
    /// a name under a directory the guest has cached.
    fn invalidate_entry(&self, parent_nodeid: u64, name: &[u8]);
}

/// Build the byte payload for a FUSE_NOTIFY_INVAL_INODE message.
/// Callers shovel this verbatim into a hipriority queue descriptor.
pub fn build_inval_inode(nodeid: u64, off: i64, len: i64) -> Vec<u8> {
    let total = core::mem::size_of::<OutHeader>() + core::mem::size_of::<NotifyInvalInodeOut>();
    let mut buf = Vec::with_capacity(total);
    let hdr = OutHeader {
        len: total as u32,
        error: -FUSE_NOTIFY_INVAL_INODE,
        unique: 0,
    };
    let body = NotifyInvalInodeOut {
        ino: nodeid as i64,
        off,
        len,
    };
    unsafe {
        buf.extend_from_slice(std::slice::from_raw_parts(
            &hdr as *const OutHeader as *const u8,
            core::mem::size_of::<OutHeader>(),
        ));
        buf.extend_from_slice(std::slice::from_raw_parts(
            &body as *const NotifyInvalInodeOut as *const u8,
            core::mem::size_of::<NotifyInvalInodeOut>(),
        ));
    }
    buf
}

/// Build the byte payload for a FUSE_NOTIFY_INVAL_ENTRY message.
/// `namelen` is the name length WITHOUT the trailing NUL; the NUL is
/// appended to the byte stream (the kernel copies `namelen + 1`
/// bytes when reading).
pub fn build_inval_entry(parent_nodeid: u64, name: &[u8]) -> Vec<u8> {
    let total = core::mem::size_of::<OutHeader>()
        + core::mem::size_of::<NotifyInvalEntryOut>()
        + name.len()
        + 1;
    let mut buf = Vec::with_capacity(total);
    let hdr = OutHeader {
        len: total as u32,
        error: -FUSE_NOTIFY_INVAL_ENTRY,
        unique: 0,
    };
    let body = NotifyInvalEntryOut {
        parent: parent_nodeid,
        namelen: name.len() as u32, // no NUL
        _pad: 0,
    };
    unsafe {
        buf.extend_from_slice(std::slice::from_raw_parts(
            &hdr as *const OutHeader as *const u8,
            core::mem::size_of::<OutHeader>(),
        ));
        buf.extend_from_slice(std::slice::from_raw_parts(
            &body as *const NotifyInvalEntryOut as *const u8,
            core::mem::size_of::<NotifyInvalEntryOut>(),
        ));
    }
    buf.extend_from_slice(name);
    buf.push(0); // NUL terminator follows the name
    buf
}

/// No-op notifier for backends that don't need invalidation.
pub struct NullNotifier;
impl Notifier for NullNotifier {
    fn invalidate_inode(&self, _nodeid: u64, _off: i64, _len: i64) {}
    fn invalidate_entry(&self, _parent_nodeid: u64, _name: &[u8]) {}
}

/// Recording notifier for tests.
pub struct MockNotifier {
    pub calls: std::sync::Mutex<Vec<NotifyCall>>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum NotifyCall {
    InvalInode { nodeid: u64, off: i64, len: i64 },
    InvalEntry { parent: u64, name: Vec<u8> },
}

impl MockNotifier {
    pub fn new() -> Self {
        Self {
            calls: std::sync::Mutex::new(Vec::new()),
        }
    }
}

impl Default for MockNotifier {
    fn default() -> Self {
        Self::new()
    }
}

impl Notifier for MockNotifier {
    fn invalidate_inode(&self, nodeid: u64, off: i64, len: i64) {
        self.calls.lock().unwrap().push(NotifyCall::InvalInode { nodeid, off, len });
    }
    fn invalidate_entry(&self, parent_nodeid: u64, name: &[u8]) {
        self.calls.lock().unwrap().push(NotifyCall::InvalEntry {
            parent: parent_nodeid,
            name: name.to_vec(),
        });
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn build_inval_inode_layout() {
        let bytes = build_inval_inode(42, 0, -1);
        assert_eq!(bytes.len(), 40); // 16 header + 24 body
        let hdr: OutHeader = unsafe { core::ptr::read_unaligned(bytes.as_ptr() as *const OutHeader) };
        assert_eq!(hdr.len, 40);
        assert_eq!(hdr.unique, 0);
        assert_eq!(hdr.error, -FUSE_NOTIFY_INVAL_INODE);
        let body: NotifyInvalInodeOut = unsafe {
            core::ptr::read_unaligned(
                bytes.as_ptr().add(core::mem::size_of::<OutHeader>())
                    as *const NotifyInvalInodeOut,
            )
        };
        assert_eq!(body.ino, 42);
        assert_eq!(body.off, 0);
        assert_eq!(body.len, -1);
    }

    #[test]
    fn build_inval_entry_includes_nul_terminator() {
        let bytes = build_inval_entry(1, b"foo.txt");
        // 16 header + 16 body + 8 ("foo.txt\0")
        assert_eq!(bytes.len(), 40);
        let hdr: OutHeader = unsafe { core::ptr::read_unaligned(bytes.as_ptr() as *const OutHeader) };
        assert_eq!(hdr.error, -FUSE_NOTIFY_INVAL_ENTRY);
        assert_eq!(hdr.unique, 0);
        // namelen field in NotifyInvalEntryOut must be 7 (no NUL).
        let body: NotifyInvalEntryOut = unsafe {
            core::ptr::read_unaligned(
                bytes.as_ptr().add(core::mem::size_of::<OutHeader>())
                    as *const NotifyInvalEntryOut,
            )
        };
        assert_eq!(body.namelen, 7);
        // Last byte should be NUL.
        assert_eq!(*bytes.last().unwrap(), 0);
        // Name should be present.
        assert_eq!(&bytes[16 + 16..16 + 16 + 7], b"foo.txt");
    }

    #[test]
    fn mock_notifier_records() {
        let n = MockNotifier::new();
        n.invalidate_inode(7, 0, -1);
        n.invalidate_entry(1, b"x");
        let calls = n.calls.lock().unwrap();
        assert_eq!(calls.len(), 2);
        assert_eq!(
            calls[0],
            NotifyCall::InvalInode { nodeid: 7, off: 0, len: -1 }
        );
        assert_eq!(
            calls[1],
            NotifyCall::InvalEntry { parent: 1, name: b"x".to_vec() }
        );
    }
}