Skip to main content

fs_ext4_strate/
main.rs

1//! EXT4 Filesystem Strate (userspace)
2//!
3//! IPC-based filesystem strate that mounts an EXT4 volume and serves
4//! file operations via the kernel VFS.
5
6#![no_std]
7#![no_main]
8#![feature(alloc_error_handler)]
9
10extern crate alloc;
11
12mod syscalls;
13
14use alloc::{format, string::String, vec, vec::Vec};
15use core::{alloc::Layout, panic::PanicInfo};
16use fs_ext4::{BlockDevice, BlockDeviceError, Ext4FileSystem};
17use strat9_syscall::error::{EINVAL, ENOSYS};
18use syscalls::*;
19
20// ---------------------------------------------------------------------------
21// Minimal bump allocator (temporary until userspace heap is wired).
22// ---------------------------------------------------------------------------
23
24alloc_freelist::define_freelist_allocator!(pub struct BumpAllocator; heap_size = 1024 * 1024;);
25
26#[global_allocator]
27static GLOBAL_ALLOCATOR: BumpAllocator = BumpAllocator;
28
29#[alloc_error_handler]
30/// Implements alloc error.
31fn alloc_error(_layout: Layout) -> ! {
32    debug_log("[fs-ext4] OOM\n");
33    exit(12);
34}
35
36use strat9_syscall::data::IpcMessage;
37
38const OPCODE_OPEN: u32 = 0x01;
39const OPCODE_READ: u32 = 0x02;
40const OPCODE_WRITE: u32 = 0x03;
41const OPCODE_CLOSE: u32 = 0x04;
42const OPCODE_CREATE_FILE: u32 = 0x05;
43const OPCODE_CREATE_DIR: u32 = 0x06;
44const OPCODE_UNLINK: u32 = 0x07;
45const OPCODE_READDIR: u32 = 0x08;
46const OPCODE_BOOTSTRAP: u32 = 0x10;
47const REPLY_MSG_TYPE: u32 = 0x80;
48const STATUS_OK: u32 = 0;
49const INITIAL_BIND_PATH: &[u8] = b"/srv/strate-fs-ext4/default";
50
51const MAX_OPEN_PATH: usize = 42;
52const MAX_WRITE_DATA: usize = 30;
53
54struct BootstrapInfo {
55    handle: u64,
56    label: String,
57}
58
59/// Implements sanitize label.
60fn sanitize_label(raw: &str) -> String {
61    let mut out = String::new();
62    for b in raw.bytes().take(31) {
63        let ok = (b as char).is_ascii_alphanumeric() || b == b'-' || b == b'_' || b == b'.';
64        out.push(if ok { b as char } else { '_' });
65    }
66    if out.is_empty() {
67        String::from("default")
68    } else {
69        out
70    }
71}
72
73/// Parses bootstrap label.
74fn parse_bootstrap_label(payload: &[u8]) -> String {
75    let len = payload.first().copied().unwrap_or(0) as usize;
76    if len == 0 {
77        return String::from("default");
78    }
79    let end = 1usize.saturating_add(len);
80    let Some(bytes) = payload.get(1..end) else {
81        return String::from("default");
82    };
83    match core::str::from_utf8(bytes) {
84        Ok(s) => sanitize_label(s),
85        Err(_) => String::from("default"),
86    }
87}
88
89/// Implements bind srv alias.
90fn bind_srv_alias(port_handle: u64, label: &str) {
91    let path = format!("/srv/strate-fs-ext4/{}", label);
92    match call::ipc_bind_port(port_handle as usize, path.as_bytes()) {
93        Ok(_) => {
94            let msg = format!("[fs-ext4] Port alias bound to {}\n", path);
95            debug_log(&msg);
96        }
97        Err(e) => {
98            let msg = format!(
99                "[fs-ext4] Failed to bind port alias {}: {}\n",
100                path,
101                e.name()
102            );
103            debug_log(&msg);
104        }
105    }
106}
107
108/// EXT4 Strate state
109struct Ext4Strate {
110    _fs: Ext4FileSystem,
111}
112
113impl Ext4Strate {
114    /// Creates a new instance.
115    fn new(fs: Ext4FileSystem) -> Self {
116        Ext4Strate { _fs: fs }
117    }
118
119    /// Implements ok reply.
120    fn ok_reply(sender: u64) -> IpcMessage {
121        let mut reply = IpcMessage::new(REPLY_MSG_TYPE);
122        reply.sender = sender;
123        reply.payload[0..4].copy_from_slice(&STATUS_OK.to_le_bytes());
124        reply
125    }
126
127    /// Implements err reply.
128    fn err_reply(sender: u64, status: u32) -> IpcMessage {
129        let mut reply = IpcMessage::new(REPLY_MSG_TYPE);
130        reply.sender = sender;
131        reply.payload[0..4].copy_from_slice(&status.to_le_bytes());
132        reply
133    }
134
135    /// Reads u16.
136    fn read_u16(payload: &[u8], start: usize) -> core::result::Result<u16, u32> {
137        let end = start.checked_add(2).ok_or(EINVAL as u32)?;
138        let bytes = payload.get(start..end).ok_or(EINVAL as u32)?;
139        Ok(u16::from_le_bytes([bytes[0], bytes[1]]))
140    }
141
142    /// Reads u32.
143    fn read_u32(payload: &[u8], start: usize) -> core::result::Result<u32, u32> {
144        let end = start.checked_add(4).ok_or(EINVAL as u32)?;
145        let bytes = payload.get(start..end).ok_or(EINVAL as u32)?;
146        Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
147    }
148
149    /// Reads u64.
150    fn read_u64(payload: &[u8], start: usize) -> core::result::Result<u64, u32> {
151        let end = start.checked_add(8).ok_or(EINVAL as u32)?;
152        let bytes = payload.get(start..end).ok_or(EINVAL as u32)?;
153        Ok(u64::from_le_bytes([
154            bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
155        ]))
156    }
157
158    fn parse_path<'a>(
159        payload: &'a [u8],
160        len_offset: usize,
161        data_offset: usize,
162        max_len: usize,
163    ) -> core::result::Result<&'a str, u32> {
164        let path_len = Self::read_u16(payload, len_offset)? as usize;
165        if path_len > max_len {
166            return Err(EINVAL as u32);
167        }
168        let end = data_offset.checked_add(path_len).ok_or(EINVAL as u32)?;
169        let path_bytes = payload.get(data_offset..end).ok_or(EINVAL as u32)?;
170        core::str::from_utf8(path_bytes).map_err(|_| EINVAL as u32)
171    }
172
173    /// Implements handle open.
174    fn handle_open(&mut self, sender: u64, payload: &[u8]) -> IpcMessage {
175        let _flags = match Self::read_u32(payload, 0) {
176            Ok(v) => v,
177            Err(code) => return Self::err_reply(sender, code),
178        };
179        let _path = match Self::parse_path(payload, 4, 6, MAX_OPEN_PATH) {
180            Ok(v) => v,
181            Err(code) => return Self::err_reply(sender, code),
182        };
183        Self::err_reply(sender, ENOSYS as u32)
184    }
185
186    /// Implements handle read.
187    fn handle_read(&mut self, sender: u64, payload: &[u8]) -> IpcMessage {
188        let _file_id = match Self::read_u64(payload, 0) {
189            Ok(v) => v,
190            Err(code) => return Self::err_reply(sender, code),
191        };
192        let _offset = match Self::read_u64(payload, 8) {
193            Ok(v) => v,
194            Err(code) => return Self::err_reply(sender, code),
195        };
196        let _requested = match Self::read_u32(payload, 16) {
197            Ok(v) => v as usize,
198            Err(code) => return Self::err_reply(sender, code),
199        };
200        Self::err_reply(sender, ENOSYS as u32)
201    }
202
203    /// Implements handle write.
204    fn handle_write(&mut self, sender: u64, payload: &[u8]) -> IpcMessage {
205        let _file_id = match Self::read_u64(payload, 0) {
206            Ok(v) => v,
207            Err(code) => return Self::err_reply(sender, code),
208        };
209        let _offset = match Self::read_u64(payload, 8) {
210            Ok(v) => v,
211            Err(code) => return Self::err_reply(sender, code),
212        };
213        let len = match Self::read_u16(payload, 16) {
214            Ok(v) => v as usize,
215            Err(code) => return Self::err_reply(sender, code),
216        };
217        if len > MAX_WRITE_DATA {
218            return Self::err_reply(sender, EINVAL as u32);
219        }
220        let end = 18 + len;
221        let _data = match payload.get(18..end) {
222            Some(s) => s,
223            None => return Self::err_reply(sender, EINVAL as u32),
224        };
225        Self::err_reply(sender, ENOSYS as u32)
226    }
227
228    /// Implements handle close.
229    fn handle_close(&mut self, sender: u64, payload: &[u8]) -> IpcMessage {
230        let _file_id = match Self::read_u64(payload, 0) {
231            Ok(v) => v,
232            Err(code) => return Self::err_reply(sender, code),
233        };
234        Self::err_reply(sender, ENOSYS as u32)
235    }
236
237    /// Main strate loop
238    fn serve(&mut self, port_handle: u64) -> ! {
239        loop {
240            let mut msg = IpcMessage::new(0);
241            if call::ipc_recv(port_handle as usize, &mut msg).is_err() {
242                let _ = call::sched_yield();
243                continue;
244            }
245
246            let reply = match msg.msg_type {
247                OPCODE_BOOTSTRAP => {
248                    let label = parse_bootstrap_label(&msg.payload);
249                    bind_srv_alias(port_handle, &label);
250                    Self::ok_reply(msg.sender)
251                }
252                OPCODE_OPEN => self.handle_open(msg.sender, &msg.payload),
253                OPCODE_READ => self.handle_read(msg.sender, &msg.payload),
254                OPCODE_WRITE => self.handle_write(msg.sender, &msg.payload),
255                OPCODE_CLOSE => self.handle_close(msg.sender, &msg.payload),
256                OPCODE_CREATE_FILE | OPCODE_CREATE_DIR | OPCODE_UNLINK | OPCODE_READDIR => {
257                    Self::err_reply(msg.sender, ENOSYS as u32)
258                }
259                _ => Self::err_reply(msg.sender, ENOSYS as u32),
260            };
261            let _ = call::ipc_reply(&reply);
262        }
263    }
264}
265
266// ---------------------------------------------------------------------------
267// Volume-backed block device (uses SYS_VOLUME_* syscalls)
268// ---------------------------------------------------------------------------
269
270const SECTOR_SIZE: usize = 512;
271const BLOCK_SIZE: usize = 4096;
272
273struct VolumeBlockDevice {
274    handle: u64,
275    sector_count: u64,
276}
277
278impl VolumeBlockDevice {
279    /// Creates a new instance.
280    fn new(handle: u64) -> core::result::Result<Self, BlockDeviceError> {
281        let sector_count = volume_info(handle).map_err(map_sys_err)?;
282        Ok(Self {
283            handle,
284            sector_count,
285        })
286    }
287}
288
289/// Implements map sys err.
290fn map_sys_err(err: Error) -> BlockDeviceError {
291    match err {
292        Error::Again => BlockDeviceError::NotReady,
293        Error::IoError => BlockDeviceError::Io,
294        Error::InvalidArgument => BlockDeviceError::InvalidOffset,
295        _ => BlockDeviceError::Other,
296    }
297}
298
299/// Implements log sys err.
300fn log_sys_err(prefix: &str, err: Error) {
301    let msg = format!("[fs-ext4] {}: {} ({})\n", prefix, err.name(), err);
302    debug_log(&msg);
303}
304
305/// Implements validate volume handle.
306fn validate_volume_handle(handle: u64) -> Result<u64> {
307    let sectors = volume_info(handle)?;
308    if sectors == 0 {
309        return Err(Error::InvalidArgument);
310    }
311    let mut probe = [0u8; SECTOR_SIZE];
312    let _ = volume_read(handle, 0, &mut probe, 1)?;
313    Ok(sectors)
314}
315
316/// Implements discover volume handle local.
317fn discover_volume_handle_local() -> Option<u64> {
318    // Pragmatic fallback: probe low capability ids for a usable Volume handle.
319    // In current boot flow, init often receives the first inserted capability.
320    for h in 0u64..256u64 {
321        if let Ok(sectors) = validate_volume_handle(h) {
322            let msg = format!(
323                "[fs-ext4] Discovered local volume handle={} sectors={}\n",
324                h, sectors
325            );
326            debug_log(&msg);
327            return Some(h);
328        }
329    }
330    None
331}
332
333impl BlockDevice for VolumeBlockDevice {
334    /// Reads offset.
335    fn read_offset(&self, offset: usize) -> core::result::Result<Vec<u8>, BlockDeviceError> {
336        if offset % SECTOR_SIZE != 0 {
337            return Err(BlockDeviceError::InvalidOffset);
338        }
339        let sector = (offset / SECTOR_SIZE) as u64;
340        let sector_count = (BLOCK_SIZE / SECTOR_SIZE) as u64;
341        if sector_count == 0 {
342            return Err(BlockDeviceError::InvalidOffset);
343        }
344        let mut buf = vec![0u8; (sector_count as usize) * SECTOR_SIZE];
345        volume_read(self.handle, sector, &mut buf, sector_count).map_err(map_sys_err)?;
346        Ok(buf)
347    }
348
349    /// Writes offset.
350    fn write_offset(
351        &mut self,
352        offset: usize,
353        data: &[u8],
354    ) -> core::result::Result<(), BlockDeviceError> {
355        if offset % SECTOR_SIZE != 0 || data.len() % SECTOR_SIZE != 0 {
356            return Err(BlockDeviceError::InvalidOffset);
357        }
358        let sector = (offset / SECTOR_SIZE) as u64;
359        let sector_count = (data.len() / SECTOR_SIZE) as u64;
360        if sector_count == 0 {
361            return Err(BlockDeviceError::InvalidOffset);
362        }
363        volume_write(self.handle, sector, data, sector_count).map_err(map_sys_err)?;
364        Ok(())
365    }
366
367    /// Implements size.
368    fn size(&self) -> core::result::Result<usize, BlockDeviceError> {
369        Ok(self.sector_count as usize * SECTOR_SIZE)
370    }
371}
372
373/// Implements wait for bootstrap.
374fn wait_for_bootstrap(port_handle: u64) -> BootstrapInfo {
375    debug_log("[fs-ext4] Waiting for volume bootstrap...\n");
376    loop {
377        let mut msg = IpcMessage::new(0);
378        if call::ipc_recv(port_handle as usize, &mut msg).is_err() {
379            let _ = call::sched_yield();
380            continue;
381        }
382
383        if msg.msg_type == OPCODE_BOOTSTRAP && msg.flags != 0 {
384            let reply = Ext4Strate::ok_reply(msg.sender);
385            let _ = call::ipc_reply(&reply);
386            return BootstrapInfo {
387                handle: msg.flags as u64,
388                label: parse_bootstrap_label(&msg.payload),
389            };
390        }
391
392        let reply = Ext4Strate::err_reply(msg.sender, ENOSYS as u32);
393        let _ = call::ipc_reply(&reply);
394    }
395}
396
397/// Attempts to wait for bootstrap.
398fn try_wait_for_bootstrap(port_handle: u64, attempts: usize) -> Option<BootstrapInfo> {
399    for _ in 0..attempts {
400        let mut msg = IpcMessage::new(0);
401        match call::ipc_try_recv(port_handle as usize, &mut msg) {
402            Ok(_) => {
403                if msg.msg_type == OPCODE_BOOTSTRAP && msg.flags != 0 {
404                    let reply = Ext4Strate::ok_reply(msg.sender);
405                    let _ = call::ipc_reply(&reply);
406                    return Some(BootstrapInfo {
407                        handle: msg.flags as u64,
408                        label: parse_bootstrap_label(&msg.payload),
409                    });
410                }
411                let reply = Ext4Strate::err_reply(msg.sender, ENOSYS as u32);
412                let _ = call::ipc_reply(&reply);
413            }
414            Err(Error::Again) => {}
415            Err(err) => {
416                log_sys_err("try_recv bootstrap failed", err);
417            }
418        }
419        let _ = call::sched_yield();
420    }
421    None
422}
423
424#[unsafe(no_mangle)]
425/// Implements start.
426pub extern "C" fn _start(bootstrap_handle: u64) -> ! {
427    // TODO: Initialize allocator (we need heap)
428    // For now, this will panic since we can't allocate
429
430    debug_log("[fs-ext4] Starting EXT4 filesystem strate\n");
431
432    let port_handle = match call::ipc_create_port(0) {
433        Ok(h) => h as u64,
434        Err(_) => {
435            debug_log("[fs-ext4] Failed to create IPC port\n");
436            exit(1);
437        }
438    };
439
440    debug_log("[fs-ext4] IPC port created\n");
441
442    if call::ipc_bind_port(port_handle as usize, INITIAL_BIND_PATH).is_err() {
443        debug_log("[fs-ext4] Failed to bind initial port alias\n");
444        exit(2);
445    }
446
447    debug_log("[fs-ext4] Port bound to /srv/strate-fs-ext4/default\n");
448
449    let mut volume_handle = bootstrap_handle;
450    let mut bootstrap_label = String::from("default");
451    if volume_handle == 0 {
452        debug_log("[fs-ext4] Waiting for early bootstrap message...\n");
453        if let Some(info) = try_wait_for_bootstrap(port_handle, 2048) {
454            let msg = format!(
455                "[fs-ext4] Received bootstrap handle: {} label: {}\n",
456                info.handle, info.label
457            );
458            debug_log(&msg);
459            volume_handle = info.handle;
460            bootstrap_label = info.label;
461        } else if let Some(h) = discover_volume_handle_local() {
462            debug_log("[fs-ext4] Bootstrap message timeout, using local discovery fallback\n");
463            volume_handle = h;
464        } else {
465            debug_log("[fs-ext4] Bootstrap message timeout, switching to blocking wait\n");
466            let info = wait_for_bootstrap(port_handle);
467            volume_handle = info.handle;
468            bootstrap_label = info.label;
469        }
470    }
471    {
472        let msg = format!(
473            "[fs-ext4] Volume handle ready: {} label: {}\n",
474            volume_handle, bootstrap_label
475        );
476        debug_log(&msg);
477    }
478    bind_srv_alias(port_handle, &bootstrap_label);
479
480    // Mount EXT4 with retry/backoff instead of hard exit.
481    let mut attempts: u64 = 0;
482    loop {
483        attempts = attempts.wrapping_add(1);
484        match validate_volume_handle(volume_handle) {
485            Ok(sectors) => {
486                let msg = format!(
487                    "[fs-ext4] volume probe OK: handle={} sectors={} (attempt={})\n",
488                    volume_handle, sectors, attempts
489                );
490                debug_log(&msg);
491            }
492            Err(err) => {
493                log_sys_err("volume probe failed", err);
494                // If we started without bootstrap capability, wait for a fresh handle periodically.
495                if bootstrap_handle == 0 && attempts % 8 == 0 {
496                    debug_log("[fs-ext4] Waiting for refreshed bootstrap handle...\n");
497                    let info = wait_for_bootstrap(port_handle);
498                    volume_handle = info.handle;
499                    bootstrap_label = info.label;
500                    let msg = format!(
501                        "[fs-ext4] Refreshed volume handle: {} label: {}\n",
502                        volume_handle, bootstrap_label
503                    );
504                    debug_log(&msg);
505                }
506                for _ in 0..2048 {
507                    let _ = call::sched_yield();
508                }
509                continue;
510            }
511        }
512
513        let device = match VolumeBlockDevice::new(volume_handle) {
514            Ok(dev) => alloc::sync::Arc::new(dev),
515            Err(e) => {
516                let msg = format!(
517                    "[fs-ext4] Failed to init volume device (attempt={}): {:?}\n",
518                    attempts, e
519                );
520                debug_log(&msg);
521                for _ in 0..2048 {
522                    let _ = call::sched_yield();
523                }
524                continue;
525            }
526        };
527
528        let fs = match Ext4FileSystem::mount(device) {
529            Ok(fs) => fs,
530            Err(e) => {
531                let msg = format!(
532                    "[fs-ext4] Failed to mount EXT4 filesystem (attempt={}): {:?}\n",
533                    attempts, e
534                );
535                debug_log(&msg);
536                for _ in 0..2048 {
537                    let _ = call::sched_yield();
538                }
539                continue;
540            }
541        };
542
543        debug_log("[fs-ext4] EXT4 mounted successfully\n");
544        debug_log("[fs-ext4] Strate ready, waiting for requests...\n");
545
546        // Create strate and start serving
547        let mut strate = Ext4Strate::new(fs);
548        strate.serve(port_handle);
549    }
550}
551
552#[panic_handler]
553/// Implements panic.
554fn panic(_info: &PanicInfo) -> ! {
555    debug_log("[fs-ext4] PANIC!\n");
556    exit(255);
557}