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