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 msg = format!("[fs-ext4] Probing volume handle={}\n", handle);
308    debug_log(&msg);
309    let sectors = volume_info(handle)?;
310    if sectors == 0 {
311        return Err(Error::InvalidArgument);
312    }
313    let msg = format!(
314        "[fs-ext4] Volume info OK: handle={} sectors={}\n",
315        handle, sectors
316    );
317    debug_log(&msg);
318    let mut probe = [0u8; SECTOR_SIZE];
319    let _ = volume_read(handle, 0, &mut probe, 1)?;
320    let msg = format!("[fs-ext4] Sector-0 probe OK: handle={}\n", handle);
321    debug_log(&msg);
322    Ok(sectors)
323}
324
325/// Implements discover volume handle local.
326fn discover_volume_handle_local() -> Option<u64> {
327    // Pragmatic fallback: probe low capability ids for a usable Volume handle.
328    // In current boot flow, init often receives the first inserted capability.
329    for h in 0u64..256u64 {
330        if let Ok(sectors) = validate_volume_handle(h) {
331            let msg = format!(
332                "[fs-ext4] Discovered local volume handle={} sectors={}\n",
333                h, sectors
334            );
335            debug_log(&msg);
336            return Some(h);
337        }
338    }
339    None
340}
341
342impl BlockDevice for VolumeBlockDevice {
343    /// Reads offset.
344    fn read_offset(&self, offset: usize) -> core::result::Result<Vec<u8>, BlockDeviceError> {
345        if offset % SECTOR_SIZE != 0 {
346            return Err(BlockDeviceError::InvalidOffset);
347        }
348        let sector = (offset / SECTOR_SIZE) as u64;
349        let sector_count = (BLOCK_SIZE / SECTOR_SIZE) as u64;
350        if sector_count == 0 {
351            return Err(BlockDeviceError::InvalidOffset);
352        }
353        let mut buf = vec![0u8; (sector_count as usize) * SECTOR_SIZE];
354        volume_read(self.handle, sector, &mut buf, sector_count).map_err(map_sys_err)?;
355        Ok(buf)
356    }
357
358    /// Writes offset.
359    fn write_offset(
360        &mut self,
361        offset: usize,
362        data: &[u8],
363    ) -> core::result::Result<(), BlockDeviceError> {
364        if offset % SECTOR_SIZE != 0 || data.len() % SECTOR_SIZE != 0 {
365            return Err(BlockDeviceError::InvalidOffset);
366        }
367        let sector = (offset / SECTOR_SIZE) as u64;
368        let sector_count = (data.len() / SECTOR_SIZE) as u64;
369        if sector_count == 0 {
370            return Err(BlockDeviceError::InvalidOffset);
371        }
372        volume_write(self.handle, sector, data, sector_count).map_err(map_sys_err)?;
373        Ok(())
374    }
375
376    /// Implements size.
377    fn size(&self) -> core::result::Result<usize, BlockDeviceError> {
378        Ok(self.sector_count as usize * SECTOR_SIZE)
379    }
380}
381
382/// Implements wait for bootstrap.
383fn wait_for_bootstrap(port_handle: u64) -> BootstrapInfo {
384    debug_log("[fs-ext4] Waiting for volume bootstrap...\n");
385    loop {
386        let mut msg = IpcMessage::new(0);
387        if call::ipc_recv(port_handle as usize, &mut msg).is_err() {
388            let _ = call::sched_yield();
389            continue;
390        }
391
392        if msg.msg_type == OPCODE_BOOTSTRAP && msg.flags != 0 {
393            let reply = Ext4Strate::ok_reply(msg.sender);
394            let _ = call::ipc_reply(&reply);
395            return BootstrapInfo {
396                handle: msg.flags as u64,
397                label: parse_bootstrap_label(&msg.payload),
398            };
399        }
400
401        let reply = Ext4Strate::err_reply(msg.sender, ENOSYS as u32);
402        let _ = call::ipc_reply(&reply);
403    }
404}
405
406/// Attempts to wait for bootstrap.
407fn try_wait_for_bootstrap(port_handle: u64, attempts: usize) -> Option<BootstrapInfo> {
408    for _ in 0..attempts {
409        let mut msg = IpcMessage::new(0);
410        match call::ipc_try_recv(port_handle as usize, &mut msg) {
411            Ok(_) => {
412                if msg.msg_type == OPCODE_BOOTSTRAP && msg.flags != 0 {
413                    let reply = Ext4Strate::ok_reply(msg.sender);
414                    let _ = call::ipc_reply(&reply);
415                    return Some(BootstrapInfo {
416                        handle: msg.flags as u64,
417                        label: parse_bootstrap_label(&msg.payload),
418                    });
419                }
420                let reply = Ext4Strate::err_reply(msg.sender, ENOSYS as u32);
421                let _ = call::ipc_reply(&reply);
422            }
423            Err(Error::Again) => {}
424            Err(err) => {
425                log_sys_err("try_recv bootstrap failed", err);
426            }
427        }
428        let _ = call::sched_yield();
429    }
430    None
431}
432
433#[unsafe(no_mangle)]
434/// Implements start.
435pub extern "C" fn _start(bootstrap_handle: u64) -> ! {
436    // TODO: Initialize allocator (we need heap)
437    // For now, this will panic since we can't allocate
438
439    debug_log("[fs-ext4] Starting EXT4 filesystem strate\n");
440
441    let port_handle = match call::ipc_create_port(0) {
442        Ok(h) => h as u64,
443        Err(_) => {
444            debug_log("[fs-ext4] Failed to create IPC port\n");
445            exit(1);
446        }
447    };
448
449    debug_log("[fs-ext4] IPC port created\n");
450
451    if call::ipc_bind_port(port_handle as usize, INITIAL_BIND_PATH).is_err() {
452        debug_log("[fs-ext4] Failed to bind initial port alias\n");
453        exit(2);
454    }
455
456    debug_log("[fs-ext4] Port bound to /srv/strate-fs-ext4/default\n");
457
458    let mut volume_handle = bootstrap_handle;
459    let mut bootstrap_label = String::from("default");
460    if volume_handle == 0 {
461        debug_log("[fs-ext4] Waiting for early bootstrap message...\n");
462        if let Some(info) = try_wait_for_bootstrap(port_handle, 2048) {
463            let msg = format!(
464                "[fs-ext4] Received bootstrap handle: {} label: {}\n",
465                info.handle, info.label
466            );
467            debug_log(&msg);
468            volume_handle = info.handle;
469            bootstrap_label = info.label;
470        } else if let Some(h) = discover_volume_handle_local() {
471            debug_log("[fs-ext4] Bootstrap message timeout, using local discovery fallback\n");
472            volume_handle = h;
473        } else {
474            debug_log("[fs-ext4] Bootstrap message timeout, switching to blocking wait\n");
475            let info = wait_for_bootstrap(port_handle);
476            volume_handle = info.handle;
477            bootstrap_label = info.label;
478        }
479    }
480    {
481        let msg = format!(
482            "[fs-ext4] Volume handle ready: {} label: {}\n",
483            volume_handle, bootstrap_label
484        );
485        debug_log(&msg);
486    }
487    bind_srv_alias(port_handle, &bootstrap_label);
488
489    // Mount EXT4 with retry/backoff instead of hard exit.
490    let mut attempts: u64 = 0;
491    loop {
492        attempts = attempts.wrapping_add(1);
493        if attempts <= 4 || attempts % 16 == 0 {
494            let msg = format!(
495                "[fs-ext4] Mount loop attempt={} handle={} label={}\n",
496                attempts, volume_handle, bootstrap_label
497            );
498            debug_log(&msg);
499        }
500        match validate_volume_handle(volume_handle) {
501            Ok(sectors) => {
502                let msg = format!(
503                    "[fs-ext4] volume probe OK: handle={} sectors={} (attempt={})\n",
504                    volume_handle, sectors, attempts
505                );
506                debug_log(&msg);
507            }
508            Err(err) => {
509                log_sys_err("volume probe failed", err);
510                // If we started without bootstrap capability, wait for a fresh handle periodically.
511                if bootstrap_handle == 0 && attempts % 8 == 0 {
512                    debug_log("[fs-ext4] Waiting for refreshed bootstrap handle...\n");
513                    let info = wait_for_bootstrap(port_handle);
514                    volume_handle = info.handle;
515                    bootstrap_label = info.label;
516                    let msg = format!(
517                        "[fs-ext4] Refreshed volume handle: {} label: {}\n",
518                        volume_handle, bootstrap_label
519                    );
520                    debug_log(&msg);
521                }
522                for _ in 0..2048 {
523                    let _ = call::sched_yield();
524                }
525                continue;
526            }
527        }
528
529        let device = match VolumeBlockDevice::new(volume_handle) {
530            Ok(dev) => alloc::sync::Arc::new(dev),
531            Err(e) => {
532                let msg = format!(
533                    "[fs-ext4] Failed to init volume device (attempt={}): {:?}\n",
534                    attempts, e
535                );
536                debug_log(&msg);
537                for _ in 0..2048 {
538                    let _ = call::sched_yield();
539                }
540                continue;
541            }
542        };
543
544        let msg = format!(
545            "[fs-ext4] Block device ready: handle={} (attempt={})\n",
546            volume_handle, attempts
547        );
548        debug_log(&msg);
549
550        let fs = match Ext4FileSystem::mount(device) {
551            Ok(fs) => fs,
552            Err(e) => {
553                let msg = format!(
554                    "[fs-ext4] Failed to mount EXT4 filesystem (attempt={}): {:?}\n",
555                    attempts, e
556                );
557                debug_log(&msg);
558                for _ in 0..2048 {
559                    let _ = call::sched_yield();
560                }
561                continue;
562            }
563        };
564
565        debug_log("[fs-ext4] EXT4 mounted successfully\n");
566        debug_log("[fs-ext4] Strate ready, waiting for requests...\n");
567
568        // Create strate and start serving
569        let mut strate = Ext4Strate::new(fs);
570        strate.serve(port_handle);
571    }
572}
573
574#[panic_handler]
575/// Implements panic.
576fn panic(_info: &PanicInfo) -> ! {
577    debug_log("[fs-ext4] PANIC!\n");
578    exit(255);
579}