Skip to main content

arcbox_virtio_fs/
session.rs

1//! FUSE session state — handles the `INIT` handshake and tracks negotiated features.
2
3use std::sync::atomic::{AtomicBool, Ordering};
4
5use arcbox_virtio_core::error::{Result, VirtioError};
6
7use crate::protocol::{
8    DEFAULT_MAX_PAGES, DEFAULT_MAX_READAHEAD, DEFAULT_MAX_WRITE, FUSE_ASYNC_READ, FUSE_BIG_WRITES,
9    FUSE_KERNEL_MINOR_VERSION, FUSE_KERNEL_VERSION, FUSE_MAP_ALIGNMENT, FUSE_WRITEBACK_CACHE,
10};
11
12/// FUSE session state.
13///
14/// Manages the FUSE protocol session including initialization handshake
15/// and feature negotiation.
16#[derive(Debug)]
17pub struct FuseSession {
18    /// Whether `FUSE_INIT` has been received and processed.
19    initialized: AtomicBool,
20    /// Negotiated protocol major version.
21    major: u32,
22    /// Negotiated protocol minor version.
23    minor: u32,
24    /// Negotiated flags.
25    flags: u32,
26    /// Maximum readahead size.
27    max_readahead: u32,
28    /// Maximum write size.
29    max_write: u32,
30    /// Maximum pages per request.
31    max_pages: u16,
32    /// Whether DAX is enabled for this session.
33    dax_enabled: bool,
34}
35
36impl FuseSession {
37    /// Creates a new FUSE session with default settings.
38    #[must_use]
39    pub const fn new() -> Self {
40        Self {
41            initialized: AtomicBool::new(false),
42            major: FUSE_KERNEL_VERSION,
43            minor: FUSE_KERNEL_MINOR_VERSION,
44            flags: FUSE_ASYNC_READ | FUSE_BIG_WRITES | FUSE_WRITEBACK_CACHE | FUSE_MAP_ALIGNMENT,
45            max_readahead: DEFAULT_MAX_READAHEAD,
46            max_write: DEFAULT_MAX_WRITE,
47            max_pages: DEFAULT_MAX_PAGES,
48            dax_enabled: false,
49        }
50    }
51
52    /// Returns whether the session is initialized.
53    #[must_use]
54    pub fn is_initialized(&self) -> bool {
55        self.initialized.load(Ordering::Acquire)
56    }
57
58    /// Returns the negotiated major version.
59    #[must_use]
60    pub const fn major(&self) -> u32 {
61        self.major
62    }
63
64    /// Returns the negotiated minor version.
65    #[must_use]
66    pub const fn minor(&self) -> u32 {
67        self.minor
68    }
69
70    /// Returns the negotiated flags.
71    #[must_use]
72    pub const fn flags(&self) -> u32 {
73        self.flags
74    }
75
76    /// Returns the maximum readahead size.
77    #[must_use]
78    pub const fn max_readahead(&self) -> u32 {
79        self.max_readahead
80    }
81
82    /// Returns the maximum write size.
83    #[must_use]
84    pub const fn max_write(&self) -> u32 {
85        self.max_write
86    }
87
88    /// Returns the maximum pages per request.
89    #[must_use]
90    pub const fn max_pages(&self) -> u16 {
91        self.max_pages
92    }
93
94    /// Handles `FUSE_INIT` request and returns the response.
95    ///
96    /// This negotiates the protocol version and features with the guest driver.
97    pub fn handle_init(&mut self, request: &[u8]) -> Result<Vec<u8>> {
98        // FUSE_INIT request body starts after the 40-byte header.
99        // Layout: major(4) + minor(4) + max_readahead(4) + flags(4)
100        if request.len() < 56 {
101            return Err(VirtioError::DeviceError {
102                device: "fs".to_string(),
103                message: "FUSE_INIT request too small".to_string(),
104            });
105        }
106
107        let body = &request[40..];
108        let guest_major = u32::from_le_bytes([body[0], body[1], body[2], body[3]]);
109        let guest_minor = u32::from_le_bytes([body[4], body[5], body[6], body[7]]);
110        let guest_max_readahead = u32::from_le_bytes([body[8], body[9], body[10], body[11]]);
111        let guest_flags = u32::from_le_bytes([body[12], body[13], body[14], body[15]]);
112
113        tracing::debug!(
114            "FUSE_INIT: guest version {}.{}, max_readahead={}, flags={:#x}",
115            guest_major,
116            guest_minor,
117            guest_max_readahead,
118            guest_flags
119        );
120
121        if guest_major < FUSE_KERNEL_VERSION {
122            return Err(VirtioError::DeviceError {
123                device: "fs".to_string(),
124                message: format!(
125                    "FUSE version mismatch: guest {guest_major}.{guest_minor} < required {FUSE_KERNEL_VERSION}.{FUSE_KERNEL_MINOR_VERSION}"
126                ),
127            });
128        }
129
130        // Negotiate features
131        self.major = FUSE_KERNEL_VERSION;
132        self.minor = FUSE_KERNEL_MINOR_VERSION;
133        self.max_readahead = guest_max_readahead.min(DEFAULT_MAX_READAHEAD);
134        self.flags &= guest_flags; // Only enable mutually supported flags
135        self.dax_enabled = self.flags & FUSE_MAP_ALIGNMENT != 0;
136        if self.dax_enabled {
137            tracing::info!("FUSE DAX negotiated");
138        }
139
140        // Build FUSE_INIT response (80 bytes total).
141        // Layout: header(16) + major(4) + minor(4) + max_readahead(4) + flags(4) +
142        //         max_background(2) + congestion_threshold(2) + max_write(4) +
143        //         time_gran(4) + max_pages(2) + map_alignment(2) + flags2(4) + unused[7](28)
144        let mut response = Vec::with_capacity(80);
145
146        let unique = u64::from_le_bytes([
147            request[8],
148            request[9],
149            request[10],
150            request[11],
151            request[12],
152            request[13],
153            request[14],
154            request[15],
155        ]);
156        let len = 80u32;
157        response.extend_from_slice(&len.to_le_bytes());
158        response.extend_from_slice(&0i32.to_le_bytes()); // error = 0
159        response.extend_from_slice(&unique.to_le_bytes());
160
161        // FUSE_INIT response body (64 bytes).
162        // Offsets below are relative to the start of the body (after the 16-byte header).
163        //   0..4  major, 4..8 minor, 8..12 max_readahead, 12..16 flags
164        //  16..18 max_background, 18..20 congestion_threshold
165        //  20..24 max_write, 24..28 time_gran
166        //  28..30 max_pages
167        //  30..32 map_alignment  ← u16 page-shift; NOT padding
168        //  32..36 flags2 (unused here, zeroed)
169        //  36..64 unused[7]
170        response.extend_from_slice(&self.major.to_le_bytes());
171        response.extend_from_slice(&self.minor.to_le_bytes());
172        response.extend_from_slice(&self.max_readahead.to_le_bytes());
173        response.extend_from_slice(&self.flags.to_le_bytes());
174        response.extend_from_slice(&16u16.to_le_bytes()); // max_background
175        response.extend_from_slice(&12u16.to_le_bytes()); // congestion_threshold
176        response.extend_from_slice(&self.max_write.to_le_bytes());
177        response.extend_from_slice(&1u32.to_le_bytes()); // time_gran (1 ns)
178        response.extend_from_slice(&self.max_pages.to_le_bytes());
179        // map_alignment: page-size shift advertised to the guest (e.g. 12 = 4 KiB,
180        // 14 = 16 KiB).  Must equal the host's actual page size, because every
181        // FUSE_SETUPMAPPING turns into an hv_vm_map on the host, which rejects
182        // sub-page-aligned requests with HV_ERROR.  On Apple Silicon the host page
183        // is 16 KiB (shift=14); on Intel/x86 it is 4 KiB (shift=12).
184        // Derive at runtime instead of hardcoding so the binary is correct on both.
185        let map_alignment: u16 = if self.dax_enabled {
186            // SAFETY: _SC_PAGESIZE is a valid sysconf key; the return value is always
187            // positive on supported platforms.  We fall back to 16 KiB (shift=14,
188            // the strictest value for current ArcBox targets) on any error.
189            let host_page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
190            let page_size: u64 = if host_page > 0 {
191                host_page as u64
192            } else {
193                16 * 1024
194            };
195            u16::try_from(page_size.trailing_zeros()).unwrap_or(14)
196        } else {
197            0
198        };
199        response.extend_from_slice(&map_alignment.to_le_bytes());
200        response.extend_from_slice(&[0u8; 32]); // flags2(4) + unused[7](28)
201
202        self.initialized.store(true, Ordering::Release);
203
204        tracing::info!(
205            "FUSE session initialized: version {}.{}, flags={:#x}, max_write={}",
206            self.major,
207            self.minor,
208            self.flags,
209            self.max_write
210        );
211
212        Ok(response)
213    }
214
215    /// Resets the session state.
216    pub fn reset(&mut self) {
217        self.initialized.store(false, Ordering::Release);
218        self.major = FUSE_KERNEL_VERSION;
219        self.minor = FUSE_KERNEL_MINOR_VERSION;
220        self.flags = FUSE_ASYNC_READ | FUSE_BIG_WRITES | FUSE_WRITEBACK_CACHE | FUSE_MAP_ALIGNMENT;
221        self.max_readahead = DEFAULT_MAX_READAHEAD;
222        self.max_write = DEFAULT_MAX_WRITE;
223        self.max_pages = DEFAULT_MAX_PAGES;
224        self.dax_enabled = false;
225    }
226}
227
228impl Default for FuseSession {
229    fn default() -> Self {
230        Self::new()
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    #[test]
239    fn test_fuse_session_new() {
240        let session = FuseSession::new();
241        assert!(!session.is_initialized());
242        assert_eq!(session.major(), FUSE_KERNEL_VERSION);
243        assert_eq!(session.minor(), FUSE_KERNEL_MINOR_VERSION);
244        assert_eq!(session.max_readahead(), DEFAULT_MAX_READAHEAD);
245        assert_eq!(session.max_write(), DEFAULT_MAX_WRITE);
246        assert_eq!(session.max_pages(), DEFAULT_MAX_PAGES);
247    }
248
249    #[test]
250    fn test_fuse_session_default() {
251        let session = FuseSession::default();
252        assert!(!session.is_initialized());
253    }
254
255    #[test]
256    fn test_fuse_session_reset() {
257        let mut session = FuseSession::new();
258        session.initialized.store(true, Ordering::SeqCst);
259
260        session.reset();
261
262        assert!(!session.is_initialized());
263        assert_eq!(session.major(), FUSE_KERNEL_VERSION);
264    }
265
266    #[test]
267    fn test_fuse_session_handle_init() {
268        let mut session = FuseSession::new();
269
270        // Build a valid FUSE_INIT request: 40-byte header + 16-byte body.
271        let mut request = vec![0u8; 56];
272
273        request[0..4].copy_from_slice(&56u32.to_le_bytes()); // length
274        request[4..8].copy_from_slice(&26u32.to_le_bytes()); // opcode FUSE_INIT
275        request[8..16].copy_from_slice(&1u64.to_le_bytes()); // unique
276        request[16..24].copy_from_slice(&0u64.to_le_bytes()); // nodeid
277        request[24..28].copy_from_slice(&0u32.to_le_bytes()); // uid
278        request[28..32].copy_from_slice(&0u32.to_le_bytes()); // gid
279        request[32..36].copy_from_slice(&0u32.to_le_bytes()); // pid
280        request[36..40].copy_from_slice(&0u32.to_le_bytes()); // padding
281
282        // FUSE_INIT body
283        request[40..44].copy_from_slice(&FUSE_KERNEL_VERSION.to_le_bytes());
284        request[44..48].copy_from_slice(&FUSE_KERNEL_MINOR_VERSION.to_le_bytes());
285        request[48..52].copy_from_slice(&(64 * 1024u32).to_le_bytes());
286        request[52..56].copy_from_slice(&(FUSE_ASYNC_READ | FUSE_BIG_WRITES).to_le_bytes());
287
288        let response = session.handle_init(&request).unwrap();
289
290        assert!(session.is_initialized());
291        assert_eq!(response.len(), 80);
292
293        let resp_len = u32::from_le_bytes([response[0], response[1], response[2], response[3]]);
294        let resp_error = i32::from_le_bytes([response[4], response[5], response[6], response[7]]);
295        let resp_unique = u64::from_le_bytes([
296            response[8],
297            response[9],
298            response[10],
299            response[11],
300            response[12],
301            response[13],
302            response[14],
303            response[15],
304        ]);
305
306        assert_eq!(resp_len, 80);
307        assert_eq!(resp_error, 0);
308        assert_eq!(resp_unique, 1);
309
310        assert_eq!(session.max_readahead(), 64 * 1024);
311    }
312
313    #[test]
314    fn test_fuse_session_handle_init_too_small() {
315        let mut session = FuseSession::new();
316        let request = vec![0u8; 40]; // Too small (need 56)
317
318        let result = session.handle_init(&request);
319        assert!(result.is_err());
320    }
321
322    #[test]
323    fn test_fuse_session_handle_init_old_version() {
324        let mut session = FuseSession::new();
325
326        let mut request = vec![0u8; 56];
327        request[0..4].copy_from_slice(&56u32.to_le_bytes());
328        request[4..8].copy_from_slice(&26u32.to_le_bytes());
329        request[8..16].copy_from_slice(&1u64.to_le_bytes());
330
331        // FUSE_INIT body with an old version
332        request[40..44].copy_from_slice(&5u32.to_le_bytes());
333        request[44..48].copy_from_slice(&0u32.to_le_bytes());
334        request[48..52].copy_from_slice(&(64 * 1024u32).to_le_bytes());
335        request[52..56].copy_from_slice(&0u32.to_le_bytes());
336
337        let result = session.handle_init(&request);
338        assert!(result.is_err());
339    }
340}