Skip to main content

strat9_kernel/vfs/
mod.rs

1//! Virtual File System (VFS) - Plan 9-inspired namespace.
2//!
3//! The VFS provides:
4//! - Scheme abstraction: Pluggable backends (IPC, kernel, devices)
5//! - Mount table: Map path prefixes to schemes
6//! - File descriptors: Per-process FD tables
7//! - Path resolution: Navigate the namespace hierarchy
8//!
9//! ## Architecture
10//!
11//! ```text
12//! User syscall (open "/net/tcp/0")
13//!      ↓
14//! VFS::open() : path resolution
15//!      ↓
16//! MountTable::resolve() → ("/net" → IpcScheme, "tcp/0")
17//!      ↓
18//! IpcScheme::open("tcp/0") → IPC message to network stack
19//!      ↓
20//! OpenFile created with scheme reference + file_id
21//!      ↓
22//! FD allocated in process FD table
23//!      ↓
24//! Returns FD to userspace
25//! ```
26
27pub mod blkdev_scheme;
28pub mod console_scheme;
29pub mod fd;
30pub mod file;
31pub mod input_scheme;
32pub mod ipcfs;
33pub mod mount;
34pub mod pipe;
35pub mod procfs;
36pub mod pty_scheme;
37pub mod ramfs_scheme;
38pub mod scheme;
39pub mod scheme_router;
40
41use crate::{process::current_task_clone, sync::SpinLock, syscall::error::SyscallError};
42use alloc::{boxed::Box, string::String, sync::Arc};
43use core::fmt::Write;
44
45pub use blkdev_scheme::BlkDevScheme;
46pub use fd::{FileDescriptorTable, STDERR, STDIN, STDOUT};
47pub use file::OpenFile;
48pub use mount::{list_mounts, mount, resolve, unmount, Namespace};
49pub use pipe::PipeScheme;
50pub use procfs::ProcScheme;
51pub use ramfs_scheme::RamfsScheme;
52pub use scheme::{
53    DirEntry, DynScheme, FileFlags, FileStat, IpcScheme, KernelScheme, OpenFlags, Scheme,
54};
55pub use scheme_router::{
56    get_initfs_file_bytes, init_builtin_schemes, list_schemes, mount_scheme, register_initfs_file,
57    register_scheme,
58};
59
60use crate::memory::{UserSliceRead, UserSliceWrite};
61
62// Re-export AT_FDCWD from the ABI so VFS callers can use it.
63pub use crate::syscall::numbers::AT_FDCWD;
64
65// ============================================================================
66// High-level VFS API
67// ============================================================================
68
69/// Open a file and return a file descriptor.
70///
71/// This is the main entry point for opening files from userspace.
72pub fn open(path: &str, flags: OpenFlags) -> Result<u32, SyscallError> {
73    // Resolve path to (scheme, relative_path)
74    let (scheme, relative_path) = mount::resolve(path)?;
75
76    // Open the file via the scheme
77    let open_result = scheme.open(&relative_path, flags)?;
78
79    // Create OpenFile wrapper : use the original path for the OpenFile
80    // (not relative_path, which is scheme-local).
81    let open_file = Arc::new(OpenFile::new(
82        scheme,
83        open_result.file_id,
84        String::from(path),
85        flags,
86        open_result.flags,
87        open_result.size,
88    ));
89
90    // Insert into current task's FD table
91    let task = current_task_clone().ok_or(SyscallError::PermissionDenied)?;
92    // SAFETY: We're in syscall context, have exclusive access to FD table
93    let fd = unsafe { (&mut *task.process.fd_table.get()).insert(open_file) };
94
95    Ok(fd)
96}
97
98/// Open a file with an already-resolved path (avoids double mount::resolve).
99///
100/// Used by `sys_open` and `open_at` which resolve the path once before calling
101/// this. The silo permission check must have already been performed.
102fn open_resolved(path: &str, flags: OpenFlags) -> Result<u32, SyscallError> {
103    let (scheme, relative_path) = mount::resolve(path)?;
104    let open_result = scheme.open(&relative_path, flags)?;
105
106    let open_file = Arc::new(OpenFile::new(
107        scheme,
108        open_result.file_id,
109        String::from(path),
110        flags,
111        open_result.flags,
112        open_result.size,
113    ));
114
115    let task = current_task_clone().ok_or(SyscallError::PermissionDenied)?;
116    let fd = unsafe { (&mut *task.process.fd_table.get()).insert(open_file) };
117
118    Ok(fd)
119}
120
121/// Resolve `path` against the current task CWD and enforce silo path policy.
122pub fn resolve_and_check_path_for_current_task(
123    path: &str,
124    want_read: bool,
125    want_write: bool,
126    want_execute: bool,
127) -> Result<String, SyscallError> {
128    let task = current_task_clone().ok_or(SyscallError::PermissionDenied)?;
129    let cwd = unsafe { (&*task.process.cwd.get()).clone() };
130    let abs = resolve_path(path, &cwd);
131    crate::silo::enforce_path_for_current_task(&abs, want_read, want_write, want_execute)?;
132    Ok(abs)
133}
134
135/// Open a file relative to a directory FD.
136///
137/// - If `dir_fd == AT_FDCWD`, resolve against the process CWD (equivalent to `open()`).
138/// - Otherwise, resolve against the path of the given directory FD.
139///
140/// This is the foundation for capability-based path resolution: the directory
141/// FD acts as the root of the namespace for this operation, naturally
142/// preventing `../` escapes beyond the FD's subtree.
143pub fn open_at(dir_fd: u64, path: &str, flags: OpenFlags) -> Result<u32, SyscallError> {
144    if dir_fd == AT_FDCWD as u64 {
145        // Fall back to process CWD : original open() behavior.
146        let task = current_task_clone().ok_or(SyscallError::PermissionDenied)?;
147        let cwd = unsafe { (&*task.process.cwd.get()).clone() };
148        let abs = resolve_path(path, &cwd);
149        crate::silo::enforce_path_for_current_task(
150            &abs,
151            flags.contains(OpenFlags::READ) || flags.contains(OpenFlags::DIRECTORY),
152            flags.contains(OpenFlags::WRITE) || flags.contains(OpenFlags::CREATE),
153            false,
154        )?;
155        open_resolved(&abs, flags)
156    } else {
157        // Resolve relative to the directory FD.
158        let task = current_task_clone().ok_or(SyscallError::PermissionDenied)?;
159        let fd_table = unsafe { &*task.process.fd_table.get() };
160        let dir_file = fd_table.get(dir_fd as u32)?;
161        if !dir_file.flags().contains(FileFlags::DIRECTORY) {
162            return Err(SyscallError::NotADirectory);
163        }
164        let dir_path = dir_file.path();
165        let abs = resolve_path(path, dir_path);
166        crate::silo::enforce_path_for_current_task(
167            &abs,
168            flags.contains(OpenFlags::READ) || flags.contains(OpenFlags::DIRECTORY),
169            flags.contains(OpenFlags::WRITE) || flags.contains(OpenFlags::CREATE),
170            false,
171        )?;
172        open_resolved(&abs, flags)
173    }
174}
175
176/// Stat a file relative to a directory FD.
177///
178/// Same resolution semantics as `open_at`.
179pub fn fstat_at(dir_fd: u64, path: &str) -> Result<FileStat, SyscallError> {
180    if dir_fd == AT_FDCWD as u64 {
181        let task = current_task_clone().ok_or(SyscallError::PermissionDenied)?;
182        let cwd = unsafe { (&*task.process.cwd.get()).clone() };
183        let abs = resolve_path(path, &cwd);
184        stat_path(&abs)
185    } else {
186        let task = current_task_clone().ok_or(SyscallError::PermissionDenied)?;
187        let fd_table = unsafe { &*task.process.fd_table.get() };
188        let dir_file = fd_table.get(dir_fd as u32)?;
189        if !dir_file.flags().contains(FileFlags::DIRECTORY) {
190            return Err(SyscallError::NotADirectory);
191        }
192        let dir_path = dir_file.path();
193        let abs = resolve_path(path, dir_path);
194        stat_path(&abs)
195    }
196}
197
198/// Create a directory.
199pub fn mkdir(path: &str, mode: u32) -> Result<(), SyscallError> {
200    let (scheme, relative_path) = mount::resolve(path)?;
201    scheme.create_directory(&relative_path, mode)?;
202    Ok(())
203}
204
205/// Create an empty regular file.
206pub fn create_file(path: &str, mode: u32) -> Result<(), SyscallError> {
207    let (scheme, relative_path) = mount::resolve(path)?;
208    scheme.create_file(&relative_path, mode)?;
209    Ok(())
210}
211
212/// Remove a file or directory.
213pub fn unlink(path: &str) -> Result<(), SyscallError> {
214    let (scheme, relative_path) = mount::resolve(path)?;
215    scheme.unlink(&relative_path)?;
216    Ok(())
217}
218
219/// Rename a file or directory (must be within the same mount).
220pub fn rename(old_path: &str, new_path: &str) -> Result<(), SyscallError> {
221    let (scheme, old_rel) = mount::resolve(old_path)?;
222    let (scheme2, new_rel) = mount::resolve(new_path)?;
223    if !Arc::ptr_eq(&scheme, &scheme2) {
224        return Err(SyscallError::NotSupported);
225    }
226    scheme.rename(&old_rel, &new_rel)
227}
228
229/// Change permission bits on a path.
230pub fn chmod(path: &str, mode: u32) -> Result<(), SyscallError> {
231    let (scheme, relative_path) = mount::resolve(path)?;
232    scheme.chmod(&relative_path, mode)
233}
234
235/// Change permission bits on an open fd.
236pub fn fchmod(fd: u32, mode: u32) -> Result<(), SyscallError> {
237    let task = current_task_clone().ok_or(SyscallError::PermissionDenied)?;
238    let fd_table = unsafe { &*task.process.fd_table.get() };
239    let file = fd_table.get(fd)?;
240    file.scheme().fchmod(file.file_id(), mode)
241}
242
243/// Truncate a file by path.
244///
245/// Opens the file with WRITE, truncates, and closes. The scheme's truncate()
246/// method is called directly if available; otherwise opens+closes as fallback.
247pub fn truncate(path: &str, length: u64) -> Result<(), SyscallError> {
248    let (scheme, relative_path) = mount::resolve(path)?;
249
250    // Try direct truncate via path first (avoids open/close round-trip).
251    // Only works if the scheme supports truncate by path.
252    match scheme.truncate_by_path(&relative_path, length) {
253        Ok(()) => return Ok(()),
254        Err(SyscallError::NotImplemented) => {
255            // Scheme doesn't support path-based truncate: fall back to
256            // open + truncate + close.
257        }
258        Err(e) => return Err(e),
259    }
260
261    let res = scheme.open(&relative_path, OpenFlags::WRITE)?;
262    let r = scheme.truncate(res.file_id, length);
263    let _ = scheme.close(res.file_id);
264    r
265}
266
267/// Truncate an open fd.
268pub fn ftruncate(fd: u32, length: u64) -> Result<(), SyscallError> {
269    let task = current_task_clone().ok_or(SyscallError::PermissionDenied)?;
270    let fd_table = unsafe { &*task.process.fd_table.get() };
271    let file = fd_table.get(fd)?;
272    file.scheme().truncate(file.file_id(), length)
273}
274
275/// Create a hard link (must be within the same mount).
276pub fn link(old_path: &str, new_path: &str) -> Result<(), SyscallError> {
277    let (scheme, old_rel) = mount::resolve(old_path)?;
278    let (scheme2, new_rel) = mount::resolve(new_path)?;
279    if !Arc::ptr_eq(&scheme, &scheme2) {
280        return Err(SyscallError::NotSupported);
281    }
282    scheme.link(&old_rel, &new_rel)
283}
284
285/// Create a symbolic link.
286pub fn symlink(target: &str, link_path: &str) -> Result<(), SyscallError> {
287    let (scheme, link_rel) = mount::resolve(link_path)?;
288    scheme.symlink(target, &link_rel)
289}
290
291/// Read the target of a symbolic link.
292pub fn readlink(path: &str) -> Result<String, SyscallError> {
293    let (scheme, relative_path) = mount::resolve(path)?;
294    scheme.readlink(&relative_path)
295}
296
297/// Read from a file descriptor.
298pub fn read(fd: u32, buf: &mut [u8]) -> Result<usize, SyscallError> {
299    let task = current_task_clone().ok_or(SyscallError::PermissionDenied)?;
300    // SAFETY: Syscall context
301    let fd_table = unsafe { &*task.process.fd_table.get() };
302    let file = fd_table.get(fd)?;
303    file.read(buf)
304}
305
306/// Write to a file descriptor.
307pub fn write(fd: u32, buf: &[u8]) -> Result<usize, SyscallError> {
308    let task = current_task_clone().ok_or(SyscallError::PermissionDenied)?;
309    // SAFETY: Syscall context
310    let fd_table = unsafe { &*task.process.fd_table.get() };
311    let file = fd_table.get(fd)?;
312    file.write(buf)
313}
314
315/// Close a file descriptor.
316///
317/// Removes the fd from the table.  If this was the last Arc<OpenFile> reference
318/// (no dup'd / fork'd copies remain) the Drop impl will call scheme.close().
319pub fn close(fd: u32) -> Result<(), SyscallError> {
320    let task = current_task_clone().ok_or(SyscallError::PermissionDenied)?;
321    // SAFETY: Syscall context
322    let fd_table = unsafe { &mut *task.process.fd_table.get() };
323    let _file = fd_table.remove(fd)?;
324    Ok(())
325    // _file (Arc<OpenFile>) is dropped here; if refcount → 0, Drop fires → scheme.close()
326}
327
328/// Seek within a file.
329pub fn seek(fd: u32, offset: u64) -> Result<u64, SyscallError> {
330    let task = current_task_clone().ok_or(SyscallError::PermissionDenied)?;
331    // SAFETY: Syscall context
332    let fd_table = unsafe { &*task.process.fd_table.get() };
333    let file = fd_table.get(fd)?;
334    file.seek(offset)
335}
336
337/// Get current offset in a file.
338pub fn tell(fd: u32) -> Result<u64, SyscallError> {
339    let task = current_task_clone().ok_or(SyscallError::PermissionDenied)?;
340    // SAFETY: Syscall context
341    let fd_table = unsafe { &*task.process.fd_table.get() };
342    let file = fd_table.get(fd)?;
343    Ok(file.tell())
344}
345
346/// Get file size.
347pub fn fsize(fd: u32) -> Result<u64, SyscallError> {
348    let task = current_task_clone().ok_or(SyscallError::PermissionDenied)?;
349    // SAFETY: Syscall context
350    let fd_table = unsafe { &*task.process.fd_table.get() };
351    let file = fd_table.get(fd)?;
352    file.size()
353}
354
355/// Sync file to storage.
356pub fn fsync(fd: u32) -> Result<(), SyscallError> {
357    let task = current_task_clone().ok_or(SyscallError::PermissionDenied)?;
358    // SAFETY: Syscall context
359    let fd_table = unsafe { &*task.process.fd_table.get() };
360    let file = fd_table.get(fd)?;
361    file.sync()
362}
363
364/// POSIX lseek on a file descriptor.
365pub fn lseek(fd: u32, offset: i64, whence: u32) -> Result<u64, SyscallError> {
366    let task = current_task_clone().ok_or(SyscallError::PermissionDenied)?;
367    let fd_table = unsafe { &*task.process.fd_table.get() };
368    let file = fd_table.get(fd)?;
369    file.lseek(offset, whence)
370}
371
372/// fstat on an open file descriptor.
373pub fn fstat(fd: u32) -> Result<FileStat, SyscallError> {
374    let task = current_task_clone().ok_or(SyscallError::PermissionDenied)?;
375    let fd_table = unsafe { &*task.process.fd_table.get() };
376    let file = fd_table.get(fd)?;
377    file.stat()
378}
379
380/// stat by path (opens, stats, closes).
381pub fn stat_path(path: &str) -> Result<FileStat, SyscallError> {
382    let (scheme, relative_path) = mount::resolve(path)?;
383    let open_result = scheme.open(&relative_path, OpenFlags::READ)?;
384    let result = scheme.stat(open_result.file_id);
385    let _ = scheme.close(open_result.file_id);
386    result
387}
388
389/// Read directory entries from an open directory fd.
390pub fn getdents(fd: u32) -> Result<alloc::vec::Vec<DirEntry>, SyscallError> {
391    let task = current_task_clone().ok_or(SyscallError::PermissionDenied)?;
392    let fd_table = unsafe { &*task.process.fd_table.get() };
393    let file = fd_table.get(fd)?;
394    file.readdir()
395}
396
397/// Create a background stdin: a pipe read-end whose write end is immediately
398/// closed.  Any `read()` on the returned file will return 0 (EOF) at once,
399/// preventing processes launched in the background from blocking on stdin or
400/// spinning on EBADF.
401pub fn create_background_stdin() -> Arc<OpenFile> {
402    let pipe_scheme = get_pipe_scheme();
403    let (base_id, pipe) = pipe_scheme.create_pipe();
404
405    // Close write end now (refcount → 0 → write_closed = true).
406    // Subsequent reads on the read end will return EOF immediately.
407    pipe.close_write();
408
409    let dyn_scheme: DynScheme = pipe_scheme as Arc<dyn Scheme>;
410    Arc::new(OpenFile::new(
411        dyn_scheme,
412        base_id, // even = read end
413        String::from("pipe:[bg-stdin]"),
414        OpenFlags::READ,
415        FileFlags::PIPE,
416        None,
417    ))
418}
419
420/// Create a pipe, returning (read_fd, write_fd).
421pub fn pipe() -> Result<(u32, u32), SyscallError> {
422    let pipe_scheme = get_pipe_scheme();
423    let (base_id, _pipe) = pipe_scheme.create_pipe();
424
425    let dyn_scheme: DynScheme = pipe_scheme as Arc<dyn Scheme>;
426
427    let read_file = Arc::new(OpenFile::new(
428        dyn_scheme.clone(),
429        base_id,
430        String::from("pipe:[read]"),
431        OpenFlags::READ,
432        FileFlags::PIPE,
433        None,
434    ));
435    let write_file = Arc::new(OpenFile::new(
436        dyn_scheme.clone(),
437        base_id + 1,
438        String::from("pipe:[write]"),
439        OpenFlags::WRITE,
440        FileFlags::PIPE,
441        None,
442    ));
443
444    let task = current_task_clone().ok_or(SyscallError::PermissionDenied)?;
445    let fd_table = unsafe { &mut *task.process.fd_table.get() };
446    let read_fd = fd_table.insert(read_file);
447    let write_fd = fd_table.insert(write_file);
448
449    Ok((read_fd, write_fd))
450}
451
452/// Duplicate a file descriptor (POSIX dup).
453pub fn dup(old_fd: u32) -> Result<u32, SyscallError> {
454    let task = current_task_clone().ok_or(SyscallError::PermissionDenied)?;
455    let fd_table = unsafe { &mut *task.process.fd_table.get() };
456    fd_table.duplicate(old_fd)
457}
458
459/// Duplicate a file descriptor to a specific number (POSIX dup2).
460pub fn dup2(old_fd: u32, new_fd: u32) -> Result<u32, SyscallError> {
461    let task = current_task_clone().ok_or(SyscallError::PermissionDenied)?;
462    let fd_table = unsafe { &mut *task.process.fd_table.get() };
463    fd_table.duplicate_to(old_fd, new_fd)
464}
465
466/// Read all remaining bytes from a file descriptor.
467pub fn read_all(fd: u32) -> Result<alloc::vec::Vec<u8>, SyscallError> {
468    let mut out = alloc::vec::Vec::new();
469    let mut buf = [0u8; 4096];
470    loop {
471        let n = read(fd, &mut buf)?;
472        if n == 0 {
473            break;
474        }
475        out.extend_from_slice(&buf[..n]);
476    }
477    Ok(out)
478}
479
480// ============================================================================
481// Syscall Handlers (Native ABI)
482// ============================================================================
483
484/// Syscall handler for opening a file.
485pub fn sys_open(path_ptr: u64, path_len: u64, flags: u64) -> Result<u64, SyscallError> {
486    let open_flags = OpenFlags::from_bits_truncate(flags as u32);
487    let want_read =
488        open_flags.contains(OpenFlags::READ) || open_flags.contains(OpenFlags::DIRECTORY);
489    let want_write = open_flags.contains(OpenFlags::WRITE)
490        || open_flags.contains(OpenFlags::CREATE)
491        || open_flags.contains(OpenFlags::TRUNCATE)
492        || open_flags.contains(OpenFlags::APPEND);
493    let path = resolve_for_syscall(path_ptr, path_len, want_read, want_write, false)?;
494    let fd = open_resolved(&path, open_flags)?;
495    Ok(fd as u64)
496}
497
498/// SYS_OPENAT (462): Open a file relative to a directory FD.
499pub fn sys_openat(
500    dir_fd: u64,
501    path_ptr: u64,
502    path_len: u64,
503    flags: u64,
504) -> Result<u64, SyscallError> {
505    const MAX_PATH_LEN: usize = 4096;
506    if path_len == 0 || path_len as usize > MAX_PATH_LEN {
507        return Err(SyscallError::InvalidArgument);
508    }
509    let raw = read_user_path(path_ptr, path_len)?;
510    let open_flags = OpenFlags::from_bits_truncate(flags as u32);
511    let fd = open_at(dir_fd, &raw, open_flags)?;
512    Ok(fd as u64)
513}
514
515/// SYS_FSTATAT (463): Stat a file relative to a directory FD.
516pub fn sys_fstatat(
517    dir_fd: u64,
518    path_ptr: u64,
519    path_len: u64,
520    _flags: u64,
521) -> Result<u64, SyscallError> {
522    const MAX_PATH_LEN: usize = 4096;
523    if path_len == 0 || path_len as usize > MAX_PATH_LEN {
524        return Err(SyscallError::InvalidArgument);
525    }
526    let raw = read_user_path(path_ptr, path_len)?;
527    let st = fstat_at(dir_fd, &raw)?;
528    // NOTE: stat_ptr is not provided in this syscall signature; the caller
529    // must use SYS_FSTAT with an open FD to get the stat struct.
530    // For now, return 0 on success (file exists).
531    let _ = st;
532    Ok(0)
533}
534
535/// SYS_ACCESS (413): Check file access permissions without opening a FD.
536pub fn sys_access(path_ptr: u64, path_len: u64, mode: u64) -> Result<u64, SyscallError> {
537    let path = resolve_for_syscall(path_ptr, path_len, true, false, false)?;
538    let st = stat_path(&path)?;
539    check_access_permissions(&st, mode as u32)?;
540    Ok(0)
541}
542
543/// SYS_FACCESSAT (468): Check file access relative to a directory FD.
544pub fn sys_faccessat(
545    dir_fd: u64,
546    path_ptr: u64,
547    path_len: u64,
548    mode: u64,
549    _flags: u64,
550) -> Result<u64, SyscallError> {
551    let raw = read_user_path(path_ptr, path_len)?;
552
553    let abs = if dir_fd == AT_FDCWD as u64 {
554        let task = current_task_clone().ok_or(SyscallError::PermissionDenied)?;
555        let cwd = unsafe { (&*task.process.cwd.get()).clone() };
556        resolve_path(&raw, &cwd)
557    } else {
558        let task = current_task_clone().ok_or(SyscallError::PermissionDenied)?;
559        let fd_table = unsafe { &*task.process.fd_table.get() };
560        let dir_file = fd_table.get(dir_fd as u32)?;
561        let dir_path = dir_file.path();
562        resolve_path(&raw, dir_path)
563    };
564
565    crate::silo::enforce_path_for_current_task(&abs, true, false, false)?;
566    let st = stat_path(&abs)?;
567    check_access_permissions(&st, mode as u32)?;
568    Ok(0)
569}
570
571/// POSIX access mode flags
572const F_OK: u32 = 0;
573const R_OK: u32 = 4;
574const W_OK: u32 = 2;
575const X_OK: u32 = 1;
576
577/// Check file permissions against the access mode.
578///
579/// F_OK (0): just check that the file exists (already done by stat_path).
580/// R_OK/W_OK/X_OK: check the corresponding permission bits for the current
581/// process's real UID/GID (access() semantics).
582fn check_access_permissions(st: &FileStat, mode: u32) -> Result<(), SyscallError> {
583    if mode == F_OK {
584        return Ok(()); // File exists (we got stat successfully)
585    }
586
587    // Get real UID/GID (access() uses REAL IDs, not effective)
588    let task = current_task_clone().ok_or(SyscallError::PermissionDenied)?;
589    let uid = task.uid.load(core::sync::atomic::Ordering::Relaxed);
590    let gid = task.gid.load(core::sync::atomic::Ordering::Relaxed);
591
592    let file_uid = st.st_uid;
593    let file_gid = st.st_gid;
594    let file_mode = st.st_mode;
595
596    let mut granted = 0u32;
597
598    // Owner permissions
599    if uid == file_uid {
600        if file_mode & 0o400 != 0 {
601            granted |= R_OK;
602        }
603        if file_mode & 0o200 != 0 {
604            granted |= W_OK;
605        }
606        if file_mode & 0o100 != 0 {
607            granted |= X_OK;
608        }
609    }
610    // Group permissions
611    if gid == file_gid {
612        if file_mode & 0o040 != 0 {
613            granted |= R_OK;
614        }
615        if file_mode & 0o020 != 0 {
616            granted |= W_OK;
617        }
618        if file_mode & 0o010 != 0 {
619            granted |= X_OK;
620        }
621    }
622    // Other permissions
623    if file_mode & 0o004 != 0 {
624        granted |= R_OK;
625    }
626    if file_mode & 0o002 != 0 {
627        granted |= W_OK;
628    }
629    if file_mode & 0o001 != 0 {
630        granted |= X_OK;
631    }
632
633    // Check that ALL requested bits are granted
634    if mode & !granted != 0 {
635        return Err(SyscallError::PermissionDenied);
636    }
637    Ok(())
638}
639
640/// Syscall handler for reading from a file.
641pub fn sys_read(fd: u32, buf_ptr: u64, buf_len: u64) -> Result<u64, SyscallError> {
642    if buf_len == 0 {
643        return Ok(0);
644    }
645
646    // Read directly into chunks to avoid large kernel allocations
647    let mut kbuf = [0u8; 4096];
648    let mut total_read = 0;
649
650    while total_read < buf_len as usize {
651        let to_read = core::cmp::min(kbuf.len(), buf_len as usize - total_read);
652        let n = read(fd, &mut kbuf[..to_read])?;
653        if n == 0 {
654            break;
655        }
656
657        let chunk_addr = buf_ptr
658            .checked_add(total_read as u64)
659            .ok_or(SyscallError::Fault)?;
660        let chunk_user = UserSliceWrite::new(chunk_addr, n)?;
661        chunk_user.copy_from(&kbuf[..n]);
662
663        total_read += n;
664        if n < to_read {
665            break;
666        }
667    }
668
669    Ok(total_read as u64)
670}
671
672/// Syscall handler for writing to a file.
673pub fn sys_write(fd: u32, buf_ptr: u64, buf_len: u64) -> Result<u64, SyscallError> {
674    if buf_len == 0 {
675        return Ok(0);
676    }
677
678    // For stdout/stderr, fall back to direct console output only when no
679    // FD entry exists (early boot).  Once the FD table is populated (or
680    // after dup2 redirection) the normal VFS path is used.
681    if fd == 1 || fd == 2 {
682        let use_console = match current_task_clone() {
683            Some(t) => {
684                let fd_table = unsafe { &*t.process.fd_table.get() };
685                !fd_table.contains(fd)
686            }
687            None => true,
688        };
689        if use_console {
690            crate::silo::enforce_console_access()?;
691            let len = core::cmp::min(buf_len as usize, 16 * 1024);
692            let mut kbuf = [0u8; 4096];
693            let mut total_written = 0;
694            while total_written < len {
695                let to_write = core::cmp::min(kbuf.len(), len - total_written);
696                let chunk_addr = buf_ptr
697                    .checked_add(total_written as u64)
698                    .ok_or(SyscallError::Fault)?;
699                let chunk = UserSliceRead::new(chunk_addr, to_write)?;
700                let n = chunk.copy_to(&mut kbuf[..to_write]);
701                if crate::arch::x86_64::vga::is_available() {
702                    if let Ok(s) = core::str::from_utf8(&kbuf[..n]) {
703                        crate::serial_print!("{}", s);
704                        crate::vga_print!("{}", s);
705                    } else {
706                        for &byte in &kbuf[..n] {
707                            crate::serial_print!("{}", byte as char);
708                        }
709                    }
710                } else {
711                    for &byte in &kbuf[..n] {
712                        crate::serial_print!("{}", byte as char);
713                    }
714                }
715                total_written += n;
716            }
717            return Ok(total_written as u64);
718        }
719    }
720
721    let mut kbuf = [0u8; 4096];
722    let mut total_written = 0;
723
724    while total_written < buf_len as usize {
725        let to_write = core::cmp::min(kbuf.len(), buf_len as usize - total_written);
726        let chunk_addr = buf_ptr
727            .checked_add(total_written as u64)
728            .ok_or(SyscallError::Fault)?;
729        let chunk_user = UserSliceRead::new(chunk_addr, to_write)?;
730        chunk_user.copy_to(&mut kbuf[..to_write]);
731
732        let n = write(fd, &kbuf[..to_write])?;
733        total_written += n;
734        if n < to_write {
735            break;
736        }
737    }
738
739    Ok(total_written as u64)
740}
741
742/// Syscall handler for closing a file.
743pub fn sys_close(fd: u32) -> Result<u64, SyscallError> {
744    close(fd)?;
745    Ok(0)
746}
747
748/// Syscall handler for lseek.
749pub fn sys_lseek(fd: u32, offset: i64, whence: u32) -> Result<u64, SyscallError> {
750    lseek(fd, offset, whence)
751}
752
753/// Syscall handler for fstat.
754pub fn sys_fstat(fd: u32, stat_ptr: u64) -> Result<u64, SyscallError> {
755    let st = fstat(fd)?;
756    let user = UserSliceWrite::new(stat_ptr, core::mem::size_of::<FileStat>())?;
757    let bytes = unsafe {
758        core::slice::from_raw_parts(
759            &st as *const FileStat as *const u8,
760            core::mem::size_of::<FileStat>(),
761        )
762    };
763    user.copy_from(bytes);
764    Ok(0)
765}
766
767/// SYS_STAT (409): Get file status by path.
768pub fn sys_stat(path_ptr: u64, path_len: u64, stat_ptr: u64) -> Result<u64, SyscallError> {
769    let path = resolve_for_syscall(path_ptr, path_len, true, false, false)?;
770    let st = stat_path(&path)?;
771    let user_out = UserSliceWrite::new(stat_ptr, core::mem::size_of::<FileStat>())?;
772    let out_bytes = unsafe {
773        core::slice::from_raw_parts(
774            &st as *const FileStat as *const u8,
775            core::mem::size_of::<FileStat>(),
776        )
777    };
778    user_out.copy_from(out_bytes);
779    Ok(0)
780}
781
782/// Syscall handler for getdents.
783///
784/// Writes a packed array of `KernelDirent` entries into the user buffer.
785/// Returns the number of bytes written.
786pub fn sys_getdents(fd: u32, buf_ptr: u64, buf_len: u64) -> Result<u64, SyscallError> {
787    use strat9_abi::data::DirentHeader;
788
789    let entries = getdents(fd)?;
790    let mut offset: usize = 0;
791    let buf_size = buf_len as usize;
792
793    for entry in &entries {
794        let name_bytes = entry.name.as_bytes();
795        let name_len = core::cmp::min(name_bytes.len(), 255) as u16;
796        let entry_size = DirentHeader::SIZE + name_len as usize + 1;
797
798        if offset + entry_size > buf_size {
799            break;
800        }
801
802        let user = UserSliceWrite::new(buf_ptr + offset as u64, entry_size)?;
803        let mut kbuf = [0u8; 268];
804        kbuf[0..8].copy_from_slice(&entry.ino.to_le_bytes());
805        kbuf[8] = entry.file_type;
806        kbuf[9..11].copy_from_slice(&name_len.to_le_bytes());
807        kbuf[11] = 0; // DirentHeader::_padding
808        kbuf[12..12 + name_len as usize].copy_from_slice(&name_bytes[..name_len as usize]);
809        kbuf[12 + name_len as usize] = 0;
810        user.copy_from(&kbuf[..entry_size]);
811
812        offset += entry_size;
813    }
814
815    Ok(offset as u64)
816}
817
818/// Syscall handler for pipe.
819pub fn sys_pipe(fds_ptr: u64) -> Result<u64, SyscallError> {
820    let (read_fd, write_fd) = pipe()?;
821    let user = UserSliceWrite::new(fds_ptr, 8)?; // 2 x u32
822    let mut buf = [0u8; 8];
823    buf[0..4].copy_from_slice(&read_fd.to_le_bytes());
824    buf[4..8].copy_from_slice(&write_fd.to_le_bytes());
825    user.copy_from(&buf);
826    Ok(0)
827}
828
829/// Syscall handler for dup.
830pub fn sys_dup(old_fd: u32) -> Result<u64, SyscallError> {
831    let new_fd = dup(old_fd)?;
832    Ok(new_fd as u64)
833}
834
835/// Syscall handler for dup2.
836pub fn sys_dup2(old_fd: u32, new_fd: u32) -> Result<u64, SyscallError> {
837    let fd = dup2(old_fd, new_fd)?;
838    Ok(fd as u64)
839}
840
841// ========== Path helpers ==============================
842
843/// Read a NUL-terminated or length-bounded path from user space.
844///
845/// `path_ptr` and `path_len` come directly from syscall arguments.
846/// If `path_len` is 0 the string is assumed to be NUL-terminated up to 4096 bytes.
847fn read_user_path(path_ptr: u64, path_len: u64) -> Result<alloc::string::String, SyscallError> {
848    const MAX_PATH: usize = 4096;
849    let len = if path_len == 0 || path_len as usize > MAX_PATH {
850        MAX_PATH
851    } else {
852        path_len as usize
853    };
854    let user = UserSliceRead::new(path_ptr, len)?;
855    let bytes = user.read_to_vec();
856    // Trim at first NUL byte if present.
857    let trimmed = bytes.split(|&b| b == 0).next().unwrap_or(&bytes);
858    if trimmed.is_empty() {
859        return Err(SyscallError::InvalidArgument);
860    }
861    core::str::from_utf8(trimmed)
862        .map(|s| alloc::string::String::from(s))
863        .map_err(|_| SyscallError::InvalidArgument)
864}
865
866/// Resolve a userspace path to an absolute path and enforce silo policy.
867///
868/// Combines: read_user_path → resolve against CWD → silo enforce.
869/// Used by most syscall handlers to avoid duplicating this 4-line pattern.
870fn resolve_for_syscall(
871    path_ptr: u64,
872    path_len: u64,
873    want_read: bool,
874    want_write: bool,
875    want_execute: bool,
876) -> Result<alloc::string::String, SyscallError> {
877    const MAX_PATH_LEN: usize = 4096;
878    if path_len == 0 || path_len as usize > MAX_PATH_LEN {
879        return Err(SyscallError::InvalidArgument);
880    }
881    let raw = read_user_path(path_ptr, path_len)?;
882    let task = current_task_clone().ok_or(SyscallError::PermissionDenied)?;
883    let cwd = unsafe { (&*task.process.cwd.get()).clone() };
884    let abs = resolve_path(&raw, &cwd);
885    crate::silo::enforce_path_for_current_task(&abs, want_read, want_write, want_execute)?;
886    Ok(abs)
887}
888
889/// Resolve `path` relative to the current working directory when it is not
890/// absolute. Returns the normalized absolute path.
891fn resolve_path(path: &str, cwd: &str) -> alloc::string::String {
892    let raw = if path.starts_with('/') {
893        alloc::string::String::from(path)
894    } else if cwd.ends_with('/') {
895        alloc::format!("{}{}", cwd, path)
896    } else {
897        alloc::format!("{}/{}", cwd, path)
898    };
899    normalize_path(&raw)
900}
901
902/// Collapse `.`, `..` and duplicate `/` in an absolute path.
903fn normalize_path(path: &str) -> alloc::string::String {
904    let mut parts: alloc::vec::Vec<&str> = alloc::vec::Vec::new();
905    for seg in path.split('/') {
906        match seg {
907            "" | "." => {}
908            ".." => {
909                parts.pop();
910            }
911            other => parts.push(other),
912        }
913    }
914    let mut out = alloc::string::String::with_capacity(path.len());
915    if parts.is_empty() {
916        out.push('/');
917    } else {
918        for p in &parts {
919            out.push('/');
920            out.push_str(p);
921        }
922    }
923    out
924}
925
926// ========== New VFS syscall handlers ================================================================================================================================================================
927
928/// SYS_CHDIR (440): Change current working directory.
929pub fn sys_chdir(path_ptr: u64, path_len: u64) -> Result<u64, SyscallError> {
930    let abs = resolve_for_syscall(path_ptr, path_len, true, false, false)?;
931
932    let (scheme, rel) = mount::resolve(&abs)?;
933    let res = scheme.open(&rel, OpenFlags::READ | OpenFlags::DIRECTORY)?;
934    let _ = scheme.close(res.file_id);
935
936    let task = current_task_clone().ok_or(SyscallError::PermissionDenied)?;
937    unsafe { *task.process.cwd.get() = abs };
938    Ok(0)
939}
940
941/// SYS_FCHDIR (441): Change cwd using an open file descriptor.
942pub fn sys_fchdir(fd: u32) -> Result<u64, SyscallError> {
943    let task = current_task_clone().ok_or(SyscallError::PermissionDenied)?;
944    let path = {
945        let fd_table = unsafe { &*task.process.fd_table.get() };
946        let file = fd_table.get(fd)?;
947        alloc::string::String::from(file.path())
948    };
949    unsafe { *task.process.cwd.get() = path };
950    Ok(0)
951}
952
953/// SYS_GETCWD (442): Write the current working directory into a user buffer.
954pub fn sys_getcwd(buf_ptr: u64, buf_len: u64) -> Result<u64, SyscallError> {
955    if buf_len == 0 {
956        return Err(SyscallError::InvalidArgument);
957    }
958    let task = current_task_clone().ok_or(SyscallError::PermissionDenied)?;
959    let cwd = unsafe { (&*task.process.cwd.get()).clone() };
960    let bytes = cwd.as_bytes();
961    let needed = bytes.len() + 1;
962    if needed > buf_len as usize {
963        return Err(SyscallError::Range);
964    }
965    let out = UserSliceWrite::new(buf_ptr, needed)?;
966    let mut tmp = alloc::vec![0u8; needed];
967    tmp[..bytes.len()].copy_from_slice(bytes);
968    tmp[bytes.len()] = 0;
969    out.copy_from(&tmp);
970    Ok(needed as u64)
971}
972
973/// SYS_IOCTL (443): I/O control : stub.
974///
975/// Returns ENOTTY for all file descriptors that are not character devices.
976/// Terminal / PTY support will be added when a TTY driver is implemented.
977pub fn sys_ioctl(_fd: u32, _request: u64, _arg: u64) -> Result<u64, SyscallError> {
978    Err(SyscallError::NotATty)
979}
980
981/// SYS_UMASK (444): Set file creation mask; return the old mask.
982pub fn sys_umask(mask: u64) -> Result<u64, SyscallError> {
983    let task = current_task_clone().ok_or(SyscallError::PermissionDenied)?;
984    let old = task
985        .process
986        .umask
987        .swap(mask as u32 & 0o777, core::sync::atomic::Ordering::Relaxed);
988    Ok(old as u64)
989}
990
991/// SYS_UNLINK (445): Remove a file.
992pub fn sys_unlink(path_ptr: u64, path_len: u64) -> Result<u64, SyscallError> {
993    let abs = resolve_for_syscall(path_ptr, path_len, false, true, false)?;
994    unlink(&abs)?;
995    Ok(0)
996}
997
998/// SYS_RMDIR (446): Remove an empty directory.
999pub fn sys_rmdir(path_ptr: u64, path_len: u64) -> Result<u64, SyscallError> {
1000    let abs = resolve_for_syscall(path_ptr, path_len, false, true, false)?;
1001    unlink(&abs)?;
1002    Ok(0)
1003}
1004
1005/// SYS_MKDIR (447): Create a directory.
1006pub fn sys_mkdir(path_ptr: u64, path_len: u64, mode: u64) -> Result<u64, SyscallError> {
1007    let abs = resolve_for_syscall(path_ptr, path_len, false, true, false)?;
1008    let task = current_task_clone().ok_or(SyscallError::PermissionDenied)?;
1009    let umask = task
1010        .process
1011        .umask
1012        .load(core::sync::atomic::Ordering::Relaxed);
1013    let effective_mode = (mode as u32) & !umask;
1014    mkdir(&abs, effective_mode)?;
1015    Ok(0)
1016}
1017
1018/// SYS_RENAME (448): Rename a file or directory.
1019pub fn sys_rename(
1020    old_ptr: u64,
1021    old_len: u64,
1022    new_ptr: u64,
1023    new_len: u64,
1024) -> Result<u64, SyscallError> {
1025    let old_abs = resolve_for_syscall(old_ptr, old_len, true, true, false)?;
1026    let new_abs = resolve_for_syscall(new_ptr, new_len, false, true, false)?;
1027    rename(&old_abs, &new_abs)?;
1028    Ok(0)
1029}
1030
1031/// SYS_LINK (449): Create a hard link.
1032pub fn sys_link(
1033    old_ptr: u64,
1034    old_len: u64,
1035    new_ptr: u64,
1036    new_len: u64,
1037) -> Result<u64, SyscallError> {
1038    let old_abs = resolve_for_syscall(old_ptr, old_len, true, false, false)?;
1039    let new_abs = resolve_for_syscall(new_ptr, new_len, false, true, false)?;
1040    link(&old_abs, &new_abs)?;
1041    Ok(0)
1042}
1043
1044/// SYS_SYMLINK (450): Create a symbolic link.
1045pub fn sys_symlink(
1046    target_ptr: u64,
1047    target_len: u64,
1048    linkpath_ptr: u64,
1049    linkpath_len: u64,
1050) -> Result<u64, SyscallError> {
1051    let target = read_user_path(target_ptr, target_len)?;
1052    let link_abs = resolve_for_syscall(linkpath_ptr, linkpath_len, false, true, false)?;
1053    symlink(&target, &link_abs)?;
1054    Ok(0)
1055}
1056
1057/// SYS_READLINK (451): Read a symbolic link.
1058pub fn sys_readlink(
1059    path_ptr: u64,
1060    path_len: u64,
1061    buf_ptr: u64,
1062    buf_len: u64,
1063) -> Result<u64, SyscallError> {
1064    let abs = resolve_for_syscall(path_ptr, path_len, true, false, false)?;
1065    let target = readlink(&abs)?;
1066    let bytes = target.as_bytes();
1067    let n = bytes.len().min(buf_len as usize);
1068    let user = UserSliceWrite::new(buf_ptr, n)?;
1069    user.copy_from(&bytes[..n]);
1070    Ok(n as u64)
1071}
1072
1073/// SYS_CHMOD (452): Change file mode bits.
1074pub fn sys_chmod(path_ptr: u64, path_len: u64, mode: u64) -> Result<u64, SyscallError> {
1075    let abs = resolve_for_syscall(path_ptr, path_len, false, true, false)?;
1076    chmod(&abs, mode as u32)?;
1077    Ok(0)
1078}
1079
1080/// SYS_TRUNCATE (454): Truncate file to given length.
1081pub fn sys_truncate(path_ptr: u64, path_len: u64, length: u64) -> Result<u64, SyscallError> {
1082    let abs = resolve_for_syscall(path_ptr, path_len, false, true, false)?;
1083    truncate(&abs, length)?;
1084    Ok(0)
1085}
1086
1087/// SYS_FCHMOD (453): Change file mode bits on open fd.
1088pub fn sys_fchmod(fd: u32, mode: u64) -> Result<u64, SyscallError> {
1089    fchmod(fd, mode as u32)?;
1090    Ok(0)
1091}
1092
1093/// SYS_FTRUNCATE (455): Truncate open fd to given length.
1094pub fn sys_ftruncate(fd: u32, length: u64) -> Result<u64, SyscallError> {
1095    ftruncate(fd, length)?;
1096    Ok(0)
1097}
1098
1099/// SYS_PREAD (456): Read at offset without changing fd position.
1100pub fn sys_pread(fd: u32, buf_ptr: u64, buf_len: u64, offset: u64) -> Result<u64, SyscallError> {
1101    if buf_len == 0 {
1102        return Ok(0);
1103    }
1104    let task = current_task_clone().ok_or(SyscallError::PermissionDenied)?;
1105    let fd_table = unsafe { &*task.process.fd_table.get() };
1106    let file = fd_table.get(fd)?;
1107    let mut kbuf = [0u8; 4096];
1108    let mut total = 0usize;
1109    let mut off = offset;
1110    while total < buf_len as usize {
1111        let to_read = core::cmp::min(kbuf.len(), buf_len as usize - total);
1112        let n = file.pread(off, &mut kbuf[..to_read])?;
1113        if n == 0 {
1114            break;
1115        }
1116        let user = UserSliceWrite::new(
1117            buf_ptr
1118                .checked_add(total as u64)
1119                .ok_or(SyscallError::Fault)?,
1120            n,
1121        )?;
1122        user.copy_from(&kbuf[..n]);
1123        total += n;
1124        off += n as u64;
1125        if n < to_read {
1126            break;
1127        }
1128    }
1129    Ok(total as u64)
1130}
1131
1132/// SYS_PWRITE (457): Write at offset without changing fd position.
1133pub fn sys_pwrite(fd: u32, buf_ptr: u64, buf_len: u64, offset: u64) -> Result<u64, SyscallError> {
1134    if buf_len == 0 {
1135        return Ok(0);
1136    }
1137    let task = current_task_clone().ok_or(SyscallError::PermissionDenied)?;
1138    let fd_table = unsafe { &*task.process.fd_table.get() };
1139    let file = fd_table.get(fd)?;
1140    let mut kbuf = [0u8; 4096];
1141    let mut total = 0usize;
1142    let mut off = offset;
1143    while total < buf_len as usize {
1144        let to_write = core::cmp::min(kbuf.len(), buf_len as usize - total);
1145        let user = UserSliceRead::new(
1146            buf_ptr
1147                .checked_add(total as u64)
1148                .ok_or(SyscallError::Fault)?,
1149            to_write,
1150        )?;
1151        user.copy_to(&mut kbuf[..to_write]);
1152        let n = file.pwrite(off, &kbuf[..to_write])?;
1153        total += n;
1154        off += n as u64;
1155        if n < to_write {
1156            break;
1157        }
1158    }
1159    Ok(total as u64)
1160}
1161
1162// ============================================================================
1163// Global PipeScheme singleton
1164// ============================================================================
1165
1166static PIPE_SCHEME: SpinLock<Option<Arc<PipeScheme>>> = SpinLock::new(None);
1167
1168/// Returns pipe scheme.
1169fn get_pipe_scheme() -> Arc<PipeScheme> {
1170    let mut guard = PIPE_SCHEME.lock();
1171    if let Some(ref scheme) = *guard {
1172        return scheme.clone();
1173    }
1174    let scheme = Arc::new(PipeScheme::new());
1175    *guard = Some(scheme.clone());
1176    scheme
1177}
1178
1179/// Performs the build pci inventory text operation.
1180fn build_pci_inventory_text() -> String {
1181    #[cfg(target_arch = "x86_64")]
1182    {
1183        let devices = crate::hardware::pci_client::all_devices();
1184        let mut out = String::new();
1185        out.push_str("bus dev fn vendor device class subclass prog_if irq\n");
1186        for dev in devices.iter() {
1187            let _ = writeln!(
1188                out,
1189                "{:02x} {:02x} {} {:04x} {:04x} {:02x} {:02x} {:02x} {}",
1190                dev.address.bus,
1191                dev.address.device,
1192                dev.address.function,
1193                dev.vendor_id,
1194                dev.device_id,
1195                dev.class_code,
1196                dev.subclass,
1197                dev.prog_if,
1198                dev.interrupt_line
1199            );
1200        }
1201        return out;
1202    }
1203
1204    #[cfg(not(target_arch = "x86_64"))]
1205    {
1206        String::from("unsupported-arch\n")
1207    }
1208}
1209
1210// ============================================================================
1211// Initialization
1212// ============================================================================
1213
1214/// Initialize the VFS with default mounts.
1215pub fn init() {
1216    log::info!("[VFS] Initializing virtual file system");
1217
1218    //  Root filesystem (RamFS on "/") ========================================================================================================================
1219    // Must be mounted before any other scheme so that longest-prefix resolution
1220    // falls back to "/" for paths not covered by a more specific mount point.
1221    let rootfs = alloc::sync::Arc::new(RamfsScheme::new());
1222    if let Err(e) = mount::mount("/", rootfs.clone()) {
1223        log::error!("[VFS] Failed to mount /: {:?}", e);
1224    } else {
1225        // Populate the standard POSIX directory skeleton.
1226        for dir in &[
1227            "bin", "sbin", "etc", "tmp", "usr", "lib", "lib64", "home", "root", "run", "var",
1228            "mnt", "opt", "srv", "dev", "proc", "sys",
1229        ] {
1230            rootfs.ensure_dir(dir);
1231        }
1232        // Nested standard directories
1233        rootfs.ensure_dir("usr/bin");
1234        rootfs.ensure_dir("usr/sbin");
1235        rootfs.ensure_dir("usr/lib");
1236        rootfs.ensure_dir("var/log");
1237        rootfs.ensure_dir("var/tmp");
1238        rootfs.ensure_dir("run/lock");
1239        log::info!("[VFS] Mounted / (ramfs) with standard directory tree");
1240    }
1241
1242    // Initialize scheme router
1243    if let Err(e) = scheme_router::init_builtin_schemes() {
1244        log::error!("[VFS] Failed to init builtin schemes: {:?}", e);
1245    }
1246
1247    // Create and mount kernel scheme for /sys
1248    let kernel_scheme = KernelScheme::new();
1249
1250    // Register some basic kernel files
1251    static VERSION: &[u8] = b"Strat9-OS v0.1.0 (Bedrock)\n";
1252    kernel_scheme.register("version", VERSION.as_ptr(), VERSION.len());
1253
1254    static CMDLINE: &[u8] = b"quiet loglevel=debug\n";
1255    kernel_scheme.register("cmdline", CMDLINE.as_ptr(), CMDLINE.len());
1256
1257    let pci_inventory = build_pci_inventory_text().into_bytes().into_boxed_slice();
1258    let pci_inventory = Box::leak(pci_inventory);
1259    kernel_scheme.register("pci/inventory", pci_inventory.as_ptr(), pci_inventory.len());
1260
1261    let pci_count = pci_inventory
1262        .split(|b| *b == b'\n')
1263        .skip(1)
1264        .filter(|line| !line.is_empty())
1265        .count();
1266    let mut pci_count_str = String::new();
1267    let _ = writeln!(pci_count_str, "{}", pci_count);
1268    let pci_count = Box::leak(pci_count_str.into_bytes().into_boxed_slice());
1269    kernel_scheme.register("pci/count", pci_count.as_ptr(), pci_count.len());
1270
1271    // /sys/cpu/* : CPU information scheme (Plan9-style)
1272    {
1273        let host = crate::arch::x86_64::cpuid::host();
1274        // VFS initializes before SMP/percpu registration is complete.
1275        // Expose at least 1 CPU (BSP) instead of showing 0.
1276        let cpu_count = crate::arch::x86_64::percpu::get_cpu_count().max(1);
1277
1278        let count_s = Box::leak(
1279            alloc::format!("{}\n", cpu_count)
1280                .into_bytes()
1281                .into_boxed_slice(),
1282        );
1283        kernel_scheme.register("cpu/count", count_s.as_ptr(), count_s.len());
1284
1285        let vendor_s = Box::leak(
1286            alloc::format!("{}\n", host.vendor_string())
1287                .into_bytes()
1288                .into_boxed_slice(),
1289        );
1290        kernel_scheme.register("cpu/vendor", vendor_s.as_ptr(), vendor_s.len());
1291
1292        let model_s = Box::leak(
1293            alloc::format!("{}\n", host.model_name_str())
1294                .into_bytes()
1295                .into_boxed_slice(),
1296        );
1297        kernel_scheme.register("cpu/model", model_s.as_ptr(), model_s.len());
1298
1299        let features_s = Box::leak(
1300            alloc::format!(
1301                "{}\n",
1302                crate::arch::x86_64::cpuid::features_to_flags_string(host.features)
1303            )
1304            .into_bytes()
1305            .into_boxed_slice(),
1306        );
1307        kernel_scheme.register("cpu/features", features_s.as_ptr(), features_s.len());
1308
1309        let xcr0_s = Box::leak(
1310            alloc::format!("{:#x}\n", host.max_xcr0)
1311                .into_bytes()
1312                .into_boxed_slice(),
1313        );
1314        kernel_scheme.register("cpu/xcr0", xcr0_s.as_ptr(), xcr0_s.len());
1315
1316        let xsave_s = Box::leak(
1317            alloc::format!("{}\n", host.xsave_size)
1318                .into_bytes()
1319                .into_boxed_slice(),
1320        );
1321        kernel_scheme.register("cpu/xsave_size", xsave_s.as_ptr(), xsave_s.len());
1322    }
1323
1324    let kernel_scheme = Arc::new(kernel_scheme);
1325
1326    // Mount /sys
1327    if let Err(e) = mount::mount("/sys", kernel_scheme.clone()) {
1328        log::error!("[VFS] Failed to mount /sys: {:?}", e);
1329    } else {
1330        log::info!("[VFS] Mounted /sys (kernel scheme)");
1331    }
1332
1333    // Register and mount procfs
1334    let proc_scheme = Arc::new(ProcScheme::new());
1335    if let Err(e) = register_scheme("proc", proc_scheme.clone()) {
1336        log::error!("[VFS] Failed to register proc scheme: {:?}", e);
1337    } else {
1338        log::info!("[VFS] Registered proc scheme");
1339    }
1340
1341    if let Err(e) = mount::mount("/proc", proc_scheme) {
1342        log::error!("[VFS] Failed to mount /proc: {:?}", e);
1343    } else {
1344        log::info!("[VFS] Mounted /proc (procfs)");
1345    }
1346
1347    let ipc_scheme = Arc::new(ipcfs::IpcControlScheme::new());
1348    if let Err(e) = mount::mount("/ipc", ipc_scheme) {
1349        log::error!("[VFS] Failed to mount /ipc: {:?}", e);
1350    } else {
1351        log::info!("[VFS] Mounted /ipc (kernel ipc control scheme)");
1352    }
1353
1354    // Mount /dev : raw block-device scheme backed by AHCI.
1355    // The scheme is registered regardless of whether a disk is present:
1356    // device files appear dynamically when the hardware is available.
1357    let dev_scheme = Arc::new(BlkDevScheme::new());
1358    if let Err(e) = mount::mount("/dev", dev_scheme) {
1359        log::error!("[VFS] Failed to mount /dev: {:?}", e);
1360    } else {
1361        log::info!("[VFS] Mounted /dev (block-device scheme)");
1362    }
1363
1364    // Console scheme (/dev/console) : backs stdin/stdout/stderr for ELF processes
1365    let console = console_scheme::init_console_scheme();
1366    if let Err(e) = mount::mount("/dev/console", console) {
1367        log::error!("[VFS] Failed to mount /dev/console: {:?}", e);
1368    } else {
1369        log::info!("[VFS] Mounted /dev/console (serial + keyboard)");
1370    }
1371
1372    // PTY scheme (/dev/pts) : pseudo-terminals for interactive programs
1373    pty_scheme::init_pty_scheme();
1374    log::info!("[VFS] Mounted /dev/pts (PTY scheme)");
1375
1376    // Input scheme (/dev/input) : raw keyboard and mouse events for userspace Input Server
1377    let input_scheme = Arc::new(input_scheme::InputScheme::new());
1378    if let Err(e) = mount::mount("/dev/input", input_scheme) {
1379        log::error!("[VFS] Failed to mount /dev/input: {:?}", e);
1380    } else {
1381        log::info!("[VFS] Mounted /dev/input (raw keyboard + mouse events)");
1382    }
1383
1384    log::info!("[VFS] VFS ready");
1385}