From 53e68d6739aa172bdc040e37c338013741365fd5 Mon Sep 17 00:00:00 2001 From: n4mlz Date: Tue, 27 Jan 2026 18:52:56 +0900 Subject: [PATCH 01/25] feat: enhance process and TTY management with job control features and syscall support --- kernel/src/container/DESIGN.md | 2 + kernel/src/container/runtime.rs | 5 + kernel/src/device/tty/DESIGN.md | 6 +- kernel/src/device/tty/mod.rs | 69 +++++++++- kernel/src/fs/fd.rs | 44 ++++++ kernel/src/process/DESIGN.md | 4 +- kernel/src/process/fs.rs | 15 ++ kernel/src/process/mod.rs | 75 ++++++++++ kernel/src/syscall/DESIGN.md | 7 +- kernel/src/syscall/linux.rs | 233 +++++++++++++++++++++++++++++++- 10 files changed, 447 insertions(+), 13 deletions(-) diff --git a/kernel/src/container/DESIGN.md b/kernel/src/container/DESIGN.md index aa0f128..a36d913 100644 --- a/kernel/src/container/DESIGN.md +++ b/kernel/src/container/DESIGN.md @@ -34,6 +34,8 @@ `cwd` before loading the ELF image. - Container init processes are created with Linux ABI and are tied to the container VFS at process creation time, ensuring path resolution never touches the host VFS. +- Container init processes are assigned the global controlling TTY so shells can enable job control + without requiring a separate `cttyhack` step. - Starting a container transitions status from `Created` to `Running` and stores the init PID. ## Future Work diff --git a/kernel/src/container/runtime.rs b/kernel/src/container/runtime.rs index cb17021..7682cfc 100644 --- a/kernel/src/container/runtime.rs +++ b/kernel/src/container/runtime.rs @@ -5,6 +5,7 @@ use crate::arch::api::ArchPageTableAccess; use crate::container::{CONTAINER_TABLE, Container, ContainerError, ContainerStatus}; use crate::fs::Path; use crate::loader::linux::{self, LinuxLoadError}; +use crate::println; use crate::process::{PROCESS_TABLE, ProcessError, ProcessId}; use crate::thread::{SCHEDULER, SpawnError}; @@ -85,6 +86,8 @@ pub fn start_container(container: Arc) -> Result>, output: SpinLock>, @@ -212,6 +217,24 @@ impl TtyDevice { fn set_pgrp(&self, pgrp: u32) { self.state.lock().pgrp = pgrp; } + + fn current_process(&self) -> Option { + let pid = SCHEDULER.current_process_id()?; + PROCESS_TABLE.process_handle(pid).ok() + } + + fn current_process_pgrp(&self) -> Option { + let proc = self.current_process()?; + Some(proc.pgrp_id() as u32) + } + + fn require_controlling_tty(&self) -> Result { + let proc = self.current_process().ok_or(ControlError::Invalid)?; + if !proc.has_controlling_tty() { + return Err(ControlError::Invalid); + } + Ok(proc) + } } impl Default for TtyDevice { @@ -261,7 +284,12 @@ impl CharDevice for TtyDevice { impl ControlOps for TtyDevice { fn control(&self, request: &ControlRequest<'_>) -> Result { - match request.command { + println!( + "[tty ioctl] pid={} cmd=0x{:x}", + SCHEDULER.current_process_id().unwrap_or(0), + request.command + ); + let result = match request.command { IOCTL_TCGETS => { let termios = self.termios_snapshot(); request.write_struct(&termios)?; @@ -282,21 +310,56 @@ impl ControlOps for TtyDevice { self.set_winsize(winsize); Ok(0) } + IOCTL_TIOCSCTTY => { + let proc = self.current_process().ok_or(ControlError::Invalid)?; + let pid = proc.id(); + if proc.session_id() != pid { + return Err(ControlError::Invalid); + } + if proc.has_controlling_tty() { + return Err(ControlError::Invalid); + } + proc.set_controlling_tty(ControllingTty::Global); + self.set_pgrp(proc.pgrp_id() as u32); + Ok(0) + } IOCTL_TIOCGPGRP => { - let pgrp = self.pgrp() as i32; + let _ = self.require_controlling_tty()?; + let mut pgrp = self.pgrp(); + if pgrp == 0 { + if let Some(current) = self.current_process_pgrp() { + pgrp = current; + self.set_pgrp(pgrp); + } + } + let pgrp = pgrp as i32; request.write_struct(&pgrp)?; Ok(0) } IOCTL_TIOCSPGRP => { + let _ = self.require_controlling_tty()?; let pgrp = request.read_struct::()?; if pgrp < 0 { return Err(ControlError::Invalid); } + if pgrp == 0 { + return Err(ControlError::Invalid); + } self.set_pgrp(pgrp as u32); Ok(0) } _ => Err(ControlError::Unsupported), - } + }; + println!( + "[tty ioctl] pid={} cmd=0x{:x} ret={}", + SCHEDULER.current_process_id().unwrap_or(0), + request.command, + match &result { + Ok(val) => alloc::format!("ok({})", val), + Err(err) => alloc::format!("err({:?})", err), + } + ); + result } } diff --git a/kernel/src/fs/fd.rs b/kernel/src/fs/fd.rs index 92a3398..2e6f21c 100644 --- a/kernel/src/fs/fd.rs +++ b/kernel/src/fs/fd.rs @@ -80,6 +80,33 @@ impl FdTable { guard.clear(fd) } + pub fn dup_min(&self, src: Fd, min: Fd, close_on_exec: bool) -> Result { + let mut guard = self.inner.lock(); + let entry = guard.get(src)?.clone(); + let mut entry = entry; + entry.set_close_on_exec(close_on_exec); + let fd = guard.allocate_fd_from(min); + guard.set(fd, entry)?; + Ok(fd) + } + + pub fn get_fd_flags(&self, fd: Fd) -> Result { + let guard = self.inner.lock(); + let entry = guard.get(fd)?; + Ok(if entry.close_on_exec() { 1 } else { 0 }) + } + + pub fn set_fd_flags(&self, fd: Fd, flags: u32) -> Result<(), VfsError> { + let mut guard = self.inner.lock(); + let entry = guard + .slots + .get_mut(fd as usize) + .and_then(|slot| slot.as_mut()) + .ok_or(VfsError::NotFound)?; + entry.set_close_on_exec(flags & 1 != 0); + Ok(()) + } + pub fn entry(&self, fd: Fd) -> Result { let guard = self.inner.lock(); guard.get(fd).cloned() @@ -112,6 +139,23 @@ impl FdTableInner { fd as Fd } + fn allocate_fd_from(&mut self, min: Fd) -> Fd { + if let Some((index, _)) = self + .slots + .iter() + .enumerate() + .skip(min as usize) + .find(|(_, entry)| entry.is_none()) + { + return index as Fd; + } + let fd = min as usize; + if fd >= self.slots.len() { + self.slots.resize(fd + 1, None); + } + fd as Fd + } + fn set(&mut self, fd: Fd, entry: FdEntry) -> Result<(), VfsError> { let index = fd as usize; if index >= self.slots.len() { diff --git a/kernel/src/process/DESIGN.md b/kernel/src/process/DESIGN.md index a9d69fa..321e7fe 100644 --- a/kernel/src/process/DESIGN.md +++ b/kernel/src/process/DESIGN.md @@ -14,12 +14,14 @@ ### Process - Stored as `Arc` so threads can hold a direct reference to their owning process without touching the global table. -- Stores `id`, `name`, `address_space`, `state`, `threads`, `fs`, `parent`, `exit_code`, `reaped`, `brk`, `abi`, and a `ProcessDomain`. +- Stores `id`, `name`, `address_space`, `state`, `threads`, `fs`, `parent`, `exit_code`, `reaped`, `brk`, `abi`, `session_id`, `pgrp_id`, `controlling_tty`, and a `ProcessDomain`. - `address_space` holds an `ArchThread::AddressSpace` (currently an `Arc` handle) so processes share explicit address-space state. - `ProcessState` now spans `Created`, `Ready`, `Running`, `Waiting`, `Terminated`; transitions are simple and primarily driven by thread attach/detach and scheduler ticks. - `ProcessDomain` decides both the ABI and the VFS binding at creation time. - `brk` tracks the user-mode heap break (base/current), seeded by Linux ELF loading and advanced by the `brk` syscall. - `parent`/`exit_code`/`reaped` provide minimal wait4 support: fork assigns a parent, exit writes a code, and wait4 marks the child as reaped. +- `session_id` and `pgrp_id` carry minimal job-control metadata; both default to the process ID and are inherited across fork. +- `controlling_tty` tracks whether the process is attached to the global TTY; it is cleared on `setsid` and inherited across fork. - If a process is created for a container, it retains the container handle and filesystem operations route through the container VFS. ## Initialization and Invariants diff --git a/kernel/src/process/fs.rs b/kernel/src/process/fs.rs index 2fe6019..badd81e 100644 --- a/kernel/src/process/fs.rs +++ b/kernel/src/process/fs.rs @@ -76,6 +76,21 @@ pub fn close_fd(pid: ProcessId, fd: Fd) -> Result<(), VfsError> { process.fd_table().close(fd) } +pub fn dup_fd_min(pid: ProcessId, fd: Fd, min: Fd, close_on_exec: bool) -> Result { + let process = process_handle(pid)?; + process.fd_table().dup_min(fd, min, close_on_exec) +} + +pub fn get_fd_flags(pid: ProcessId, fd: Fd) -> Result { + let process = process_handle(pid)?; + process.fd_table().get_fd_flags(fd) +} + +pub fn set_fd_flags(pid: ProcessId, fd: Fd, flags: u32) -> Result<(), VfsError> { + let process = process_handle(pid)?; + process.fd_table().set_fd_flags(fd, flags) +} + pub fn control_fd( pid: ProcessId, fd: Fd, diff --git a/kernel/src/process/mod.rs b/kernel/src/process/mod.rs index e2339ca..8c9c293 100644 --- a/kernel/src/process/mod.rs +++ b/kernel/src/process/mod.rs @@ -291,6 +291,9 @@ pub struct Process { brk: SpinLock, abi: Abi, domain: ProcessDomain, + session_id: SpinLock, + pgrp_id: SpinLock, + controlling_tty: SpinLock>, } /// Process domain determines the ABI and VFS visibility contract. @@ -352,6 +355,9 @@ impl Process { brk: SpinLock::new(BrkState::empty()), abi, domain: ProcessDomain::Host, + session_id: SpinLock::new(id), + pgrp_id: SpinLock::new(id), + controlling_tty: SpinLock::new(None), } } @@ -376,6 +382,9 @@ impl Process { brk: SpinLock::new(BrkState::empty()), abi, domain, + session_id: SpinLock::new(id), + pgrp_id: SpinLock::new(id), + controlling_tty: SpinLock::new(None), } } @@ -508,6 +517,42 @@ impl Process { guard.current = base; } + pub fn session_id(&self) -> ProcessId { + *self.session_id.lock() + } + + pub fn set_session_id(&self, session: ProcessId) { + let mut guard = self.session_id.lock(); + *guard = session; + } + + pub fn pgrp_id(&self) -> ProcessId { + *self.pgrp_id.lock() + } + + pub fn set_pgrp_id(&self, pgrp: ProcessId) { + let mut guard = self.pgrp_id.lock(); + *guard = pgrp; + } + + pub fn controlling_tty(&self) -> Option { + *self.controlling_tty.lock() + } + + pub fn has_controlling_tty(&self) -> bool { + self.controlling_tty.lock().is_some() + } + + pub fn set_controlling_tty(&self, tty: ControllingTty) { + let mut guard = self.controlling_tty.lock(); + *guard = Some(tty); + } + + pub fn clear_controlling_tty(&self) { + let mut guard = self.controlling_tty.lock(); + *guard = None; + } + fn set_state_if_alive(&self, state: ProcessState) { if matches!(self.state(), ProcessState::Terminated) { return; @@ -546,6 +591,11 @@ enum ProcessKind { User, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ControllingTty { + Global, +} + #[cfg(test)] mod tests { use alloc::string::String; @@ -627,4 +677,29 @@ mod tests { let abi = PROCESS_TABLE.abi(pid).expect("abi present"); assert_eq!(abi, Abi::Linux); } + + #[kernel_test_case] + fn process_defaults_session_and_pgrp_to_pid() { + println!("[test] process_defaults_session_and_pgrp_to_pid"); + + let _ = PROCESS_TABLE.init_kernel(); + let pid = PROCESS_TABLE + .create_user_process("session-proc", ProcessDomain::Host) + .expect("create user process"); + let proc = PROCESS_TABLE.process_handle(pid).expect("process handle"); + assert_eq!(proc.session_id(), pid); + assert_eq!(proc.pgrp_id(), pid); + } + + #[kernel_test_case] + fn process_default_has_no_controlling_tty() { + println!("[test] process_default_has_no_controlling_tty"); + + let _ = PROCESS_TABLE.init_kernel(); + let pid = PROCESS_TABLE + .create_user_process("ctty-proc", ProcessDomain::Host) + .expect("create user process"); + let proc = PROCESS_TABLE.process_handle(pid).expect("process handle"); + assert!(!proc.has_controlling_tty()); + } } diff --git a/kernel/src/syscall/DESIGN.md b/kernel/src/syscall/DESIGN.md index c0666d8..1574704 100644 --- a/kernel/src/syscall/DESIGN.md +++ b/kernel/src/syscall/DESIGN.md @@ -20,8 +20,11 @@ userland separation exists. - Linux dispatch implements a minimal set of process/syscall plumbing needed by static busybox: `read`, `write`, `open`, `close`, `writev`, `stat`, `brk`, `fork`, `execve`, `wait4`, `arch_prctl`, - `ioctl` (routed through `ControlOps`), plus stubbed signal calls. Unsupported numbers map to - `ENOSYS`, while unsupported ioctls map to `ENOTTY`. + `ioctl` (routed through `ControlOps`), `fcntl` (dup + FD_CLOEXEC), and basic process/session + metadata (`getppid`, `getpgrp`, `getpgid`, `setpgid`, `getsid`, `setsid`), plus stubbed signal + calls. Unsupported numbers map to `ENOSYS`, while unsupported ioctls map to `ENOTTY`. +- `/dev/tty` open assigns the global controlling TTY when the caller is a session leader and no + controlling TTY is present yet; this is a minimal bridge until full tty/session semantics land. ## Extension Points / TODO - Add architecture-specific fast paths (`syscall`/`sysret`) once MSR programming is available. diff --git a/kernel/src/syscall/linux.rs b/kernel/src/syscall/linux.rs index c56b673..8372e70 100644 --- a/kernel/src/syscall/linux.rs +++ b/kernel/src/syscall/linux.rs @@ -14,7 +14,7 @@ use crate::mem::paging::{FrameAllocator, MapError, PageTableOps, PhysMapper}; use crate::mem::user::{UserMemoryAccess, copy_from_user, copy_to_user, with_user_slice}; use crate::println; use crate::process::fs as proc_fs; -use crate::process::{PROCESS_TABLE, ProcessId}; +use crate::process::{ControllingTty, PROCESS_TABLE, ProcessId}; use crate::thread::SCHEDULER; use crate::trap::CurrentTrapFrame; @@ -44,6 +44,11 @@ pub enum LinuxSyscall { Ioctl = 16, Writev = 20, GetPid = 39, + Fcntl = 72, + SetPgid = 109, + GetPpid = 110, + GetPgrp = 111, + SetSid = 112, Fork = 57, Execve = 59, Exit = 60, @@ -52,6 +57,8 @@ pub enum LinuxSyscall { GetGid = 104, SetUid = 105, SetGid = 106, + GetPgid = 121, + GetSid = 124, ArchPrctl = 158, SetTidAddress = 218, } @@ -72,6 +79,11 @@ impl LinuxSyscall { 16 => Some(Self::Ioctl), 20 => Some(Self::Writev), 39 => Some(Self::GetPid), + 72 => Some(Self::Fcntl), + 109 => Some(Self::SetPgid), + 110 => Some(Self::GetPpid), + 111 => Some(Self::GetPgrp), + 112 => Some(Self::SetSid), 57 => Some(Self::Fork), 59 => Some(Self::Execve), 60 => Some(Self::Exit), @@ -80,6 +92,8 @@ impl LinuxSyscall { 104 => Some(Self::GetGid), 105 => Some(Self::SetUid), 106 => Some(Self::SetGid), + 121 => Some(Self::GetPgid), + 124 => Some(Self::GetSid), 158 => Some(Self::ArchPrctl), 218 => Some(Self::SetTidAddress), _ => None, @@ -106,6 +120,11 @@ pub fn dispatch( Some(LinuxSyscall::Mmap) => DispatchResult::Completed(handle_mmap(invocation)), Some(LinuxSyscall::Munmap) => DispatchResult::Completed(handle_munmap(invocation)), Some(LinuxSyscall::Brk) => DispatchResult::Completed(handle_brk(invocation)), + Some(LinuxSyscall::Fcntl) => DispatchResult::Completed(handle_fcntl(invocation)), + Some(LinuxSyscall::SetPgid) => DispatchResult::Completed(handle_setpgid(invocation)), + Some(LinuxSyscall::GetPpid) => DispatchResult::Completed(handle_getppid(invocation)), + Some(LinuxSyscall::GetPgrp) => DispatchResult::Completed(handle_getpgrp(invocation)), + Some(LinuxSyscall::SetSid) => DispatchResult::Completed(handle_setsid(invocation)), Some(LinuxSyscall::Fork) => handle_fork(invocation, frame), Some(LinuxSyscall::Execve) => handle_execve(invocation, frame), Some(LinuxSyscall::Wait4) => DispatchResult::Completed(handle_wait4(invocation)), @@ -121,6 +140,8 @@ pub fn dispatch( Some(LinuxSyscall::SetUid) => DispatchResult::Completed(Ok(0)), Some(LinuxSyscall::SetGid) => DispatchResult::Completed(Ok(0)), Some(LinuxSyscall::GetPid) => DispatchResult::Completed(handle_getpid(invocation)), + Some(LinuxSyscall::GetPgid) => DispatchResult::Completed(handle_getpgid(invocation)), + Some(LinuxSyscall::GetSid) => DispatchResult::Completed(handle_getsid(invocation)), Some(LinuxSyscall::Exit) => handle_exit(invocation), None => DispatchResult::Completed(Err(SysError::NotImplemented)), } @@ -195,13 +216,32 @@ fn handle_open(invocation: &SyscallInvocation) -> SysResult { })?; let create = (flags & LinuxOpenFlags::Creat as u64) != 0; - let fd = if create { + let result = if create { proc_fs::open_path_with_create(pid, &path, flags) } else { proc_fs::open_path(pid, &path, flags) + }; + + if path == "/dev/tty" || path == "/dev/console" { + println!( + "[open] pid={} path={} flags=0x{:x} => {}", + pid, + path, + flags, + match &result { + Ok(fd) => alloc::format!("fd={}", fd), + Err(_) => "err".into(), + } + ); } - .map_err(|_| SysError::InvalidArgument)?; + let fd = result.map_err(|_| SysError::InvalidArgument)?; + if path == "/dev/tty" { + if process.session_id() == pid && !process.has_controlling_tty() { + process.set_controlling_tty(ControllingTty::Global); + println!("[ctty] pid={} acquired=tty", pid); + } + } Ok(fd as u64) } @@ -241,6 +281,57 @@ fn handle_writev(invocation: &SyscallInvocation) -> SysResult { Ok(total) } +fn handle_fcntl(invocation: &SyscallInvocation) -> SysResult { + const F_DUPFD: u64 = 0; + const F_GETFD: u64 = 1; + const F_SETFD: u64 = 2; + const F_DUPFD_CLOEXEC: u64 = 1030; + + let pid = current_pid()?; + let fd = invocation.arg(0).ok_or(SysError::InvalidArgument)? as u32; + let cmd = invocation.arg(1).ok_or(SysError::InvalidArgument)?; + let arg = invocation.arg(2).unwrap_or(0); + + let result = match cmd { + F_DUPFD => { + let min = u32::try_from(arg).map_err(|_| SysError::InvalidArgument)?; + proc_fs::dup_fd_min(pid, fd, min, false) + .map(|val| val as u64) + .map_err(|_| SysError::InvalidArgument) + } + F_DUPFD_CLOEXEC => { + let min = u32::try_from(arg).map_err(|_| SysError::InvalidArgument)?; + proc_fs::dup_fd_min(pid, fd, min, true) + .map(|val| val as u64) + .map_err(|_| SysError::InvalidArgument) + } + F_GETFD => proc_fs::get_fd_flags(pid, fd) + .map(|val| val as u64) + .map_err(|_| SysError::InvalidArgument), + F_SETFD => { + let flags = u32::try_from(arg).map_err(|_| SysError::InvalidArgument)?; + proc_fs::set_fd_flags(pid, fd, flags) + .map(|_| 0) + .map_err(|_| SysError::InvalidArgument) + } + _ => Err(SysError::NotImplemented), + }; + + println!( + "[fcntl] pid={} fd={} cmd={} arg={} => {}", + pid, + fd, + cmd, + arg, + match &result { + Ok(val) => alloc::format!("ok({})", val), + Err(err) => alloc::format!("err({:?})", err), + } + ); + + result +} + fn handle_ioctl(invocation: &SyscallInvocation) -> SysResult { let pid = current_pid()?; let fd = invocation.arg(0).ok_or(SysError::InvalidArgument)?; @@ -250,11 +341,24 @@ fn handle_ioctl(invocation: &SyscallInvocation) -> SysResult { let process = PROCESS_TABLE .process_handle(pid) .map_err(|_| SysError::InvalidArgument)?; - process.address_space().with_page_table(|table, _| { + let result = process.address_space().with_page_table(|table, _| { let user = UserMemoryAccess::new(table); let request = crate::util::stream::ControlRequest::new(cmd, arg, &user); proc_fs::control_fd(pid, fd as u32, &request).map_err(map_control_error) - }) + }); + + println!( + "[ioctl] pid={} fd={} cmd=0x{:x} => {}", + pid, + fd, + cmd, + match &result { + Ok(val) => alloc::format!("ok({})", val), + Err(err) => alloc::format!("err({:?})", err), + } + ); + + result } fn handle_getpid(_invocation: &SyscallInvocation) -> SysResult { @@ -264,6 +368,91 @@ fn handle_getpid(_invocation: &SyscallInvocation) -> SysResult { Ok(pid) } +fn handle_getppid(_invocation: &SyscallInvocation) -> SysResult { + let pid = current_pid()?; + let process = PROCESS_TABLE + .process_handle(pid) + .map_err(|_| SysError::InvalidArgument)?; + Ok(process.parent().unwrap_or(0)) +} + +fn handle_getpgrp(_invocation: &SyscallInvocation) -> SysResult { + let pid = current_pid()?; + let process = PROCESS_TABLE + .process_handle(pid) + .map_err(|_| SysError::InvalidArgument)?; + Ok(process.pgrp_id()) +} + +fn handle_setsid(_invocation: &SyscallInvocation) -> SysResult { + let pid = current_pid()?; + let process = PROCESS_TABLE + .process_handle(pid) + .map_err(|_| SysError::InvalidArgument)?; + println!( + "[setsid] pid={} old_sid={} old_pgrp={}", + pid, + process.session_id(), + process.pgrp_id() + ); + if process.session_id() == pid { + return Err(SysError::InvalidArgument); + } + process.set_session_id(pid); + process.set_pgrp_id(pid); + process.clear_controlling_tty(); + Ok(pid) +} + +fn handle_getpgid(invocation: &SyscallInvocation) -> SysResult { + let pid = invocation.arg(0).unwrap_or(0); + let target = if pid == 0 { current_pid()? } else { pid }; + let process = PROCESS_TABLE + .process_handle(target) + .map_err(|_| SysError::NotFound)?; + Ok(process.pgrp_id()) +} + +fn handle_getsid(invocation: &SyscallInvocation) -> SysResult { + let pid = invocation.arg(0).unwrap_or(0); + let target = if pid == 0 { current_pid()? } else { pid }; + let process = PROCESS_TABLE + .process_handle(target) + .map_err(|_| SysError::NotFound)?; + Ok(process.session_id()) +} + +fn handle_setpgid(invocation: &SyscallInvocation) -> SysResult { + let pid_arg = invocation.arg(0).unwrap_or(0); + let pgid_arg = invocation.arg(1).unwrap_or(0); + + let caller_pid = current_pid()?; + let target_pid = if pid_arg == 0 { caller_pid } else { pid_arg }; + let target_pgid = if pgid_arg == 0 { target_pid } else { pgid_arg }; + + let caller = PROCESS_TABLE + .process_handle(caller_pid) + .map_err(|_| SysError::InvalidArgument)?; + let target = PROCESS_TABLE + .process_handle(target_pid) + .map_err(|_| SysError::NotFound)?; + + if target_pid != caller_pid && !PROCESS_TABLE.is_child(caller_pid, target_pid) { + return Err(SysError::InvalidArgument); + } + + if target.session_id() != caller.session_id() { + return Err(SysError::InvalidArgument); + } + + println!( + "[setpgid] caller={} target={} new_pgrp={}", + caller_pid, target_pid, target_pgid + ); + target.set_pgrp_id(target_pgid); + Ok(0) +} + fn handle_exit(invocation: &SyscallInvocation) -> DispatchResult { let code = invocation.arg(0).unwrap_or(0) as i32; if let Ok(pid) = current_pid() @@ -436,6 +625,11 @@ fn handle_fork( if let Ok(child_proc) = PROCESS_TABLE.process_handle(child_pid) { child_proc.set_brk_state(parent_proc.brk_state()); child_proc.set_parent(pid); + child_proc.set_session_id(parent_proc.session_id()); + child_proc.set_pgrp_id(parent_proc.pgrp_id()); + if let Some(tty) = parent_proc.controlling_tty() { + child_proc.set_controlling_tty(tty); + } } let stack_size = parent_stack.size; @@ -549,6 +743,13 @@ fn handle_execve( if let Ok(process) = PROCESS_TABLE.process_handle(pid) { process.set_brk_base(program.heap_base); + println!( + "[proc] pid={} sid={} pgrp={} ctty={}", + pid, + process.session_id(), + process.pgrp_id(), + process.has_controlling_tty() + ); } frame.rip = program.entry.as_raw() as u64; @@ -1139,6 +1340,28 @@ mod tests { } } + #[kernel_test_case] + fn fcntl_dupfd_sets_cloexec() { + println!("[test] fcntl_dupfd_sets_cloexec"); + + let _ = PROCESS_TABLE.init_kernel(); + SCHEDULER.init().expect("scheduler init"); + + let dup_inv = SyscallInvocation::new(LinuxSyscall::Fcntl as u64, [0, 1030, 10, 0, 0, 0]); + let new_fd = match dispatch(&dup_inv, None) { + DispatchResult::Completed(Ok(fd)) => fd as u32, + other => panic!("unexpected dispatch result: {:?}", other), + }; + assert_eq!(new_fd, 10); + + let get_inv = + SyscallInvocation::new(LinuxSyscall::Fcntl as u64, [new_fd as u64, 1, 0, 0, 0, 0]); + match dispatch(&get_inv, None) { + DispatchResult::Completed(Ok(flags)) => assert_eq!(flags, 1), + other => panic!("unexpected dispatch result: {:?}", other), + } + } + #[kernel_test_case] fn stat_reports_file_size() { println!("[test] stat_reports_file_size"); From 26bb43e3f4a7e5823f55fe5859905c5fc74884c8 Mon Sep 17 00:00:00 2001 From: n4mlz Date: Wed, 28 Jan 2026 10:54:38 +0900 Subject: [PATCH 02/25] feat: implement container start with wait for exit and TTY attachment --- kernel/src/kernel_proc/DESIGN.md | 3 + kernel/src/kernel_proc/oci_runtime.rs | 20 ++++- kernel/src/kernel_proc/shell.rs | 103 +++++++++++++++++++++++++- 3 files changed, 120 insertions(+), 6 deletions(-) diff --git a/kernel/src/kernel_proc/DESIGN.md b/kernel/src/kernel_proc/DESIGN.md index cf83ffd..4f6932d 100644 --- a/kernel/src/kernel_proc/DESIGN.md +++ b/kernel/src/kernel_proc/DESIGN.md @@ -9,6 +9,9 @@ `linux-box run `, and host container management via `oci-runtime create/start/state`. - `oci_runtime` houses the host ABI bridge for OCI-style commands to keep shell parsing focused on REPL concerns. +- `oci-runtime start` blocks the shell until the container init process exits, and the container + I/O is attached to the global TTY for interactive sessions (currently unconditional; intended to + respect `process.terminal=true` in OCI config in the future). - Tokenisation is whitespace-based; quoted strings are not supported. - Errors bubble up from the filesystem (`VfsError`), process table (`ProcessError`), loader (`LinuxLoadError`), and thread spawning (`SpawnError`) without wrapping in an extra linux-box-specific error layer. - Runs as a kernel thread associated with a kernel process, reusing the process CWD and FD table for all commands. diff --git a/kernel/src/kernel_proc/oci_runtime.rs b/kernel/src/kernel_proc/oci_runtime.rs index 5412446..97fe790 100644 --- a/kernel/src/kernel_proc/oci_runtime.rs +++ b/kernel/src/kernel_proc/oci_runtime.rs @@ -4,8 +4,8 @@ use alloc::string::{String, ToString}; use crate::container::runtime::{ContainerStartError, start_container_by_id}; use crate::container::{CONTAINER_TABLE, ContainerError}; use crate::fs::Path; -use crate::process::ProcessId; use crate::process::fs as proc_fs; +use crate::process::{PROCESS_TABLE, ProcessId}; #[derive(Debug)] pub enum OciRuntimeError { @@ -23,9 +23,21 @@ pub fn create_container(pid: ProcessId, id: &str, bundle: &str) -> Result Result { - let pid = start_container_by_id(id).map_err(OciRuntimeError::Start)?; - Ok(format!("container {id} started (pid {pid})")) +pub fn start_container(id: &str) -> Result { + start_container_by_id(id).map_err(OciRuntimeError::Start) +} + +pub fn wait_for_exit(pid: ProcessId) { + while PROCESS_TABLE + .thread_count(pid) + .map(|count| count > 0) + .unwrap_or(false) + { + #[cfg(target_arch = "x86_64")] + crate::arch::x86_64::halt(); + #[cfg(not(target_arch = "x86_64"))] + core::hint::spin_loop(); + } } pub fn state_container(id: &str) -> Result { diff --git a/kernel/src/kernel_proc/shell.rs b/kernel/src/kernel_proc/shell.rs index fbaf3b1..cf0ad43 100644 --- a/kernel/src/kernel_proc/shell.rs +++ b/kernel/src/kernel_proc/shell.rs @@ -304,8 +304,13 @@ fn cmd_oci_runtime(pid: ProcessId, args: &str) -> Result, ShellEr Ok(Some(output)) } (Some("start"), Some(id), None, None) => { - let output = oci_runtime::start_container(id).map_err(ShellError::OciRuntime)?; - Ok(Some(output)) + let pid = oci_runtime::start_container(id).map_err(ShellError::OciRuntime)?; + // NOTE: This wait+TTY-attach mode is always on today, but should be conditional on + // `process.terminal = true` from the OCI bundle config in the future. + // NOTE: Waiting by host PID is a pragmatic interim choice; container PID spaces will + // eventually diverge from the host, so the wait logic should move to container state. + oci_runtime::wait_for_exit(pid); + Ok(None) } (Some("state"), Some(id), None, None) => { let output = oci_runtime::state_container(id).map_err(ShellError::OciRuntime)?; @@ -370,10 +375,21 @@ fn read_line(console: &impl CharDevice, buf: &mut [u8]) -> usize { #[cfg(test)] mod tests { use super::*; + use crate::container::CONTAINER_TABLE; + use crate::device::tty::global_tty; + use crate::fs::DirNode; use crate::fs::force_replace_root; use crate::fs::memfs::MemDirectory; + use crate::interrupt::{INTERRUPTS, SYSTEM_TIMER, TimerTicks}; use crate::process::PROCESS_TABLE; use crate::test::kernel_test_case; + use crate::thread::{SCHEDULER, SchedulerError}; + + /// ELF fixture generated by `xtask` (via `xtask-assets`) under `target/xtask-assets`. + const LINUX_SYSCALL_ELF: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../target/xtask-assets/linux-syscall.elf" + )); #[kernel_test_case] fn parse_commands() { @@ -500,6 +516,89 @@ mod tests { assert_eq!(sym_up_content, "hello from tar"); } + #[kernel_test_case] + fn oci_runtime_start_waits_for_exit_and_attaches_tty() { + println!("[test] oci_runtime_start_waits_for_exit_and_attaches_tty"); + + let _ = PROCESS_TABLE.init_kernel(); + SCHEDULER.init().expect("scheduler init"); + let started = match SCHEDULER.start() { + Ok(()) => true, + Err(SchedulerError::AlreadyStarted) => false, + Err(err) => panic!("scheduler start failed: {:?}", err), + }; + + let root = MemDirectory::new(); + force_replace_root(root.clone()); + CONTAINER_TABLE.clear_for_tests(); + + let bundle_dir = root.create_dir("bundle").expect("create bundle dir"); + let bundle_dir_view = bundle_dir.as_dir().expect("bundle is dir"); + let rootfs_dir = bundle_dir_view + .create_dir("rootfs") + .expect("create rootfs dir"); + let rootfs_dir_view = rootfs_dir.as_dir().expect("rootfs is dir"); + let bin = rootfs_dir_view.create_file("demo").expect("create demo"); + let handle = bin.open(crate::fs::OpenOptions::new(0)).expect("open demo"); + let _ = handle.write(LINUX_SYSCALL_ELF).expect("write demo"); + let msg = rootfs_dir_view + .create_file("msg.txt") + .expect("create msg.txt"); + let handle = msg + .open(crate::fs::OpenOptions::new(0)) + .expect("open msg.txt"); + let _ = handle.write(b"FILE\n").expect("write msg.txt"); + + let config = bundle_dir_view + .create_file("config.json") + .expect("create config"); + let handle = config + .open(crate::fs::OpenOptions::new(0)) + .expect("open config"); + handle + .write( + br#"{"ociVersion":"1.0.2","root":{"path":"rootfs"},"process":{"cwd":"/","args":["/demo"],"user":{"uid":0,"gid":0},"terminal":true}}"#, + ) + .expect("write config"); + + let shell_pid = PROCESS_TABLE + .create_kernel_process("shell-oci-test") + .expect("create process"); + + let tty = global_tty(); + tty.clear_output(); + tty.push_input(b"IN\n"); + + run_command(shell_pid, "oci-runtime create demo /bundle").expect("create"); + run_command(shell_pid, "oci-runtime start demo").expect("start"); + + let output = tty.drain_output(); + let expected = b"IN\nFILE\n"; + assert!( + output + .windows(expected.len()) + .any(|chunk| chunk == expected), + "output missing container IO: {:?}", + output + ); + + let container = CONTAINER_TABLE.get("demo").expect("container"); + let pid = container.state().pid.expect("pid"); + assert_eq!( + PROCESS_TABLE.thread_count(pid).unwrap_or(0), + 0, + "container process still running" + ); + + if started { + SCHEDULER.shutdown(); + SYSTEM_TIMER + .start_periodic(TimerTicks::new(10_000_000)) + .expect("failed to restart system timer after container test"); + INTERRUPTS.enable(); + } + } + fn build_test_tar() -> Vec { let mut archive = Vec::new(); let data = b"hello from tar"; From b4c201acdf647f5b5f9be8aca248d8b46b4cd3fb Mon Sep 17 00:00:00 2001 From: n4mlz Date: Wed, 28 Jan 2026 10:54:54 +0900 Subject: [PATCH 03/25] refactor: remove unnecessary println! statements --- kernel/src/container/runtime.rs | 2 - kernel/src/device/tty/mod.rs | 30 +++---------- kernel/src/syscall/linux.rs | 78 +++------------------------------ 3 files changed, 14 insertions(+), 96 deletions(-) diff --git a/kernel/src/container/runtime.rs b/kernel/src/container/runtime.rs index 7682cfc..71de9ec 100644 --- a/kernel/src/container/runtime.rs +++ b/kernel/src/container/runtime.rs @@ -5,7 +5,6 @@ use crate::arch::api::ArchPageTableAccess; use crate::container::{CONTAINER_TABLE, Container, ContainerError, ContainerStatus}; use crate::fs::Path; use crate::loader::linux::{self, LinuxLoadError}; -use crate::println; use crate::process::{PROCESS_TABLE, ProcessError, ProcessId}; use crate::thread::{SCHEDULER, SpawnError}; @@ -87,7 +86,6 @@ pub fn start_container(container: Arc) -> Result) -> Result { - println!( - "[tty ioctl] pid={} cmd=0x{:x}", - SCHEDULER.current_process_id().unwrap_or(0), - request.command - ); - let result = match request.command { + match request.command { IOCTL_TCGETS => { let termios = self.termios_snapshot(); request.write_struct(&termios)?; @@ -326,11 +320,11 @@ impl ControlOps for TtyDevice { IOCTL_TIOCGPGRP => { let _ = self.require_controlling_tty()?; let mut pgrp = self.pgrp(); - if pgrp == 0 { - if let Some(current) = self.current_process_pgrp() { - pgrp = current; - self.set_pgrp(pgrp); - } + if pgrp == 0 + && let Some(current) = self.current_process_pgrp() + { + pgrp = current; + self.set_pgrp(pgrp); } let pgrp = pgrp as i32; request.write_struct(&pgrp)?; @@ -349,17 +343,7 @@ impl ControlOps for TtyDevice { Ok(0) } _ => Err(ControlError::Unsupported), - }; - println!( - "[tty ioctl] pid={} cmd=0x{:x} ret={}", - SCHEDULER.current_process_id().unwrap_or(0), - request.command, - match &result { - Ok(val) => alloc::format!("ok({})", val), - Err(err) => alloc::format!("err({:?})", err), - } - ); - result + } } } diff --git a/kernel/src/syscall/linux.rs b/kernel/src/syscall/linux.rs index 8372e70..d3d40ae 100644 --- a/kernel/src/syscall/linux.rs +++ b/kernel/src/syscall/linux.rs @@ -12,7 +12,6 @@ use crate::mem::addr::{ use crate::mem::manager; use crate::mem::paging::{FrameAllocator, MapError, PageTableOps, PhysMapper}; use crate::mem::user::{UserMemoryAccess, copy_from_user, copy_to_user, with_user_slice}; -use crate::println; use crate::process::fs as proc_fs; use crate::process::{ControllingTty, PROCESS_TABLE, ProcessId}; use crate::thread::SCHEDULER; @@ -106,10 +105,6 @@ pub fn dispatch( invocation: &SyscallInvocation, frame: Option<&mut CurrentTrapFrame>, ) -> DispatchResult { - println!( - "Linux syscall invoked: number={}, args={:?}", - invocation.number, invocation.args - ); match LinuxSyscall::from_raw(invocation.number) { Some(LinuxSyscall::Read) => DispatchResult::Completed(handle_read(invocation)), Some(LinuxSyscall::Write) => DispatchResult::Completed(handle_write(invocation)), @@ -222,25 +217,9 @@ fn handle_open(invocation: &SyscallInvocation) -> SysResult { proc_fs::open_path(pid, &path, flags) }; - if path == "/dev/tty" || path == "/dev/console" { - println!( - "[open] pid={} path={} flags=0x{:x} => {}", - pid, - path, - flags, - match &result { - Ok(fd) => alloc::format!("fd={}", fd), - Err(_) => "err".into(), - } - ); - } - let fd = result.map_err(|_| SysError::InvalidArgument)?; - if path == "/dev/tty" { - if process.session_id() == pid && !process.has_controlling_tty() { - process.set_controlling_tty(ControllingTty::Global); - println!("[ctty] pid={} acquired=tty", pid); - } + if path == "/dev/tty" && process.session_id() == pid && !process.has_controlling_tty() { + process.set_controlling_tty(ControllingTty::Global); } Ok(fd as u64) } @@ -292,7 +271,7 @@ fn handle_fcntl(invocation: &SyscallInvocation) -> SysResult { let cmd = invocation.arg(1).ok_or(SysError::InvalidArgument)?; let arg = invocation.arg(2).unwrap_or(0); - let result = match cmd { + match cmd { F_DUPFD => { let min = u32::try_from(arg).map_err(|_| SysError::InvalidArgument)?; proc_fs::dup_fd_min(pid, fd, min, false) @@ -315,21 +294,7 @@ fn handle_fcntl(invocation: &SyscallInvocation) -> SysResult { .map_err(|_| SysError::InvalidArgument) } _ => Err(SysError::NotImplemented), - }; - - println!( - "[fcntl] pid={} fd={} cmd={} arg={} => {}", - pid, - fd, - cmd, - arg, - match &result { - Ok(val) => alloc::format!("ok({})", val), - Err(err) => alloc::format!("err({:?})", err), - } - ); - - result + } } fn handle_ioctl(invocation: &SyscallInvocation) -> SysResult { @@ -341,24 +306,12 @@ fn handle_ioctl(invocation: &SyscallInvocation) -> SysResult { let process = PROCESS_TABLE .process_handle(pid) .map_err(|_| SysError::InvalidArgument)?; - let result = process.address_space().with_page_table(|table, _| { + + process.address_space().with_page_table(|table, _| { let user = UserMemoryAccess::new(table); let request = crate::util::stream::ControlRequest::new(cmd, arg, &user); proc_fs::control_fd(pid, fd as u32, &request).map_err(map_control_error) - }); - - println!( - "[ioctl] pid={} fd={} cmd=0x{:x} => {}", - pid, - fd, - cmd, - match &result { - Ok(val) => alloc::format!("ok({})", val), - Err(err) => alloc::format!("err({:?})", err), - } - ); - - result + }) } fn handle_getpid(_invocation: &SyscallInvocation) -> SysResult { @@ -389,12 +342,6 @@ fn handle_setsid(_invocation: &SyscallInvocation) -> SysResult { let process = PROCESS_TABLE .process_handle(pid) .map_err(|_| SysError::InvalidArgument)?; - println!( - "[setsid] pid={} old_sid={} old_pgrp={}", - pid, - process.session_id(), - process.pgrp_id() - ); if process.session_id() == pid { return Err(SysError::InvalidArgument); } @@ -445,10 +392,6 @@ fn handle_setpgid(invocation: &SyscallInvocation) -> SysResult { return Err(SysError::InvalidArgument); } - println!( - "[setpgid] caller={} target={} new_pgrp={}", - caller_pid, target_pid, target_pgid - ); target.set_pgrp_id(target_pgid); Ok(0) } @@ -743,13 +686,6 @@ fn handle_execve( if let Ok(process) = PROCESS_TABLE.process_handle(pid) { process.set_brk_base(program.heap_base); - println!( - "[proc] pid={} sid={} pgrp={} ctty={}", - pid, - process.session_id(), - process.pgrp_id(), - process.has_controlling_tty() - ); } frame.rip = program.entry.as_raw() as u64; From 6ea7569e21ac6f746b521e82d563bd68bd12ab62 Mon Sep 17 00:00:00 2001 From: n4mlz Date: Wed, 28 Jan 2026 12:48:51 +0900 Subject: [PATCH 04/25] feat: implement blocking read and canonical input handling for TTY device --- kernel/src/device/tty/DESIGN.md | 4 ++ kernel/src/device/tty/mod.rs | 96 +++++++++++++++++++++++++++++++-- 2 files changed, 95 insertions(+), 5 deletions(-) diff --git a/kernel/src/device/tty/DESIGN.md b/kernel/src/device/tty/DESIGN.md index f367537..f71efdd 100644 --- a/kernel/src/device/tty/DESIGN.md +++ b/kernel/src/device/tty/DESIGN.md @@ -10,6 +10,10 @@ - `TtyDevice` is a character device backed by the architecture console (`Arch::console()`). - Input/output buffers are maintained for tests and for cases where caller-supplied input should be consumed before touching hardware. +- Reads block (via halt/spin) until at least one byte is available, so interactive programs do not + observe spurious EOF when no input is pending. +- Canonical reads perform basic line editing (erase, EOF) and map carriage return to newline to + keep serial console input usable without a full line discipline. - Control operations (`ControlOps`) implement the minimal ioctl set required by BusyBox: termios getters/setters, window size queries, foreground process-group access, and controlling TTY acquisition. diff --git a/kernel/src/device/tty/mod.rs b/kernel/src/device/tty/mod.rs index d4f6273..775b19f 100644 --- a/kernel/src/device/tty/mod.rs +++ b/kernel/src/device/tty/mod.rs @@ -217,6 +217,64 @@ impl TtyDevice { self.state.lock().pgrp = pgrp; } + fn read_byte_blocking(&self) -> u8 { + loop { + let mut byte = [0u8; 1]; + if self.read_from_input(&mut byte) == 1 { + return byte[0]; + } + let console = Arch::console(); + if console.read(&mut byte).unwrap_or(0) == 1 { + return byte[0]; + } + #[cfg(target_arch = "x86_64")] + crate::arch::x86_64::halt(); + #[cfg(not(target_arch = "x86_64"))] + core::hint::spin_loop(); + } + } + + fn read_canonical(&self, buf: &mut [u8], echo: bool, c_cc: [u8; NCCS]) -> usize { + let mut total = 0usize; + let mut eof_seen = false; + while total < buf.len() { + let mut b = self.read_byte_blocking(); + if b == b'\r' { + b = b'\n'; + } + if b == c_cc[VERASE] { + if total > 0 { + total -= 1; + if echo { + let _ = Arch::console().write(b"\x08 \x08"); + } + } + continue; + } + if b == c_cc[VEOF] { + eof_seen = true; + break; + } + buf[total] = b; + total += 1; + if echo { + let out = if b == b'\n' && (self.termios_snapshot().c_oflag & OF_ONLCR) != 0 { + b"\r\n" + } else { + core::slice::from_ref(&b) + }; + let _ = Arch::console().write(out); + } + if b == b'\n' { + break; + } + } + if eof_seen && total == 0 { + return 0; + } + total + } + fn current_process(&self) -> Option { let pid = SCHEDULER.current_process_id()?; PROCESS_TABLE.process_handle(pid).ok() @@ -256,11 +314,39 @@ impl ReadOps for TtyDevice { type Error = core::convert::Infallible; fn read(&self, buf: &mut [u8]) -> Result { - let mut total = self.read_from_input(buf); - if total == 0 { - let console = Arch::console(); - let read = console.read(&mut buf[total..]).unwrap_or(0); - total += read; + let termios = self.termios_snapshot(); + let icrnl = (termios.c_iflag & IF_ICRNL) != 0; + let icanon = (termios.c_lflag & LF_ICANON) != 0; + let echo = (termios.c_lflag & LF_ECHO) != 0; + if buf.is_empty() { + return Ok(0); + } + + if icanon { + return Ok(self.read_canonical(buf, echo, termios.c_cc)); + } + + let mut total = 0usize; + while total == 0 { + total = self.read_from_input(buf); + if total == 0 { + let console = Arch::console(); + let read = console.read(&mut buf[total..]).unwrap_or(0); + total += read; + } + if total == 0 { + #[cfg(target_arch = "x86_64")] + crate::arch::x86_64::halt(); + #[cfg(not(target_arch = "x86_64"))] + core::hint::spin_loop(); + } + } + if icrnl { + for byte in &mut buf[..total] { + if *byte == b'\r' { + *byte = b'\n'; + } + } } Ok(total) } From f6ebbd49d9ae3ff2ee5a8a180c7354cc09cd3301 Mon Sep 17 00:00:00 2001 From: n4mlz Date: Wed, 28 Jan 2026 12:49:15 +0900 Subject: [PATCH 05/25] tmp: add debug logging for TTY and Linux syscall operations --- kernel/src/device/tty/mod.rs | 21 ++++++++++++++++++++- kernel/src/syscall/linux.rs | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/kernel/src/device/tty/mod.rs b/kernel/src/device/tty/mod.rs index 775b19f..1fb0cd3 100644 --- a/kernel/src/device/tty/mod.rs +++ b/kernel/src/device/tty/mod.rs @@ -11,6 +11,8 @@ use crate::util::lazylock::LazyLock; use crate::util::spinlock::SpinLock; use crate::util::stream::{ControlError, ControlOps, ControlRequest, ReadOps, WriteOps}; +const DEBUG_TTY: bool = true; + const TTY_BUFFER_LIMIT: usize = 4096; const IOCTL_TCGETS: u64 = 0x5401; @@ -323,7 +325,11 @@ impl ReadOps for TtyDevice { } if icanon { - return Ok(self.read_canonical(buf, echo, termios.c_cc)); + let read = self.read_canonical(buf, echo, termios.c_cc); + if DEBUG_TTY && read > 0 { + crate::println!("[tty] read canonical {} byte(s)", read); + } + return Ok(read); } let mut total = 0usize; @@ -348,6 +354,9 @@ impl ReadOps for TtyDevice { } } } + if DEBUG_TTY && total > 0 { + crate::println!("[tty] read raw {} byte(s)", total); + } Ok(total) } } @@ -359,6 +368,9 @@ impl WriteOps for TtyDevice { let console = Arch::console(); let written = console.write(data).unwrap_or(0); self.record_output(&data[..written]); + if DEBUG_TTY && written > 0 { + crate::println!("[tty] write {} byte(s)", written); + } Ok(written) } } @@ -369,6 +381,13 @@ impl CharDevice for TtyDevice { impl ControlOps for TtyDevice { fn control(&self, request: &ControlRequest<'_>) -> Result { + if DEBUG_TTY { + crate::println!( + "[tty] ioctl cmd=0x{:x} arg=0x{:x}", + request.command, + request.arg + ); + } match request.command { IOCTL_TCGETS => { let termios = self.termios_snapshot(); diff --git a/kernel/src/syscall/linux.rs b/kernel/src/syscall/linux.rs index d3d40ae..a8ed85e 100644 --- a/kernel/src/syscall/linux.rs +++ b/kernel/src/syscall/linux.rs @@ -17,6 +17,8 @@ use crate::process::{ControllingTty, PROCESS_TABLE, ProcessId}; use crate::thread::SCHEDULER; use crate::trap::CurrentTrapFrame; +const DEBUG_LINUX_SYSCALL: bool = true; + #[repr(u16)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum LinuxErrno { @@ -105,6 +107,18 @@ pub fn dispatch( invocation: &SyscallInvocation, frame: Option<&mut CurrentTrapFrame>, ) -> DispatchResult { + if DEBUG_LINUX_SYSCALL { + crate::println!( + "[linux-syscall] nr={} args=[{:x}, {:x}, {:x}, {:x}, {:x}, {:x}]", + invocation.number, + invocation.args[0], + invocation.args[1], + invocation.args[2], + invocation.args[3], + invocation.args[4], + invocation.args[5], + ); + } match LinuxSyscall::from_raw(invocation.number) { Some(LinuxSyscall::Read) => DispatchResult::Completed(handle_read(invocation)), Some(LinuxSyscall::Write) => DispatchResult::Completed(handle_write(invocation)), @@ -138,7 +152,12 @@ pub fn dispatch( Some(LinuxSyscall::GetPgid) => DispatchResult::Completed(handle_getpgid(invocation)), Some(LinuxSyscall::GetSid) => DispatchResult::Completed(handle_getsid(invocation)), Some(LinuxSyscall::Exit) => handle_exit(invocation), - None => DispatchResult::Completed(Err(SysError::NotImplemented)), + None => { + if DEBUG_LINUX_SYSCALL { + crate::println!("[linux-syscall] nr={} -> ENOSYS", invocation.number); + } + DispatchResult::Completed(Err(SysError::NotImplemented)) + } } } @@ -171,6 +190,9 @@ fn handle_read(invocation: &SyscallInvocation) -> SysResult { .map_err(|_| SysError::InvalidArgument)?; let user_ptr = VirtAddr::new(ptr as usize); copy_to_user(user_ptr, &buf[..read]).map_err(|_| SysError::InvalidArgument)?; + if DEBUG_LINUX_SYSCALL { + crate::println!("[linux-syscall] read fd={} len={} -> {}", fd, read_len, read); + } Ok(read as u64) } @@ -193,6 +215,12 @@ fn handle_write(invocation: &SyscallInvocation) -> SysResult { copy_from_user(&mut buf[..read_len], user_ptr).map_err(|_| SysError::InvalidArgument)?; let written = proc_fs::write_fd(pid, fd as u32, &buf[..read_len]) .map_err(|_| SysError::InvalidArgument)?; + if DEBUG_LINUX_SYSCALL { + crate::println!( + "[linux-syscall] write fd={} len={} -> {}", + fd, read_len, written + ); + } Ok(written as u64) } @@ -271,6 +299,9 @@ fn handle_fcntl(invocation: &SyscallInvocation) -> SysResult { let cmd = invocation.arg(1).ok_or(SysError::InvalidArgument)?; let arg = invocation.arg(2).unwrap_or(0); + if DEBUG_LINUX_SYSCALL { + crate::println!("[linux-syscall] fcntl fd={} cmd={} arg={}", fd, cmd, arg); + } match cmd { F_DUPFD => { let min = u32::try_from(arg).map_err(|_| SysError::InvalidArgument)?; @@ -307,6 +338,9 @@ fn handle_ioctl(invocation: &SyscallInvocation) -> SysResult { .process_handle(pid) .map_err(|_| SysError::InvalidArgument)?; + if DEBUG_LINUX_SYSCALL { + crate::println!("[linux-syscall] ioctl fd={} cmd=0x{:x} arg=0x{:x}", fd, cmd, arg); + } process.address_space().with_page_table(|table, _| { let user = UserMemoryAccess::new(table); let request = crate::util::stream::ControlRequest::new(cmd, arg, &user); From fbfb9b703c8dfc138a9163585ebe8bd67bbc128a Mon Sep 17 00:00:00 2001 From: n4mlz Date: Wed, 28 Jan 2026 16:04:32 +0900 Subject: [PATCH 06/25] feat: add poll syscall support for TTY input availability --- kernel/src/device/tty/mod.rs | 13 +++ kernel/src/syscall/DESIGN.md | 5 +- kernel/src/syscall/linux.rs | 201 ++++++++++++++++++++++++++++++++++- 3 files changed, 215 insertions(+), 4 deletions(-) diff --git a/kernel/src/device/tty/mod.rs b/kernel/src/device/tty/mod.rs index 1fb0cd3..ee0b5a8 100644 --- a/kernel/src/device/tty/mod.rs +++ b/kernel/src/device/tty/mod.rs @@ -171,6 +171,19 @@ impl TtyDevice { guard.clear(); } + pub fn input_available(&self) -> bool { + if !self.input.lock().is_empty() { + return true; + } + let console = Arch::console(); + let mut byte = [0u8; 1]; + if console.read(&mut byte).unwrap_or(0) == 1 { + self.input.lock().push_back(byte[0]); + return true; + } + false + } + fn record_output(&self, data: &[u8]) { let mut guard = self.output.lock(); for byte in data { diff --git a/kernel/src/syscall/DESIGN.md b/kernel/src/syscall/DESIGN.md index 1574704..853602f 100644 --- a/kernel/src/syscall/DESIGN.md +++ b/kernel/src/syscall/DESIGN.md @@ -19,8 +19,9 @@ process using the container VFS. Host pointers are treated as kernel-mapped addresses until userland separation exists. - Linux dispatch implements a minimal set of process/syscall plumbing needed by static busybox: - `read`, `write`, `open`, `close`, `writev`, `stat`, `brk`, `fork`, `execve`, `wait4`, `arch_prctl`, - `ioctl` (routed through `ControlOps`), `fcntl` (dup + FD_CLOEXEC), and basic process/session + `read`, `write`, `open`, `close`, `writev`, `stat`, `brk`, `poll` (TTY-only, blocking until input), + `fork`, `execve`, `wait4`, `arch_prctl`, `ioctl` (routed through `ControlOps`), `fcntl` (dup + + FD_CLOEXEC), and basic process/session metadata (`getppid`, `getpgrp`, `getpgid`, `setpgid`, `getsid`, `setsid`), plus stubbed signal calls. Unsupported numbers map to `ENOSYS`, while unsupported ioctls map to `ENOTTY`. - `/dev/tty` open assigns the global controlling TTY when the caller is a session leader and no diff --git a/kernel/src/syscall/linux.rs b/kernel/src/syscall/linux.rs index a8ed85e..5b27699 100644 --- a/kernel/src/syscall/linux.rs +++ b/kernel/src/syscall/linux.rs @@ -11,11 +11,14 @@ use crate::mem::addr::{ }; use crate::mem::manager; use crate::mem::paging::{FrameAllocator, MapError, PageTableOps, PhysMapper}; -use crate::mem::user::{UserMemoryAccess, copy_from_user, copy_to_user, with_user_slice}; +use crate::mem::user::{ + UserMemoryAccess, copy_from_user, copy_to_user, with_user_slice, with_user_slice_mut, +}; use crate::process::fs as proc_fs; -use crate::process::{ControllingTty, PROCESS_TABLE, ProcessId}; +use crate::process::{ControllingTty, PROCESS_TABLE, ProcessHandle, ProcessId}; use crate::thread::SCHEDULER; use crate::trap::CurrentTrapFrame; +use crate::util::stream::{ControlAccess, ControlError, ControlRequest}; const DEBUG_LINUX_SYSCALL: bool = true; @@ -46,6 +49,7 @@ pub enum LinuxSyscall { Writev = 20, GetPid = 39, Fcntl = 72, + Poll = 7, SetPgid = 109, GetPpid = 110, GetPgrp = 111, @@ -72,6 +76,7 @@ impl LinuxSyscall { 2 => Some(Self::Open), 3 => Some(Self::Close), 4 => Some(Self::Stat), + 7 => Some(Self::Poll), 9 => Some(Self::Mmap), 11 => Some(Self::Munmap), 12 => Some(Self::Brk), @@ -126,6 +131,7 @@ pub fn dispatch( Some(LinuxSyscall::Close) => DispatchResult::Completed(handle_close(invocation)), Some(LinuxSyscall::Writev) => DispatchResult::Completed(handle_writev(invocation)), Some(LinuxSyscall::Stat) => DispatchResult::Completed(handle_stat(invocation)), + Some(LinuxSyscall::Poll) => DispatchResult::Completed(handle_poll(invocation)), Some(LinuxSyscall::Mmap) => DispatchResult::Completed(handle_mmap(invocation)), Some(LinuxSyscall::Munmap) => DispatchResult::Completed(handle_munmap(invocation)), Some(LinuxSyscall::Brk) => DispatchResult::Completed(handle_brk(invocation)), @@ -472,6 +478,42 @@ fn handle_stat(invocation: &SyscallInvocation) -> SysResult { Ok(0) } +fn handle_poll(invocation: &SyscallInvocation) -> SysResult { + const POLLIN: i16 = 0x0001; + const POLLNVAL: i16 = 0x0020; + + let pid = current_pid()?; + let fds_ptr = invocation.arg(0).ok_or(SysError::InvalidArgument)?; + let nfds = invocation.arg(1).ok_or(SysError::InvalidArgument)?; + let timeout_raw = invocation.arg(2).unwrap_or(0); + let timeout_ms = if timeout_raw == u64::MAX { + -1 + } else { + i64::try_from(timeout_raw).map_err(|_| SysError::InvalidArgument)? + }; + let nfds = usize::try_from(nfds).map_err(|_| SysError::InvalidArgument)?; + if nfds == 0 { + return Ok(0); + } + + let process = PROCESS_TABLE + .process_handle(pid) + .map_err(|_| SysError::InvalidArgument)?; + let ptr = VirtAddr::new(fds_ptr as usize); + + loop { + let ready = with_user_slice_mut(ptr, nfds, |fds| poll_once(fds, &process, POLLIN, POLLNVAL)) + .map_err(|_| SysError::BadAddress)?; + if ready > 0 || timeout_ms == 0 { + return Ok(ready as u64); + } + // NOTE: Timeout handling is intentionally simplified; any non-zero timeout blocks + // until an event arrives. We spin here instead of halting because syscalls may run + // with interrupts disabled, making `halt` non-resumable. + core::hint::spin_loop(); + } +} + fn handle_brk(invocation: &SyscallInvocation) -> SysResult { // TODO: This is a grow-only brk; shrinking does not unmap pages and no heap upper bound // is enforced yet. The behavior is enough for busybox's basic allocator path. @@ -1141,6 +1183,84 @@ fn writev_from_iovecs( Ok(()) } +fn poll_once( + fds: &mut [LinuxPollFd], + process: &ProcessHandle, + pollin: i16, + pollnval: i16, +) -> usize { + let mut wants_input = false; + let mut tty_flags = Vec::with_capacity(fds.len()); + for fd in fds.iter_mut() { + fd.revents = 0; + if fd.fd < 0 { + tty_flags.push(false); + continue; + } + if process.fd_table().entry(fd.fd as u32).is_err() { + fd.revents = pollnval; + tty_flags.push(false); + continue; + } + let is_tty = file_is_tty(process, fd.fd); + tty_flags.push(is_tty); + if (fd.events & pollin) != 0 { + if is_tty { + wants_input = true; + } else { + fd.revents |= pollin; + } + } + } + + let input_ready = if wants_input { + crate::device::tty::global_tty().input_available() + } else { + false + }; + + let mut ready = 0usize; + for (fd, is_tty) in fds.iter_mut().zip(tty_flags.iter()) { + if fd.revents != 0 { + ready += 1; + continue; + } + if *is_tty && (fd.events & pollin) != 0 && input_ready { + fd.revents |= pollin; + } + if fd.revents != 0 { + ready += 1; + } + } + ready +} + +fn file_is_tty(process: &ProcessHandle, fd: i32) -> bool { + const IOCTL_TIOCGWINSZ: u64 = 0x5413; + + let entry = match process.fd_table().entry(fd as u32) { + Ok(entry) => entry, + Err(_) => return false, + }; + let mut winsize = LinuxWinsize { + ws_row: 0, + ws_col: 0, + ws_xpixel: 0, + ws_ypixel: 0, + }; + let access = KernelControlAccess; + let request = ControlRequest::new( + IOCTL_TIOCGWINSZ, + core::ptr::addr_of_mut!(winsize) as u64, + &access, + ); + match entry.file().ioctl(&request) { + Ok(_) => true, + Err(ControlError::Unsupported) => false, + Err(_) => false, + } +} + fn encode_wait_status(code: i32) -> u32 { ((code as u32) & 0xFF) << 8 } @@ -1166,6 +1286,55 @@ struct LinuxIovec { len: u64, } +#[repr(C)] +#[derive(Clone, Copy)] +struct LinuxPollFd { + fd: i32, + events: i16, + revents: i16, +} + +#[repr(C)] +#[derive(Clone, Copy)] +struct LinuxWinsize { + ws_row: u16, + ws_col: u16, + ws_xpixel: u16, + ws_ypixel: u16, +} + +struct KernelControlAccess; + +impl ControlAccess for KernelControlAccess { + fn read(&self, addr: u64, dst: &mut [u8]) -> Result<(), ControlError> { + if dst.is_empty() { + return Ok(()); + } + let ptr = addr as *const u8; + if ptr.is_null() { + return Err(ControlError::BadAddress); + } + unsafe { + core::ptr::copy_nonoverlapping(ptr, dst.as_mut_ptr(), dst.len()); + } + Ok(()) + } + + fn write(&self, addr: u64, src: &[u8]) -> Result<(), ControlError> { + if src.is_empty() { + return Ok(()); + } + let ptr = addr as *mut u8; + if ptr.is_null() { + return Err(ControlError::BadAddress); + } + unsafe { + core::ptr::copy_nonoverlapping(src.as_ptr(), ptr, src.len()); + } + Ok(()) + } +} + #[repr(C)] #[derive(Clone, Copy)] struct LinuxStat { @@ -1230,6 +1399,7 @@ impl LinuxStat { mod tests { use super::*; use crate::fs::DirNode; + use crate::device::tty::global_tty; use crate::mem::addr::VirtAddr; use crate::println; use crate::process::PROCESS_TABLE; @@ -1310,6 +1480,33 @@ mod tests { } } + #[kernel_test_case] + fn poll_reports_tty_input() { + println!("[test] poll_reports_tty_input"); + + let _ = PROCESS_TABLE.init_kernel(); + SCHEDULER.init().expect("scheduler init"); + let tty = global_tty(); + tty.push_input(b"X"); + + let mut fds = [LinuxPollFd { + fd: 0, + events: 0x0001, + revents: 0, + }]; + let invocation = SyscallInvocation::new( + LinuxSyscall::Poll as u64, + [fds.as_mut_ptr() as u64, fds.len() as u64, 0, 0, 0, 0], + ); + match dispatch(&invocation, None) { + DispatchResult::Completed(Ok(ready)) => { + assert_eq!(ready, 1); + assert_eq!(fds[0].revents & 0x0001, 0x0001); + } + other => panic!("unexpected dispatch result: {:?}", other), + } + } + #[kernel_test_case] fn fcntl_dupfd_sets_cloexec() { println!("[test] fcntl_dupfd_sets_cloexec"); From 5ee7dd0e7ce739046446a2188f6f3f3b36127177 Mon Sep 17 00:00:00 2001 From: n4mlz Date: Wed, 28 Jan 2026 19:31:45 +0900 Subject: [PATCH 07/25] feat: implement seek functionality for file and memory filesystems --- kernel/src/device/tty/mod.rs | 18 ----- kernel/src/fs/DESIGN.md | 2 + kernel/src/fs/file.rs | 4 + kernel/src/fs/vfs/fat32.rs | 17 +++++ kernel/src/fs/vfs/memfs.rs | 43 +++++++++++ kernel/src/process/fs.rs | 6 ++ kernel/src/syscall/DESIGN.md | 4 +- kernel/src/syscall/host.rs | 1 + kernel/src/syscall/linux.rs | 139 ++++++++++++++++++++++++----------- kernel/src/syscall/mod.rs | 1 + 10 files changed, 172 insertions(+), 63 deletions(-) diff --git a/kernel/src/device/tty/mod.rs b/kernel/src/device/tty/mod.rs index ee0b5a8..7e77913 100644 --- a/kernel/src/device/tty/mod.rs +++ b/kernel/src/device/tty/mod.rs @@ -11,8 +11,6 @@ use crate::util::lazylock::LazyLock; use crate::util::spinlock::SpinLock; use crate::util::stream::{ControlError, ControlOps, ControlRequest, ReadOps, WriteOps}; -const DEBUG_TTY: bool = true; - const TTY_BUFFER_LIMIT: usize = 4096; const IOCTL_TCGETS: u64 = 0x5401; @@ -339,9 +337,6 @@ impl ReadOps for TtyDevice { if icanon { let read = self.read_canonical(buf, echo, termios.c_cc); - if DEBUG_TTY && read > 0 { - crate::println!("[tty] read canonical {} byte(s)", read); - } return Ok(read); } @@ -367,9 +362,6 @@ impl ReadOps for TtyDevice { } } } - if DEBUG_TTY && total > 0 { - crate::println!("[tty] read raw {} byte(s)", total); - } Ok(total) } } @@ -381,9 +373,6 @@ impl WriteOps for TtyDevice { let console = Arch::console(); let written = console.write(data).unwrap_or(0); self.record_output(&data[..written]); - if DEBUG_TTY && written > 0 { - crate::println!("[tty] write {} byte(s)", written); - } Ok(written) } } @@ -394,13 +383,6 @@ impl CharDevice for TtyDevice { impl ControlOps for TtyDevice { fn control(&self, request: &ControlRequest<'_>) -> Result { - if DEBUG_TTY { - crate::println!( - "[tty] ioctl cmd=0x{:x} arg=0x{:x}", - request.command, - request.arg - ); - } match request.command { IOCTL_TCGETS => { let termios = self.termios_snapshot(); diff --git a/kernel/src/fs/DESIGN.md b/kernel/src/fs/DESIGN.md index 7ead12d..794e81e 100644 --- a/kernel/src/fs/DESIGN.md +++ b/kernel/src/fs/DESIGN.md @@ -33,6 +33,8 @@ at the root so callers (process VFS ops, tar extraction, loader) share consistent behaviour. - Process FDs advance offsets on successful reads/writes. Write support is provided by filesystems that opt in (e.g. memfs); read-only filesystems return `ReadOnly`. +- `File::seek` allows per-open offsets to be repositioned; regular file handles implement it while + character devices and directories report `NotFile`. - Control-plane operations (ioctl-style) are routed through `File::ioctl` and only device-backed `File` implementations opt in, keeping ioctl out of regular file nodes. - `FileSystemProbe` abstracts per-filesystem probing so boot-time selection can iterate through diff --git a/kernel/src/fs/file.rs b/kernel/src/fs/file.rs index 6feb3b9..c50ee9e 100644 --- a/kernel/src/fs/file.rs +++ b/kernel/src/fs/file.rs @@ -15,6 +15,10 @@ pub trait File: Send + Sync { Err(VfsError::NotDirectory) } + fn seek(&self, _offset: i64, _whence: u32) -> Result { + Err(VfsError::NotFile) + } + fn ioctl(&self, _request: &ControlRequest<'_>) -> Result { Err(ControlError::Unsupported) } diff --git a/kernel/src/fs/vfs/fat32.rs b/kernel/src/fs/vfs/fat32.rs index 8dd11bc..9eb2d49 100644 --- a/kernel/src/fs/vfs/fat32.rs +++ b/kernel/src/fs/vfs/fat32.rs @@ -512,6 +512,23 @@ impl File for FatFileHandle { *guard = guard.checked_add(read).ok_or(VfsError::Corrupted)?; Ok(read) } + + fn seek(&self, offset: i64, whence: u32) -> Result { + let mut guard = self.pos.lock(); + let base = match whence { + 0 => 0i64, + 1 => *guard as i64, + 2 => i64::from(self.node.size), + _ => return Err(VfsError::InvalidPath), + }; + let next = base.checked_add(offset).ok_or(VfsError::Corrupted)?; + if next < 0 { + return Err(VfsError::InvalidPath); + } + let next = usize::try_from(next).map_err(|_| VfsError::Corrupted)?; + *guard = next; + Ok(next as u64) + } } impl Node for FatFileNode { diff --git a/kernel/src/fs/vfs/memfs.rs b/kernel/src/fs/vfs/memfs.rs index 30b754f..f723a76 100644 --- a/kernel/src/fs/vfs/memfs.rs +++ b/kernel/src/fs/vfs/memfs.rs @@ -113,6 +113,23 @@ impl File for MemFileHandle { *guard = guard.checked_add(written).ok_or(VfsError::Corrupted)?; Ok(written) } + + fn seek(&self, offset: i64, whence: u32) -> Result { + let mut guard = self.pos.lock(); + let base = match whence { + 0 => 0i64, + 1 => *guard as i64, + 2 => self.node.size() as i64, + _ => return Err(VfsError::InvalidPath), + }; + let next = base.checked_add(offset).ok_or(VfsError::Corrupted)?; + if next < 0 { + return Err(VfsError::InvalidPath); + } + let next = usize::try_from(next).map_err(|_| VfsError::Corrupted)?; + *guard = next; + Ok(next as u64) + } } struct MemDirFile { @@ -269,6 +286,32 @@ impl Node for MemSymlink { } } +#[cfg(test)] +mod tests { + use super::*; + use crate::println; + use crate::test::kernel_test_case; + + #[kernel_test_case] + fn memfs_seek_updates_offset() { + println!("[test] memfs_seek_updates_offset"); + + let root = MemDirectory::new(); + let root_view = root.as_dir().expect("root dir"); + let file_node = root_view.create_file("note").expect("create file"); + let file = file_node.open(OpenOptions::new(0)).expect("open file"); + + let _ = file.write(b"abcd").expect("write"); + let pos = file.seek(1, 0).expect("seek"); + assert_eq!(pos, 1); + + let mut buf = [0u8; 2]; + let read = file.read(&mut buf).expect("read"); + assert_eq!(read, 2); + assert_eq!(&buf[..read], b"bc"); + } +} + impl SymlinkNode for MemSymlink { fn readlink(&self) -> Result { Ok(self.target.clone()) diff --git a/kernel/src/process/fs.rs b/kernel/src/process/fs.rs index badd81e..fb2e617 100644 --- a/kernel/src/process/fs.rs +++ b/kernel/src/process/fs.rs @@ -71,6 +71,12 @@ pub fn write_fd(pid: ProcessId, fd: Fd, data: &[u8]) -> Result process.fd_table().write(fd, data) } +pub fn seek_fd(pid: ProcessId, fd: Fd, offset: i64, whence: u32) -> Result { + let process = process_handle(pid)?; + let entry = process.fd_table().entry(fd)?; + entry.file().seek(offset, whence) +} + pub fn close_fd(pid: ProcessId, fd: Fd) -> Result<(), VfsError> { let process = process_handle(pid)?; process.fd_table().close(fd) diff --git a/kernel/src/syscall/DESIGN.md b/kernel/src/syscall/DESIGN.md index 853602f..11c6e4e 100644 --- a/kernel/src/syscall/DESIGN.md +++ b/kernel/src/syscall/DESIGN.md @@ -20,8 +20,8 @@ userland separation exists. - Linux dispatch implements a minimal set of process/syscall plumbing needed by static busybox: `read`, `write`, `open`, `close`, `writev`, `stat`, `brk`, `poll` (TTY-only, blocking until input), - `fork`, `execve`, `wait4`, `arch_prctl`, `ioctl` (routed through `ControlOps`), `fcntl` (dup + - FD_CLOEXEC), and basic process/session + `lseek` (currently reports `ESPIPE`), `getcwd`, `chdir`, `fork`, `execve`, `wait4`, `arch_prctl`, + `ioctl` (routed through `ControlOps`), `fcntl` (dup + FD_CLOEXEC), and basic process/session metadata (`getppid`, `getpgrp`, `getpgid`, `setpgid`, `getsid`, `setsid`), plus stubbed signal calls. Unsupported numbers map to `ENOSYS`, while unsupported ioctls map to `ENOTTY`. - `/dev/tty` open assigns the global controlling TTY when the caller is a session leader and no diff --git a/kernel/src/syscall/host.rs b/kernel/src/syscall/host.rs index deaf03a..af42fbc 100644 --- a/kernel/src/syscall/host.rs +++ b/kernel/src/syscall/host.rs @@ -55,6 +55,7 @@ fn encode_error(err: SysError) -> u64 { SysError::NotFound => HostErrno::NotFound as u64, SysError::BadAddress => HostErrno::BadAddress as u64, SysError::NotTty => HostErrno::InvalidArgument as u64, + SysError::IllegalSeek => HostErrno::InvalidArgument as u64, } } diff --git a/kernel/src/syscall/linux.rs b/kernel/src/syscall/linux.rs index 5b27699..e183e16 100644 --- a/kernel/src/syscall/linux.rs +++ b/kernel/src/syscall/linux.rs @@ -1,11 +1,12 @@ use super::{DispatchResult, SysError, SysResult, SyscallInvocation}; -use alloc::string::String; +use alloc::string::{String, ToString}; use alloc::vec::Vec; use crate::arch::Arch; use crate::arch::api::{ArchPageTableAccess, ArchThread}; use crate::fs::NodeKind; +use crate::interrupt::INTERRUPTS; use crate::mem::addr::{ Addr, MemPerm, Page, PageSize, VirtAddr, VirtIntoPtr, align_down, align_up, }; @@ -20,8 +21,6 @@ use crate::thread::SCHEDULER; use crate::trap::CurrentTrapFrame; use crate::util::stream::{ControlAccess, ControlError, ControlRequest}; -const DEBUG_LINUX_SYSCALL: bool = true; - #[repr(u16)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum LinuxErrno { @@ -30,6 +29,7 @@ pub enum LinuxErrno { InvalidArgument = 22, BadAddress = 14, NotTty = 25, + IllegalSeek = 29, } #[repr(u64)] @@ -40,6 +40,8 @@ pub enum LinuxSyscall { Open = 2, Close = 3, Stat = 4, + Poll = 7, + Lseek = 8, Mmap = 9, Munmap = 11, Brk = 12, @@ -49,7 +51,8 @@ pub enum LinuxSyscall { Writev = 20, GetPid = 39, Fcntl = 72, - Poll = 7, + GetCwd = 79, + Chdir = 80, SetPgid = 109, GetPpid = 110, GetPgrp = 111, @@ -77,6 +80,7 @@ impl LinuxSyscall { 3 => Some(Self::Close), 4 => Some(Self::Stat), 7 => Some(Self::Poll), + 8 => Some(Self::Lseek), 9 => Some(Self::Mmap), 11 => Some(Self::Munmap), 12 => Some(Self::Brk), @@ -86,6 +90,8 @@ impl LinuxSyscall { 20 => Some(Self::Writev), 39 => Some(Self::GetPid), 72 => Some(Self::Fcntl), + 79 => Some(Self::GetCwd), + 80 => Some(Self::Chdir), 109 => Some(Self::SetPgid), 110 => Some(Self::GetPpid), 111 => Some(Self::GetPgrp), @@ -112,18 +118,6 @@ pub fn dispatch( invocation: &SyscallInvocation, frame: Option<&mut CurrentTrapFrame>, ) -> DispatchResult { - if DEBUG_LINUX_SYSCALL { - crate::println!( - "[linux-syscall] nr={} args=[{:x}, {:x}, {:x}, {:x}, {:x}, {:x}]", - invocation.number, - invocation.args[0], - invocation.args[1], - invocation.args[2], - invocation.args[3], - invocation.args[4], - invocation.args[5], - ); - } match LinuxSyscall::from_raw(invocation.number) { Some(LinuxSyscall::Read) => DispatchResult::Completed(handle_read(invocation)), Some(LinuxSyscall::Write) => DispatchResult::Completed(handle_write(invocation)), @@ -132,10 +126,13 @@ pub fn dispatch( Some(LinuxSyscall::Writev) => DispatchResult::Completed(handle_writev(invocation)), Some(LinuxSyscall::Stat) => DispatchResult::Completed(handle_stat(invocation)), Some(LinuxSyscall::Poll) => DispatchResult::Completed(handle_poll(invocation)), + Some(LinuxSyscall::Lseek) => DispatchResult::Completed(handle_lseek(invocation)), Some(LinuxSyscall::Mmap) => DispatchResult::Completed(handle_mmap(invocation)), Some(LinuxSyscall::Munmap) => DispatchResult::Completed(handle_munmap(invocation)), Some(LinuxSyscall::Brk) => DispatchResult::Completed(handle_brk(invocation)), Some(LinuxSyscall::Fcntl) => DispatchResult::Completed(handle_fcntl(invocation)), + Some(LinuxSyscall::GetCwd) => DispatchResult::Completed(handle_getcwd(invocation)), + Some(LinuxSyscall::Chdir) => DispatchResult::Completed(handle_chdir(invocation)), Some(LinuxSyscall::SetPgid) => DispatchResult::Completed(handle_setpgid(invocation)), Some(LinuxSyscall::GetPpid) => DispatchResult::Completed(handle_getppid(invocation)), Some(LinuxSyscall::GetPgrp) => DispatchResult::Completed(handle_getpgrp(invocation)), @@ -158,12 +155,7 @@ pub fn dispatch( Some(LinuxSyscall::GetPgid) => DispatchResult::Completed(handle_getpgid(invocation)), Some(LinuxSyscall::GetSid) => DispatchResult::Completed(handle_getsid(invocation)), Some(LinuxSyscall::Exit) => handle_exit(invocation), - None => { - if DEBUG_LINUX_SYSCALL { - crate::println!("[linux-syscall] nr={} -> ENOSYS", invocation.number); - } - DispatchResult::Completed(Err(SysError::NotImplemented)) - } + None => DispatchResult::Completed(Err(SysError::NotImplemented)), } } @@ -196,9 +188,6 @@ fn handle_read(invocation: &SyscallInvocation) -> SysResult { .map_err(|_| SysError::InvalidArgument)?; let user_ptr = VirtAddr::new(ptr as usize); copy_to_user(user_ptr, &buf[..read]).map_err(|_| SysError::InvalidArgument)?; - if DEBUG_LINUX_SYSCALL { - crate::println!("[linux-syscall] read fd={} len={} -> {}", fd, read_len, read); - } Ok(read as u64) } @@ -221,12 +210,6 @@ fn handle_write(invocation: &SyscallInvocation) -> SysResult { copy_from_user(&mut buf[..read_len], user_ptr).map_err(|_| SysError::InvalidArgument)?; let written = proc_fs::write_fd(pid, fd as u32, &buf[..read_len]) .map_err(|_| SysError::InvalidArgument)?; - if DEBUG_LINUX_SYSCALL { - crate::println!( - "[linux-syscall] write fd={} len={} -> {}", - fd, read_len, written - ); - } Ok(written as u64) } @@ -305,9 +288,6 @@ fn handle_fcntl(invocation: &SyscallInvocation) -> SysResult { let cmd = invocation.arg(1).ok_or(SysError::InvalidArgument)?; let arg = invocation.arg(2).unwrap_or(0); - if DEBUG_LINUX_SYSCALL { - crate::println!("[linux-syscall] fcntl fd={} cmd={} arg={}", fd, cmd, arg); - } match cmd { F_DUPFD => { let min = u32::try_from(arg).map_err(|_| SysError::InvalidArgument)?; @@ -344,9 +324,6 @@ fn handle_ioctl(invocation: &SyscallInvocation) -> SysResult { .process_handle(pid) .map_err(|_| SysError::InvalidArgument)?; - if DEBUG_LINUX_SYSCALL { - crate::println!("[linux-syscall] ioctl fd={} cmd=0x{:x} arg=0x{:x}", fd, cmd, arg); - } process.address_space().with_page_table(|table, _| { let user = UserMemoryAccess::new(table); let request = crate::util::stream::ControlRequest::new(cmd, arg, &user); @@ -478,6 +455,58 @@ fn handle_stat(invocation: &SyscallInvocation) -> SysResult { Ok(0) } +fn handle_lseek(invocation: &SyscallInvocation) -> SysResult { + let pid = current_pid()?; + let fd = invocation.arg(0).ok_or(SysError::InvalidArgument)?; + let offset = invocation.arg(1).ok_or(SysError::InvalidArgument)? as i64; + let whence = invocation.arg(2).ok_or(SysError::InvalidArgument)?; + let whence = u32::try_from(whence).map_err(|_| SysError::InvalidArgument)?; + match proc_fs::seek_fd(pid, fd as u32, offset, whence) { + Ok(pos) => Ok(pos), + Err(crate::fs::VfsError::NotFile) => Err(SysError::IllegalSeek), + Err(crate::fs::VfsError::NotDirectory) => Err(SysError::IllegalSeek), + Err(crate::fs::VfsError::InvalidPath) => Err(SysError::InvalidArgument), + Err(crate::fs::VfsError::NotFound) => Err(SysError::InvalidArgument), + Err(_) => Err(SysError::InvalidArgument), + } +} + +fn handle_getcwd(invocation: &SyscallInvocation) -> SysResult { + let pid = current_pid()?; + let buf_ptr = invocation.arg(0).ok_or(SysError::InvalidArgument)?; + let size = invocation.arg(1).ok_or(SysError::InvalidArgument)?; + let size = usize::try_from(size).map_err(|_| SysError::InvalidArgument)?; + if size == 0 { + return Err(SysError::InvalidArgument); + } + + let cwd = proc_fs::cwd(pid).map_err(|_| SysError::InvalidArgument)?; + let text = cwd.to_string(); + let bytes = text.as_bytes(); + if bytes.len().saturating_add(1) > size { + return Err(SysError::InvalidArgument); + } + let user_ptr = VirtAddr::new(buf_ptr as usize); + copy_to_user(user_ptr, bytes).map_err(|_| SysError::InvalidArgument)?; + let nul = VirtAddr::new(user_ptr.as_raw() + bytes.len()); + copy_to_user(nul, &[0]).map_err(|_| SysError::InvalidArgument)?; + Ok(buf_ptr) +} + +fn handle_chdir(invocation: &SyscallInvocation) -> SysResult { + let pid = current_pid()?; + let ptr = invocation.arg(0).ok_or(SysError::InvalidArgument)?; + let process = PROCESS_TABLE + .process_handle(pid) + .map_err(|_| SysError::InvalidArgument)?; + let path = process.address_space().with_page_table(|table, _| { + let user = UserMemoryAccess::new(table); + read_cstring_with_user(&user, ptr) + })?; + proc_fs::change_dir(pid, &path).map_err(|_| SysError::InvalidArgument)?; + Ok(0) +} + fn handle_poll(invocation: &SyscallInvocation) -> SysResult { const POLLIN: i16 = 0x0001; const POLLNVAL: i16 = 0x0020; @@ -502,8 +531,9 @@ fn handle_poll(invocation: &SyscallInvocation) -> SysResult { let ptr = VirtAddr::new(fds_ptr as usize); loop { - let ready = with_user_slice_mut(ptr, nfds, |fds| poll_once(fds, &process, POLLIN, POLLNVAL)) - .map_err(|_| SysError::BadAddress)?; + let ready = + with_user_slice_mut(ptr, nfds, |fds| poll_once(fds, &process, POLLIN, POLLNVAL)) + .map_err(|_| SysError::BadAddress)?; if ready > 0 || timeout_ms == 0 { return Ok(ready as u64); } @@ -831,9 +861,9 @@ fn handle_wait4(invocation: &SyscallInvocation) -> SysResult { return Ok(0); } - #[cfg(target_arch = "x86_64")] - crate::arch::x86_64::halt(); - #[cfg(not(target_arch = "x86_64"))] + // NOTE: Syscalls may run with interrupts disabled; enable them so the scheduler can + // observe child exits while we wait. + INTERRUPTS.enable(); core::hint::spin_loop(); } } @@ -1048,6 +1078,7 @@ fn errno_for(err: SysError) -> u16 { SysError::NotFound => LinuxErrno::NoEntry as u16, SysError::BadAddress => LinuxErrno::BadAddress as u16, SysError::NotTty => LinuxErrno::NotTty as u16, + SysError::IllegalSeek => LinuxErrno::IllegalSeek as u16, } } @@ -1398,8 +1429,8 @@ impl LinuxStat { #[cfg(test)] mod tests { use super::*; - use crate::fs::DirNode; use crate::device::tty::global_tty; + use crate::fs::DirNode; use crate::mem::addr::VirtAddr; use crate::println; use crate::process::PROCESS_TABLE; @@ -1507,6 +1538,28 @@ mod tests { } } + #[kernel_test_case] + fn getcwd_returns_root_path() { + println!("[test] getcwd_returns_root_path"); + + let _ = PROCESS_TABLE.init_kernel(); + SCHEDULER.init().expect("scheduler init"); + + let mut buf = [0u8; 8]; + let invocation = SyscallInvocation::new( + LinuxSyscall::GetCwd as u64, + [buf.as_mut_ptr() as u64, buf.len() as u64, 0, 0, 0, 0], + ); + match dispatch(&invocation, None) { + DispatchResult::Completed(Ok(ptr)) => { + assert_eq!(ptr, buf.as_ptr() as u64); + assert_eq!(buf[0], b'/'); + assert_eq!(buf[1], 0); + } + other => panic!("unexpected dispatch result: {:?}", other), + } + } + #[kernel_test_case] fn fcntl_dupfd_sets_cloexec() { println!("[test] fcntl_dupfd_sets_cloexec"); diff --git a/kernel/src/syscall/mod.rs b/kernel/src/syscall/mod.rs index 77dd7ca..a57ec41 100644 --- a/kernel/src/syscall/mod.rs +++ b/kernel/src/syscall/mod.rs @@ -28,6 +28,7 @@ pub enum SysError { NotFound, BadAddress, NotTty, + IllegalSeek, } pub type SysResult = Result; From 65d4540d97a6a75c73a6618b6bf7e80b563d0896 Mon Sep 17 00:00:00 2001 From: n4mlz Date: Wed, 28 Jan 2026 20:30:35 +0900 Subject: [PATCH 08/25] feat: implement clone functionality for FdTable and ProcessFs --- kernel/src/device/tty/mod.rs | 24 +++++++ kernel/src/fs/fd.rs | 31 +++++++++ kernel/src/process/fs.rs | 24 +++++-- kernel/src/process/mod.rs | 9 +++ kernel/src/syscall/DESIGN.md | 3 +- kernel/src/syscall/linux.rs | 129 ++++++++++++++++++++++++++++++++++- 6 files changed, 211 insertions(+), 9 deletions(-) diff --git a/kernel/src/device/tty/mod.rs b/kernel/src/device/tty/mod.rs index 7e77913..440679f 100644 --- a/kernel/src/device/tty/mod.rs +++ b/kernel/src/device/tty/mod.rs @@ -11,6 +11,8 @@ use crate::util::lazylock::LazyLock; use crate::util::spinlock::SpinLock; use crate::util::stream::{ControlError, ControlOps, ControlRequest, ReadOps, WriteOps}; +const DEBUG_TTY: bool = true; + const TTY_BUFFER_LIMIT: usize = 4096; const IOCTL_TCGETS: u64 = 0x5401; @@ -301,6 +303,14 @@ impl TtyDevice { fn require_controlling_tty(&self) -> Result { let proc = self.current_process().ok_or(ControlError::Invalid)?; if !proc.has_controlling_tty() { + if DEBUG_TTY { + crate::println!( + "[tty] missing controlling tty pid={} pgrp={} sid={}", + proc.id(), + proc.pgrp_id(), + proc.session_id() + ); + } return Err(ControlError::Invalid); } Ok(proc) @@ -407,6 +417,14 @@ impl ControlOps for TtyDevice { IOCTL_TIOCSCTTY => { let proc = self.current_process().ok_or(ControlError::Invalid)?; let pid = proc.id(); + if DEBUG_TTY { + crate::println!( + "[tty] TIOCSCTTY pid={} sid={} has_ctty={}", + pid, + proc.session_id(), + proc.has_controlling_tty() + ); + } if proc.session_id() != pid { return Err(ControlError::Invalid); } @@ -426,6 +444,9 @@ impl ControlOps for TtyDevice { pgrp = current; self.set_pgrp(pgrp); } + if DEBUG_TTY { + crate::println!("[tty] TIOCGPGRP -> {}", pgrp); + } let pgrp = pgrp as i32; request.write_struct(&pgrp)?; Ok(0) @@ -433,6 +454,9 @@ impl ControlOps for TtyDevice { IOCTL_TIOCSPGRP => { let _ = self.require_controlling_tty()?; let pgrp = request.read_struct::()?; + if DEBUG_TTY { + crate::println!("[tty] TIOCSPGRP set pgrp {}", pgrp); + } if pgrp < 0 { return Err(ControlError::Invalid); } diff --git a/kernel/src/fs/fd.rs b/kernel/src/fs/fd.rs index 2e6f21c..162138f 100644 --- a/kernel/src/fs/fd.rs +++ b/kernel/src/fs/fd.rs @@ -111,6 +111,14 @@ impl FdTable { let guard = self.inner.lock(); guard.get(fd).cloned() } + + pub fn clone_from(&self, other: &FdTable) { + let other_guard = other.inner.lock(); + let mut guard = self.inner.lock(); + guard.slots = other_guard.slots.clone(); + self.next_fd + .store(other.next_fd.load(Ordering::Acquire), Ordering::Release); + } } impl Default for FdTable { @@ -188,3 +196,26 @@ impl FdTableInner { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::fs::devfs::global_tty_node; + use crate::println; + use crate::test::kernel_test_case; + + #[kernel_test_case] + fn fd_table_clone_copies_entries() { + println!("[test] fd_table_clone_copies_entries"); + + let src = FdTable::new(); + let dst = FdTable::new(); + let tty = global_tty_node() + .open(crate::fs::OpenOptions::new(0)) + .expect("open tty"); + src.open_fixed(10, tty).expect("open fixed"); + + dst.clone_from(&src); + assert!(dst.entry(10).is_ok(), "cloned fd missing"); + } +} diff --git a/kernel/src/process/fs.rs b/kernel/src/process/fs.rs index fb2e617..16159ae 100644 --- a/kernel/src/process/fs.rs +++ b/kernel/src/process/fs.rs @@ -103,11 +103,25 @@ pub fn control_fd( request: &ControlRequest<'_>, ) -> Result { let process = process_handle(pid).map_err(|_| ControlError::Invalid)?; - let entry = process - .fd_table() - .entry(fd) - .map_err(|_| ControlError::Invalid)?; - entry.file().ioctl(request) + let entry = match process.fd_table().entry(fd) { + Ok(entry) => entry, + Err(_) => { + if request.command == 0x5410 { + crate::println!("[proc-fs] ioctl fd not found pid={} fd={}", pid, fd); + } + return Err(ControlError::Invalid); + } + }; + let result = entry.file().ioctl(request); + if request.command == 0x5410 { + crate::println!( + "[proc-fs] ioctl fd={} cmd=0x{:x} -> {:?}", + fd, + request.command, + result + ); + } + result } pub fn change_dir(pid: ProcessId, raw_path: &str) -> Result<(), VfsError> { diff --git a/kernel/src/process/mod.rs b/kernel/src/process/mod.rs index 8c9c293..c3aa32f 100644 --- a/kernel/src/process/mod.rs +++ b/kernel/src/process/mod.rs @@ -67,6 +67,11 @@ impl ProcessFs { let guard = self.cwd.lock(); guard.clone() } + + pub fn clone_from(&self, other: &ProcessFs) { + self.fd_table.clone_from(&other.fd_table); + self.set_cwd(other.cwd()); + } } impl Default for ProcessFs { @@ -475,6 +480,10 @@ impl Process { &self.fs.fd_table } + pub fn clone_fs_from(&self, other: &Process) { + self.fs.clone_from(&other.fs); + } + pub fn parent(&self) -> Option { *self.parent.lock() } diff --git a/kernel/src/syscall/DESIGN.md b/kernel/src/syscall/DESIGN.md index 11c6e4e..4cb9bfd 100644 --- a/kernel/src/syscall/DESIGN.md +++ b/kernel/src/syscall/DESIGN.md @@ -22,7 +22,8 @@ `read`, `write`, `open`, `close`, `writev`, `stat`, `brk`, `poll` (TTY-only, blocking until input), `lseek` (currently reports `ESPIPE`), `getcwd`, `chdir`, `fork`, `execve`, `wait4`, `arch_prctl`, `ioctl` (routed through `ControlOps`), `fcntl` (dup + FD_CLOEXEC), and basic process/session - metadata (`getppid`, `getpgrp`, `getpgid`, `setpgid`, `getsid`, `setsid`), plus stubbed signal + metadata (`getppid`, `getpgrp`, `getpgid`, `setpgid`, `getsid`, `setsid`), plus `uname`, + `geteuid`, and stubbed signal calls. Unsupported numbers map to `ENOSYS`, while unsupported ioctls map to `ENOTTY`. - `/dev/tty` open assigns the global controlling TTY when the caller is a session leader and no controlling TTY is present yet; this is a minimal bridge until full tty/session semantics land. diff --git a/kernel/src/syscall/linux.rs b/kernel/src/syscall/linux.rs index e183e16..a462230 100644 --- a/kernel/src/syscall/linux.rs +++ b/kernel/src/syscall/linux.rs @@ -21,6 +21,10 @@ use crate::thread::SCHEDULER; use crate::trap::CurrentTrapFrame; use crate::util::stream::{ControlAccess, ControlError, ControlRequest}; +const DEBUG_LS_SYSCALL: bool = true; +const DEBUG_LINUX_ENOSYS: bool = true; +const DEBUG_LINUX_EXECVE: bool = true; + #[repr(u16)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum LinuxErrno { @@ -49,6 +53,7 @@ pub enum LinuxSyscall { RtSigprocmask = 14, Ioctl = 16, Writev = 20, + Uname = 63, GetPid = 39, Fcntl = 72, GetCwd = 79, @@ -60,9 +65,11 @@ pub enum LinuxSyscall { Fork = 57, Execve = 59, Exit = 60, + ExitGroup = 231, Wait4 = 61, GetUid = 102, GetGid = 104, + GetEuid = 107, SetUid = 105, SetGid = 106, GetPgid = 121, @@ -73,6 +80,7 @@ pub enum LinuxSyscall { impl LinuxSyscall { pub fn from_raw(value: u64) -> Option { + let value = value as u32 as u64; match value { 0 => Some(Self::Read), 1 => Some(Self::Write), @@ -89,6 +97,7 @@ impl LinuxSyscall { 16 => Some(Self::Ioctl), 20 => Some(Self::Writev), 39 => Some(Self::GetPid), + 63 => Some(Self::Uname), 72 => Some(Self::Fcntl), 79 => Some(Self::GetCwd), 80 => Some(Self::Chdir), @@ -99,9 +108,11 @@ impl LinuxSyscall { 57 => Some(Self::Fork), 59 => Some(Self::Execve), 60 => Some(Self::Exit), + 231 => Some(Self::ExitGroup), 61 => Some(Self::Wait4), 102 => Some(Self::GetUid), 104 => Some(Self::GetGid), + 107 => Some(Self::GetEuid), 105 => Some(Self::SetUid), 106 => Some(Self::SetGid), 121 => Some(Self::GetPgid), @@ -118,6 +129,23 @@ pub fn dispatch( invocation: &SyscallInvocation, frame: Option<&mut CurrentTrapFrame>, ) -> DispatchResult { + if DEBUG_LS_SYSCALL { + match invocation.number { + 217 | 257 | 262 => { + crate::println!( + "[linux-ls] nr={} args=[{:x}, {:x}, {:x}, {:x}, {:x}, {:x}]", + invocation.number, + invocation.args[0], + invocation.args[1], + invocation.args[2], + invocation.args[3], + invocation.args[4], + invocation.args[5], + ); + } + _ => {} + } + } match LinuxSyscall::from_raw(invocation.number) { Some(LinuxSyscall::Read) => DispatchResult::Completed(handle_read(invocation)), Some(LinuxSyscall::Write) => DispatchResult::Completed(handle_write(invocation)), @@ -131,6 +159,7 @@ pub fn dispatch( Some(LinuxSyscall::Munmap) => DispatchResult::Completed(handle_munmap(invocation)), Some(LinuxSyscall::Brk) => DispatchResult::Completed(handle_brk(invocation)), Some(LinuxSyscall::Fcntl) => DispatchResult::Completed(handle_fcntl(invocation)), + Some(LinuxSyscall::Uname) => DispatchResult::Completed(handle_uname(invocation)), Some(LinuxSyscall::GetCwd) => DispatchResult::Completed(handle_getcwd(invocation)), Some(LinuxSyscall::Chdir) => DispatchResult::Completed(handle_chdir(invocation)), Some(LinuxSyscall::SetPgid) => DispatchResult::Completed(handle_setpgid(invocation)), @@ -149,13 +178,29 @@ pub fn dispatch( // NOTE: UID/GID syscalls are stubbed to 0 for now; user/cred support is not implemented yet. Some(LinuxSyscall::GetUid) => DispatchResult::Completed(Ok(0)), Some(LinuxSyscall::GetGid) => DispatchResult::Completed(Ok(0)), + Some(LinuxSyscall::GetEuid) => DispatchResult::Completed(Ok(0)), Some(LinuxSyscall::SetUid) => DispatchResult::Completed(Ok(0)), Some(LinuxSyscall::SetGid) => DispatchResult::Completed(Ok(0)), Some(LinuxSyscall::GetPid) => DispatchResult::Completed(handle_getpid(invocation)), Some(LinuxSyscall::GetPgid) => DispatchResult::Completed(handle_getpgid(invocation)), Some(LinuxSyscall::GetSid) => DispatchResult::Completed(handle_getsid(invocation)), Some(LinuxSyscall::Exit) => handle_exit(invocation), - None => DispatchResult::Completed(Err(SysError::NotImplemented)), + Some(LinuxSyscall::ExitGroup) => handle_exit(invocation), + None => { + if DEBUG_LINUX_ENOSYS { + crate::println!( + "[linux-syscall] ENOSYS nr={} args=[{:x}, {:x}, {:x}, {:x}, {:x}, {:x}]", + invocation.number, + invocation.args[0], + invocation.args[1], + invocation.args[2], + invocation.args[3], + invocation.args[4], + invocation.args[5], + ); + } + DispatchResult::Completed(Err(SysError::NotImplemented)) + } } } @@ -324,11 +369,21 @@ fn handle_ioctl(invocation: &SyscallInvocation) -> SysResult { .process_handle(pid) .map_err(|_| SysError::InvalidArgument)?; - process.address_space().with_page_table(|table, _| { + let result = process.address_space().with_page_table(|table, _| { let user = UserMemoryAccess::new(table); let request = crate::util::stream::ControlRequest::new(cmd, arg, &user); proc_fs::control_fd(pid, fd as u32, &request).map_err(map_control_error) - }) + }); + if cmd == 0x540e || cmd == 0x540f || cmd == 0x5410 { + crate::println!( + "[linux-ioctl] pid={} fd={} cmd=0x{:x} -> {:?}", + pid, + fd, + cmd, + result + ); + } + result } fn handle_getpid(_invocation: &SyscallInvocation) -> SysResult { @@ -507,6 +562,23 @@ fn handle_chdir(invocation: &SyscallInvocation) -> SysResult { Ok(0) } +fn handle_uname(invocation: &SyscallInvocation) -> SysResult { + let pid = current_pid()?; + let addr = invocation.arg(0).ok_or(SysError::InvalidArgument)?; + let process = PROCESS_TABLE + .process_handle(pid) + .map_err(|_| SysError::InvalidArgument)?; + let uts = LinuxUtsName::new(); + let dst = VirtAddr::new(addr as usize); + process.address_space().with_page_table(|table, _| { + let user = UserMemoryAccess::new(table); + user.write_bytes(dst, uts.as_bytes()) + .map_err(|_| SysError::BadAddress)?; + Ok::<(), SysError>(()) + })?; + Ok(0) +} + fn handle_poll(invocation: &SyscallInvocation) -> SysResult { const POLLIN: i16 = 0x0001; const POLLNVAL: i16 = 0x0020; @@ -676,6 +748,7 @@ fn handle_fork( child_proc.set_parent(pid); child_proc.set_session_id(parent_proc.session_id()); child_proc.set_pgrp_id(parent_proc.pgrp_id()); + child_proc.clone_fs_from(&parent_proc); if let Some(tty) = parent_proc.controlling_tty() { child_proc.set_controlling_tty(tty); } @@ -740,6 +813,10 @@ fn handle_execve( let argv_ptr = invocation.arg(1).unwrap_or(0); let envp_ptr = invocation.arg(2).unwrap_or(0); + if DEBUG_LINUX_EXECVE { + crate::println!("[linux-execve] path={path}"); + } + let argv = match process.address_space().with_page_table(|table, _| { let user = UserMemoryAccess::new(table); read_cstring_array_with_user(&user, argv_ptr, 128) @@ -1334,6 +1411,52 @@ struct LinuxWinsize { ws_ypixel: u16, } +#[repr(C)] +#[derive(Clone, Copy)] +struct LinuxUtsName { + sysname: [u8; 65], + nodename: [u8; 65], + release: [u8; 65], + version: [u8; 65], + machine: [u8; 65], + domainname: [u8; 65], +} + +impl LinuxUtsName { + fn new() -> Self { + fn write_field(dst: &mut [u8; 65], src: &[u8]) { + let len = dst.len().saturating_sub(1).min(src.len()); + dst[..len].copy_from_slice(&src[..len]); + dst[len] = 0; + } + + let mut uts = Self { + sysname: [0; 65], + nodename: [0; 65], + release: [0; 65], + version: [0; 65], + machine: [0; 65], + domainname: [0; 65], + }; + write_field(&mut uts.sysname, b"Linux"); + write_field(&mut uts.nodename, b"cyrius"); + write_field(&mut uts.release, b"5.10.0"); + write_field(&mut uts.version, b"cyrius"); + write_field(&mut uts.machine, b"x86_64"); + write_field(&mut uts.domainname, b""); + uts + } + + fn as_bytes(&self) -> &[u8] { + unsafe { + core::slice::from_raw_parts( + (self as *const LinuxUtsName) as *const u8, + core::mem::size_of::(), + ) + } + } +} + struct KernelControlAccess; impl ControlAccess for KernelControlAccess { From bfbcb03d57274d3369e87ad38cfb9f1669c3f8e9 Mon Sep 17 00:00:00 2001 From: n4mlz Date: Thu, 29 Jan 2026 11:41:42 +0900 Subject: [PATCH 09/25] fix: add interrupt management functions and enhance stack initialization for Linux processes --- kernel/src/arch/api.rs | 1 + kernel/src/arch/x86_64/mod.rs | 4 + kernel/src/arch/x86_64/trap/handlers.rs | 19 +- kernel/src/kernel_proc/linux_box.rs | 3 + kernel/src/loader/linux/mod.rs | 1 + kernel/src/loader/linux/reloc.rs | 388 +++++++++++++++++++++--- kernel/src/loader/linux/stack.rs | 48 ++- kernel/src/syscall/linux.rs | 84 ++++- kernel/src/thread/mod.rs | 111 ++++--- 9 files changed, 571 insertions(+), 88 deletions(-) diff --git a/kernel/src/arch/api.rs b/kernel/src/arch/api.rs index cd93994..dda8e46 100644 --- a/kernel/src/arch/api.rs +++ b/kernel/src/arch/api.rs @@ -243,6 +243,7 @@ pub trait ArchInterrupt { fn init_interrupts(boot_info: &'static BootInfo) -> Result<(), InterruptInitError>; + fn are_interrupts_enabled() -> bool; fn enable_interrupts(); fn disable_interrupts(); fn end_of_interrupt(vector: u8); diff --git a/kernel/src/arch/x86_64/mod.rs b/kernel/src/arch/x86_64/mod.rs index e8f3368..59ff720 100644 --- a/kernel/src/arch/x86_64/mod.rs +++ b/kernel/src/arch/x86_64/mod.rs @@ -105,6 +105,10 @@ impl ArchInterrupt for X86_64 { interrupt::LOCAL_APIC.init(boot_info) } + fn are_interrupts_enabled() -> bool { + x86_64::instructions::interrupts::are_enabled() + } + fn enable_interrupts() { interrupt::LOCAL_APIC.enable(); } diff --git a/kernel/src/arch/x86_64/trap/handlers.rs b/kernel/src/arch/x86_64/trap/handlers.rs index 08d2aca..5dfd75e 100644 --- a/kernel/src/arch/x86_64/trap/handlers.rs +++ b/kernel/src/arch/x86_64/trap/handlers.rs @@ -1,6 +1,8 @@ use x86_64::registers::control::Cr2; use crate::println; +use crate::process::PROCESS_TABLE; +use crate::thread::SCHEDULER; use crate::trap::TrapInfo; use super::TrapFrame; @@ -8,8 +10,7 @@ use super::TrapFrame; pub fn handle_exception(info: TrapInfo, frame: &mut TrapFrame) -> bool { match info.vector { 14 => { - handle_page_fault(frame); - true + handle_page_fault(frame) } 6 => { handle_invalid_opcode(frame); @@ -27,13 +28,13 @@ pub fn handle_exception(info: TrapInfo, frame: &mut TrapFrame) -> bool { } } -fn handle_page_fault(frame: &TrapFrame) { +fn handle_page_fault(frame: &mut TrapFrame) -> bool { let fault_addr = Cr2::read().expect("CR2 must contain a canonical address"); let code = frame.error_code; let present = (code & 1) != 0; let write = (code & 1 << 1) != 0; - let user = (code & 1 << 2) != 0; + let user = (code & 1 << 2) != 0 || (frame.cs & 3) != 0; let reserved = (code & 1 << 3) != 0; let instruction = (code & 1 << 4) != 0; @@ -47,6 +48,16 @@ fn handle_page_fault(frame: &TrapFrame) { instruction ); println!("[#PF] frame={:#?}", frame); + if user { + if let Some(pid) = SCHEDULER.current_process_id() { + if let Ok(process) = PROCESS_TABLE.process_handle(pid) { + // Use a conventional non-zero status for user faults. + process.set_exit_code(139); + } + } + SCHEDULER.terminate_current(frame); + return true; + } panic!("page fault while executing in kernel context"); } diff --git a/kernel/src/kernel_proc/linux_box.rs b/kernel/src/kernel_proc/linux_box.rs index d78f608..c221cee 100644 --- a/kernel/src/kernel_proc/linux_box.rs +++ b/kernel/src/kernel_proc/linux_box.rs @@ -7,6 +7,7 @@ use crate::fs::{Path, VfsError}; use crate::loader::linux::{self, LinuxLoadError}; use crate::process::fs as proc_fs; use crate::process::{PROCESS_TABLE, ProcessError, ProcessId}; +use crate::interrupt::INTERRUPTS; use crate::thread::{SCHEDULER, SpawnError}; /// Errors surfaced while launching or supervising a Linux guest process. @@ -96,6 +97,8 @@ fn wait_for_exit(pid: ProcessId) { .map(|count| count > 0) .unwrap_or(false) { + // Ensure interrupts are enabled so HALT can resume. + INTERRUPTS.enable(); #[cfg(target_arch = "x86_64")] crate::arch::x86_64::halt(); #[cfg(not(target_arch = "x86_64"))] diff --git a/kernel/src/loader/linux/mod.rs b/kernel/src/loader/linux/mod.rs index 22d4ee9..81dc6de 100644 --- a/kernel/src/loader/linux/mod.rs +++ b/kernel/src/loader/linux/mod.rs @@ -24,6 +24,7 @@ pub const AT_PHNUM: u64 = 5; pub const AT_PAGESZ: u64 = 6; pub const AT_BASE: u64 = 7; pub const AT_ENTRY: u64 = 9; +pub const AT_RANDOM: u64 = 25; /// Linux ELF image loaded into a process address space. pub struct LinuxProgram { diff --git a/kernel/src/loader/linux/reloc.rs b/kernel/src/loader/linux/reloc.rs index d83769b..19c21ff 100644 --- a/kernel/src/loader/linux/reloc.rs +++ b/kernel/src/loader/linux/reloc.rs @@ -1,7 +1,6 @@ use crate::mem::addr::{Addr, MemPerm, Page, PageSize, VirtAddr, align_down, align_up}; use crate::mem::paging::PageTableOps; use crate::mem::user::{UserAccessError, UserMemoryAccess}; -use crate::println; use super::LinuxLoadError; use super::add_base; @@ -12,14 +11,36 @@ const DT_NULL: i64 = 0; const DT_RELA: i64 = 7; const DT_RELASZ: i64 = 8; const DT_RELAENT: i64 = 9; +const DT_REL: i64 = 17; +const DT_RELSZ: i64 = 18; +const DT_RELENT: i64 = 19; +const DT_PLTREL: i64 = 20; +const DT_JMPREL: i64 = 23; +const DT_PLTRELSZ: i64 = 2; +const DT_RELRSZ: i64 = 35; +const DT_RELR: i64 = 36; +const DT_RELRENT: i64 = 37; +const R_X86_64_64: u32 = 1; +const R_X86_64_GLOB_DAT: u32 = 6; +const R_X86_64_JUMP_SLOT: u32 = 7; const R_X86_64_RELATIVE: u32 = 8; + #[derive(Debug, Default)] pub struct DynamicInfo { pub rela_addr: Option, pub rela_size: usize, pub rela_ent: usize, + pub rel_addr: Option, + pub rel_size: usize, + pub rel_ent: usize, + pub jmprel_addr: Option, + pub jmprel_size: usize, + pub jmprel_is_rela: bool, + pub relr_addr: Option, + pub relr_size: usize, + pub relr_ent: usize, } pub fn read_dynamic_info( @@ -32,6 +53,7 @@ pub fn read_dynamic_info( let mut info = DynamicInfo::default(); let mut offset = 0usize; let entry_size = core::mem::size_of::(); + info.jmprel_is_rela = true; if !segment.mem_size.is_multiple_of(entry_size) { return Err(LinuxLoadError::InvalidElf( "dynamic section size misaligned", @@ -53,9 +75,8 @@ pub fn read_dynamic_info( } match tag { DT_RELA => { - info.rela_addr = Some(VirtAddr::new( - usize::try_from(val).map_err(|_| LinuxLoadError::SizeOverflow)?, - )); + let raw = usize::try_from(val).map_err(|_| LinuxLoadError::SizeOverflow)?; + info.rela_addr = Some(resolve_dynamic_ptr(table, base, raw)?); } DT_RELASZ => { info.rela_size = usize::try_from(val).map_err(|_| LinuxLoadError::SizeOverflow)?; @@ -63,6 +84,41 @@ pub fn read_dynamic_info( DT_RELAENT => { info.rela_ent = usize::try_from(val).map_err(|_| LinuxLoadError::SizeOverflow)?; } + DT_REL => { + let raw = usize::try_from(val).map_err(|_| LinuxLoadError::SizeOverflow)?; + info.rel_addr = Some(resolve_dynamic_ptr(table, base, raw)?); + } + DT_RELSZ => { + info.rel_size = usize::try_from(val).map_err(|_| LinuxLoadError::SizeOverflow)?; + } + DT_RELENT => { + info.rel_ent = usize::try_from(val).map_err(|_| LinuxLoadError::SizeOverflow)?; + } + DT_JMPREL => { + let raw = usize::try_from(val).map_err(|_| LinuxLoadError::SizeOverflow)?; + info.jmprel_addr = Some(resolve_dynamic_ptr(table, base, raw)?); + } + DT_PLTRELSZ => { + info.jmprel_size = + usize::try_from(val).map_err(|_| LinuxLoadError::SizeOverflow)?; + } + DT_PLTREL => { + info.jmprel_is_rela = match val as i64 { + DT_RELA => true, + DT_REL => false, + _ => return Err(LinuxLoadError::InvalidElf("unsupported PLTREL type")), + }; + } + DT_RELR => { + let raw = usize::try_from(val).map_err(|_| LinuxLoadError::SizeOverflow)?; + info.relr_addr = Some(resolve_dynamic_ptr(table, base, raw)?); + } + DT_RELRSZ => { + info.relr_size = usize::try_from(val).map_err(|_| LinuxLoadError::SizeOverflow)?; + } + DT_RELRENT => { + info.relr_ent = usize::try_from(val).map_err(|_| LinuxLoadError::SizeOverflow)?; + } _ => {} } offset = offset @@ -76,6 +132,18 @@ pub fn read_dynamic_info( if info.rela_size > 0 && info.rela_addr.is_none() { return Err(LinuxLoadError::InvalidElf("DT_RELA missing")); } + if info.rel_size > 0 && info.rel_ent == 0 { + return Err(LinuxLoadError::InvalidElf("DT_RELENT missing")); + } + if info.rel_size > 0 && info.rel_addr.is_none() { + return Err(LinuxLoadError::InvalidElf("DT_REL missing")); + } + if info.jmprel_size > 0 && info.jmprel_addr.is_none() { + return Err(LinuxLoadError::InvalidElf("DT_JMPREL missing")); + } + if info.relr_size > 0 && info.relr_addr.is_none() { + return Err(LinuxLoadError::InvalidElf("DT_RELR missing")); + } Ok(info) } @@ -84,24 +152,98 @@ pub fn apply_relocations( base: VirtAddr, info: &DynamicInfo, segments: &[MappedSegment], +) -> Result<(), LinuxLoadError> { + apply_rel(table, base, info, segments)?; + apply_rela(table, base, info, segments)?; + apply_relr(table, base, info, segments)?; + apply_jmprel(table, base, info, segments)?; + Ok(()) +} + +fn apply_rel( + table: &T, + base: VirtAddr, + info: &DynamicInfo, + segments: &[MappedSegment], +) -> Result<(), LinuxLoadError> { + let rel_addr = match info.rel_addr { + Some(addr) => addr, + None => return Ok(()), + }; + apply_rel_table(table, base, rel_addr, info.rel_size, info.rel_ent, segments, "REL") +} + +fn apply_rela( + table: &T, + base: VirtAddr, + info: &DynamicInfo, + segments: &[MappedSegment], ) -> Result<(), LinuxLoadError> { let rela_addr = match info.rela_addr { - Some(addr) => add_base(base, addr)?, + Some(addr) => addr, + None => return Ok(()), + }; + apply_rela_table(table, base, rela_addr, info.rela_size, info.rela_ent, segments, "RELA") +} + +fn apply_jmprel( + table: &T, + base: VirtAddr, + info: &DynamicInfo, + segments: &[MappedSegment], +) -> Result<(), LinuxLoadError> { + let jmprel_addr = match info.jmprel_addr { + Some(addr) => addr, None => return Ok(()), }; - if info.rela_size == 0 { + if info.jmprel_size == 0 { + return Ok(()); + } + if info.jmprel_is_rela { + apply_rela_table( + table, + base, + jmprel_addr, + info.jmprel_size, + core::mem::size_of::(), + segments, + "JMPRELA", + ) + } else { + apply_rel_table( + table, + base, + jmprel_addr, + info.jmprel_size, + core::mem::size_of::(), + segments, + "JMPREL", + ) + } +} + +fn apply_rela_table( + table: &T, + base: VirtAddr, + rela_addr: VirtAddr, + rela_size: usize, + rela_ent: usize, + segments: &[MappedSegment], + _label: &str, +) -> Result<(), LinuxLoadError> { + if rela_size == 0 { return Ok(()); } let entry_size = core::mem::size_of::(); - if info.rela_ent != entry_size { + if rela_ent != entry_size { return Err(LinuxLoadError::InvalidElf("unexpected Rela entry size")); } - if !info.rela_size.is_multiple_of(entry_size) { + if !rela_size.is_multiple_of(entry_size) { return Err(LinuxLoadError::InvalidElf("Rela size not aligned")); } let user = UserMemoryAccess::new(table); - let count = info.rela_size / entry_size; + let count = rela_size / entry_size; for idx in 0..count { let entry_addr = rela_addr .checked_add(idx * entry_size) @@ -117,32 +259,214 @@ pub fn apply_relocations( .checked_add(16) .ok_or(LinuxLoadError::SizeOverflow)?, )? as i64; - + let sym = (r_info >> 32) as u32; let reloc_type = (r_info & 0xffff_ffff) as u32; - if reloc_type != R_X86_64_RELATIVE { - return Err(LinuxLoadError::UnsupportedRelocation(reloc_type)); - } + let raw_offset = usize::try_from(r_offset).map_err(|_| LinuxLoadError::SizeOverflow)?; + let target = resolve_reloc_target(base, raw_offset, segments)?; + let value_raw = resolve_symbol_reloc(base, reloc_type, sym, r_addend, segments)?; + user.write_u64(target, value_raw)?; + } - let target = base - .checked_add(usize::try_from(r_offset).map_err(|_| LinuxLoadError::SizeOverflow)?) + Ok(()) +} + +fn apply_rel_table( + table: &T, + base: VirtAddr, + rel_addr: VirtAddr, + rel_size: usize, + rel_ent: usize, + segments: &[MappedSegment], + _label: &str, +) -> Result<(), LinuxLoadError> { + if rel_size == 0 { + return Ok(()); + } + let entry_size = core::mem::size_of::(); + if rel_ent != entry_size { + return Err(LinuxLoadError::InvalidElf("unexpected Rel entry size")); + } + if !rel_size.is_multiple_of(entry_size) { + return Err(LinuxLoadError::InvalidElf("Rel size not aligned")); + } + + let user = UserMemoryAccess::new(table); + let count = rel_size / entry_size; + for idx in 0..count { + let entry_addr = rel_addr + .checked_add(idx * entry_size) .ok_or(LinuxLoadError::SizeOverflow)?; - if !is_mapped(target, segments) { - return Err(LinuxLoadError::RelocationTargetOutOfRange); - } + let r_offset = user.read_u64(entry_addr)?; + let r_info = user.read_u64( + entry_addr + .checked_add(8) + .ok_or(LinuxLoadError::SizeOverflow)?, + )?; + let sym = (r_info >> 32) as u32; + let reloc_type = (r_info & 0xffff_ffff) as u32; + + let raw_offset = usize::try_from(r_offset).map_err(|_| LinuxLoadError::SizeOverflow)?; + let target = resolve_reloc_target(base, raw_offset, segments)?; + let addend = user_read_u64(table, target)? as i64; + let value = resolve_symbol_reloc(base, reloc_type, sym, addend, segments)?; + user.write_u64(target, value)?; + } + Ok(()) +} - let base_raw = base.as_raw() as i64; - let value_raw = base_raw - .checked_add(r_addend) +fn apply_relr( + table: &T, + base: VirtAddr, + info: &DynamicInfo, + segments: &[MappedSegment], +) -> Result<(), LinuxLoadError> { + let relr_addr = match info.relr_addr { + Some(addr) => addr, + None => return Ok(()), + }; + if info.relr_size == 0 { + return Ok(()); + } + let entry_size = core::mem::size_of::(); + if info.relr_ent != 0 && info.relr_ent != entry_size { + return Err(LinuxLoadError::InvalidElf("unexpected RELR entry size")); + } + if !info.relr_size.is_multiple_of(entry_size) { + return Err(LinuxLoadError::InvalidElf("RELR size not aligned")); + } + + let user = UserMemoryAccess::new(table); + let count = info.relr_size / entry_size; + let mut base_offset = 0usize; + for idx in 0..count { + let entry_addr = relr_addr + .checked_add(idx * entry_size) .ok_or(LinuxLoadError::SizeOverflow)?; - if value_raw < 0 { - return Err(LinuxLoadError::RelocationTargetOutOfRange); + let entry = user.read_u64(entry_addr)?; + if entry & 1 == 0 { + base_offset = usize::try_from(entry).map_err(|_| LinuxLoadError::SizeOverflow)?; + apply_relative_at(table, base, base_offset, segments)?; + base_offset = base_offset + .checked_add(entry_size) + .ok_or(LinuxLoadError::SizeOverflow)?; + } else { + let mut bitmap = entry >> 1; + for bit in 0..(u64::BITS - 1) { + if (bitmap & 1) != 0 { + let rel = base_offset + .checked_add(bit as usize * entry_size) + .ok_or(LinuxLoadError::SizeOverflow)?; + apply_relative_at(table, base, rel, segments)?; + } + bitmap >>= 1; + } + base_offset = base_offset + .checked_add((u64::BITS as usize - 1) * entry_size) + .ok_or(LinuxLoadError::SizeOverflow)?; } - user.write_u64(target, value_raw as u64)?; } + Ok(()) +} +fn apply_relative_at( + table: &T, + base: VirtAddr, + rel: usize, + segments: &[MappedSegment], +) -> Result<(), LinuxLoadError> { + let target = resolve_reloc_target(base, rel, segments)?; + let addend = user_read_u64(table, target)? as i64; + let value_raw = resolve_relative_value(base, addend, segments)?; + let user = UserMemoryAccess::new(table); + user.write_u64(target, value_raw)?; Ok(()) } +fn user_read_u64( + table: &T, + addr: VirtAddr, +) -> Result { + let user = UserMemoryAccess::new(table); + user.read_u64(addr).map_err(LinuxLoadError::from) +} + +fn resolve_reloc_target( + base: VirtAddr, + raw: usize, + segments: &[MappedSegment], +) -> Result { + let base_target = base + .checked_add(raw) + .ok_or(LinuxLoadError::SizeOverflow)?; + if is_mapped(base_target, segments) { + return Ok(base_target); + } + let abs_target = VirtAddr::new(raw); + if is_mapped(abs_target, segments) { + return Ok(abs_target); + } + Err(LinuxLoadError::RelocationTargetOutOfRange) +} + +/// Resolve dynamic pointers that may be encoded as base-relative or absolute. +fn resolve_dynamic_ptr( + table: &T, + base: VirtAddr, + raw: usize, +) -> Result { + let base_addr = add_base(base, VirtAddr::new(raw))?; + if table.translate(base_addr).is_ok() { + return Ok(base_addr); + } + let abs_addr = VirtAddr::new(raw); + if table.translate(abs_addr).is_ok() { + return Ok(abs_addr); + } + Ok(base_addr) +} + +fn resolve_relative_value( + base: VirtAddr, + addend: i64, + segments: &[MappedSegment], +) -> Result { + if addend < 0 { + return Err(LinuxLoadError::RelocationTargetOutOfRange); + } + let addend_usize = addend as usize; + let base_value = base + .as_raw() + .checked_add(addend_usize) + .ok_or(LinuxLoadError::SizeOverflow)?; + let base_addr = VirtAddr::new(base_value); + if is_mapped(base_addr, segments) { + return Ok(base_value as u64); + } + let abs_addr = VirtAddr::new(addend_usize); + if is_mapped(abs_addr, segments) { + return Ok(addend_usize as u64); + } + Ok(base_value as u64) +} + +fn resolve_symbol_reloc( + base: VirtAddr, + reloc_type: u32, + sym: u32, + addend: i64, + segments: &[MappedSegment], +) -> Result { + if sym != 0 { + return Err(LinuxLoadError::UnsupportedRelocation(reloc_type)); + } + match reloc_type { + R_X86_64_RELATIVE | R_X86_64_64 | R_X86_64_GLOB_DAT | R_X86_64_JUMP_SLOT => { + resolve_relative_value(base, addend, segments) + } + _ => Err(LinuxLoadError::UnsupportedRelocation(reloc_type)), + } +} + pub fn apply_gnu_relro( table: &mut T, base: VirtAddr, @@ -161,22 +485,12 @@ pub fn apply_gnu_relro( let start = align_down(relro_start.as_raw(), page_size); let end = align_up(relro_end.as_raw(), page_size); - println!( - "[loader] GNU_RELRO {:#x}-{:#x}", - relro_start.as_raw(), - relro_end.as_raw() - ); - for addr in (start..end).step_by(page_size) { let page_start = addr; let page_end = addr .checked_add(page_size) .ok_or(LinuxLoadError::SizeOverflow)?; if relro_start.as_raw() > page_start || relro_end.as_raw() < page_end { - println!( - "[loader] relro page {:#x}-{:#x} skipped (partial coverage)", - page_start, page_end - ); continue; } let page = Page::new(VirtAddr::new(addr), PageSize(page_size)); @@ -220,3 +534,9 @@ struct Elf64Rela { _info: u64, _addend: i64, } + +#[repr(C)] +struct Elf64Rel { + _offset: u64, + _info: u64, +} diff --git a/kernel/src/loader/linux/stack.rs b/kernel/src/loader/linux/stack.rs index 520484d..5ab8e4e 100644 --- a/kernel/src/loader/linux/stack.rs +++ b/kernel/src/loader/linux/stack.rs @@ -5,6 +5,10 @@ use crate::mem::addr::{Addr, VirtAddr, align_down}; use crate::mem::paging::PageTableOps; use crate::mem::user::{UserAccessError, UserMemoryAccess}; +use super::AT_RANDOM; + +const AT_RANDOM_LEN: usize = 16; + #[derive(Clone, Copy)] pub struct AuxvEntry { pub key: u64, @@ -68,10 +72,28 @@ pub fn initialise_stack_with_args( sp = align_down(sp, 16); - // auxv (terminate with AT_NULL) + // TODO: Replace zeroed bytes with a kernel RNG once available. + let random_ptr = { + sp = sp.checked_sub(AT_RANDOM_LEN).ok_or(StackBuildError::Overflow)?; + unsafe { + core::ptr::write_bytes(sp as *mut u8, 0, AT_RANDOM_LEN); + } + sp as u64 + }; + + let mut auxv_entries: Vec = auxv.to_vec(); + if !auxv_entries.iter().any(|entry| entry.key == AT_RANDOM) { + auxv_entries.push(AuxvEntry { + key: AT_RANDOM, + value: random_ptr, + }); + } + + sp = align_down(sp, 16); + push_u64(&mut sp, 0)?; push_u64(&mut sp, 0)?; - for entry in auxv.iter().rev() { + for entry in auxv_entries.iter().rev() { push_u64(&mut sp, entry.value)?; push_u64(&mut sp, entry.key)?; } @@ -117,9 +139,29 @@ pub fn initialise_stack_with_args_in_table( sp = align_down(sp, 16); + // TODO: Replace zeroed bytes with a kernel RNG once available. + let random_ptr = { + sp = sp.checked_sub(AT_RANDOM_LEN).ok_or(StackBuildError::Overflow)?; + let addr = VirtAddr::new(sp); + let zeros = [0u8; AT_RANDOM_LEN]; + user.write_bytes(addr, &zeros) + .map_err(StackBuildError::from)?; + sp as u64 + }; + + let mut auxv_entries: Vec = auxv.to_vec(); + if !auxv_entries.iter().any(|entry| entry.key == AT_RANDOM) { + auxv_entries.push(AuxvEntry { + key: AT_RANDOM, + value: random_ptr, + }); + } + + sp = align_down(sp, 16); + push_u64_in_table(&user, &mut sp, 0)?; push_u64_in_table(&user, &mut sp, 0)?; - for entry in auxv.iter().rev() { + for entry in auxv_entries.iter().rev() { push_u64_in_table(&user, &mut sp, entry.value)?; push_u64_in_table(&user, &mut sp, entry.key)?; } diff --git a/kernel/src/syscall/linux.rs b/kernel/src/syscall/linux.rs index a462230..548b518 100644 --- a/kernel/src/syscall/linux.rs +++ b/kernel/src/syscall/linux.rs @@ -23,7 +23,6 @@ use crate::util::stream::{ControlAccess, ControlError, ControlRequest}; const DEBUG_LS_SYSCALL: bool = true; const DEBUG_LINUX_ENOSYS: bool = true; -const DEBUG_LINUX_EXECVE: bool = true; #[repr(u16)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -55,6 +54,7 @@ pub enum LinuxSyscall { Writev = 20, Uname = 63, GetPid = 39, + GetTimeOfDay = 96, Fcntl = 72, GetCwd = 79, Chdir = 80, @@ -76,6 +76,7 @@ pub enum LinuxSyscall { GetSid = 124, ArchPrctl = 158, SetTidAddress = 218, + ClockGetTime = 228, } impl LinuxSyscall { @@ -98,6 +99,7 @@ impl LinuxSyscall { 20 => Some(Self::Writev), 39 => Some(Self::GetPid), 63 => Some(Self::Uname), + 96 => Some(Self::GetTimeOfDay), 72 => Some(Self::Fcntl), 79 => Some(Self::GetCwd), 80 => Some(Self::Chdir), @@ -119,6 +121,7 @@ impl LinuxSyscall { 124 => Some(Self::GetSid), 158 => Some(Self::ArchPrctl), 218 => Some(Self::SetTidAddress), + 228 => Some(Self::ClockGetTime), _ => None, } } @@ -182,8 +185,10 @@ pub fn dispatch( Some(LinuxSyscall::SetUid) => DispatchResult::Completed(Ok(0)), Some(LinuxSyscall::SetGid) => DispatchResult::Completed(Ok(0)), Some(LinuxSyscall::GetPid) => DispatchResult::Completed(handle_getpid(invocation)), + Some(LinuxSyscall::GetTimeOfDay) => DispatchResult::Completed(handle_gettimeofday(invocation)), Some(LinuxSyscall::GetPgid) => DispatchResult::Completed(handle_getpgid(invocation)), Some(LinuxSyscall::GetSid) => DispatchResult::Completed(handle_getsid(invocation)), + Some(LinuxSyscall::ClockGetTime) => DispatchResult::Completed(handle_clock_gettime(invocation)), Some(LinuxSyscall::Exit) => handle_exit(invocation), Some(LinuxSyscall::ExitGroup) => handle_exit(invocation), None => { @@ -813,10 +818,6 @@ fn handle_execve( let argv_ptr = invocation.arg(1).unwrap_or(0); let envp_ptr = invocation.arg(2).unwrap_or(0); - if DEBUG_LINUX_EXECVE { - crate::println!("[linux-execve] path={path}"); - } - let argv = match process.address_space().with_page_table(|table, _| { let user = UserMemoryAccess::new(table); read_cstring_array_with_user(&user, argv_ptr, 128) @@ -834,17 +835,21 @@ fn handle_execve( // Drop the previous user stack before clearing mappings so we do not unmap the // freshly allocated stack on replacement. - if let Err(err) = SCHEDULER.clear_current_user_stack() { - return DispatchResult::Completed(Err(spawn_error_to_sys(err))); - } + let old_stack = match SCHEDULER.take_current_user_stack() { + Ok(stack) => stack, + Err(err) => { + return DispatchResult::Completed(Err(spawn_error_to_sys(err))); + } + }; if ::clear_user_mappings(&process.address_space()).is_err() { return DispatchResult::Completed(Err(SysError::InvalidArgument)); } + drop(old_stack); let program = match crate::loader::linux::load_elf(pid, &path) { Ok(program) => program, - Err(_) => return DispatchResult::Completed(Err(SysError::InvalidArgument)), + Err(_err) => return DispatchResult::Completed(Err(SysError::InvalidArgument)), }; let auxv = crate::loader::linux::build_auxv(&program, PageSize::SIZE_4K.bytes()); @@ -858,7 +863,7 @@ fn handle_execve( ) }) { Ok(ptr) => ptr, - Err(_) => return DispatchResult::Completed(Err(SysError::InvalidArgument)), + Err(_err) => return DispatchResult::Completed(Err(SysError::InvalidArgument)), }, None => return DispatchResult::Completed(Err(SysError::InvalidArgument)), }; @@ -958,6 +963,65 @@ fn handle_arch_prctl(invocation: &SyscallInvocation) -> SysResult { Ok(0) } +#[repr(C)] +struct LinuxTimeval { + tv_sec: i64, + tv_usec: i64, +} + +impl LinuxTimeval { + fn as_bytes(&self) -> [u8; 16] { + let mut out = [0u8; 16]; + out[..8].copy_from_slice(&self.tv_sec.to_ne_bytes()); + out[8..].copy_from_slice(&self.tv_usec.to_ne_bytes()); + out + } +} + +#[repr(C)] +struct LinuxTimespec { + tv_sec: i64, + tv_nsec: i64, +} + +impl LinuxTimespec { + fn as_bytes(&self) -> [u8; 16] { + let mut out = [0u8; 16]; + out[..8].copy_from_slice(&self.tv_sec.to_ne_bytes()); + out[8..].copy_from_slice(&self.tv_nsec.to_ne_bytes()); + out + } +} + +fn handle_gettimeofday(invocation: &SyscallInvocation) -> SysResult { + let tv_ptr = invocation.arg(0).unwrap_or(0); + if tv_ptr != 0 { + // TODO: Provide real wall-clock time once the time source is implemented. + let tv = LinuxTimeval { + tv_sec: 0, + tv_usec: 0, + }; + let dst = VirtAddr::new(tv_ptr as usize); + copy_to_user(dst, &tv.as_bytes()).map_err(|_| SysError::BadAddress)?; + } + Ok(0) +} + +fn handle_clock_gettime(invocation: &SyscallInvocation) -> SysResult { + let tp_ptr = invocation.arg(1).unwrap_or(0); + if tp_ptr == 0 { + return Err(SysError::InvalidArgument); + } + // TODO: Provide a real clock source once timekeeping is implemented. + let ts = LinuxTimespec { + tv_sec: 0, + tv_nsec: 0, + }; + let dst = VirtAddr::new(tp_ptr as usize); + copy_to_user(dst, &ts.as_bytes()).map_err(|_| SysError::BadAddress)?; + Ok(0) +} + // NOTE: This is a minimal anonymous mmap implementation for busybox startup. // It only supports private, anonymous mappings with optional MAP_FIXED and // ignores file-backed mappings, offsets, and advanced flags. diff --git a/kernel/src/thread/mod.rs b/kernel/src/thread/mod.rs index 6f30a4c..4224ed0 100644 --- a/kernel/src/thread/mod.rs +++ b/kernel/src/thread/mod.rs @@ -14,7 +14,7 @@ use crate::mem::addr::VirtAddr; use crate::process::{PROCESS_TABLE, ProcessError, ProcessHandle, ProcessId}; use crate::syscall; use crate::trap::{CurrentTrapFrame, TrapInfo}; -use crate::util::spinlock::SpinLock; +use crate::util::spinlock::{SpinLock, SpinLockGuard}; pub type ThreadId = u64; pub type KernelThreadEntry = fn() -> !; @@ -29,6 +29,28 @@ const KERNEL_STACK_SIZE: usize = 32 * 1024; const KERNEL_STACK_ALIGN: usize = 16; const USER_STACK_SIZE: usize = 32 * 1024; +struct InterruptGuard { + was_enabled: bool, +} + +impl InterruptGuard { + fn new() -> Self { + let was_enabled = ::are_interrupts_enabled(); + if was_enabled { + ::disable_interrupts(); + } + Self { was_enabled } + } +} + +impl Drop for InterruptGuard { + fn drop(&mut self) { + if self.was_enabled { + ::enable_interrupts(); + } + } +} + /// Lightweight kernel-managed execution unit representing a single thread of execution. pub static SCHEDULER: Scheduler = Scheduler::new(); static SCHEDULER_DISPATCH: SchedulerDispatch = SchedulerDispatch; @@ -54,8 +76,16 @@ impl Scheduler { } } + /// Acquire the scheduler lock with interrupts masked to avoid re-entrancy + /// from interrupt handlers that also touch the scheduler. + fn lock_inner(&self) -> (InterruptGuard, SpinLockGuard<'_, SchedulerInner>) { + let guard = InterruptGuard::new(); + let inner = self.inner.lock(); + (guard, inner) + } + pub fn init(&self) -> Result<(), SchedulerError> { - let mut inner = self.inner.lock(); + let (_guard, mut inner) = self.lock_inner(); if inner.initialised { return Ok(()); } @@ -95,13 +125,13 @@ impl Scheduler { /// Return the process ID of the currently running thread, if any. pub fn current_process_id(&self) -> Option { - let inner = self.inner.lock(); + let (_guard, inner) = self.lock_inner(); let current = inner.current?; inner.thread(current).map(|thread| thread.process_id()) } pub fn current_user_stack_info(&self) -> Option { - let inner = self.inner.lock(); + let (_guard, inner) = self.lock_inner(); let current = inner.current?; inner .thread(current) @@ -114,7 +144,7 @@ impl Scheduler { entry: KernelThreadEntry, ) -> Result { let process = { - let inner = self.inner.lock(); + let (_guard, inner) = self.lock_inner(); if !inner.initialised { return Err(SpawnError::SchedulerNotReady); } @@ -132,7 +162,7 @@ impl Scheduler { name: &'static str, entry: KernelThreadEntry, ) -> Result { - let mut inner = self.inner.lock(); + let (_guard, mut inner) = self.lock_inner(); if !inner.initialised { return Err(SpawnError::SchedulerNotReady); } @@ -147,7 +177,7 @@ impl Scheduler { entry: VirtAddr, stack_size: usize, ) -> Result { - let mut inner = self.inner.lock(); + let (_guard, mut inner) = self.lock_inner(); if !inner.initialised { return Err(SpawnError::SchedulerNotReady); } @@ -163,7 +193,7 @@ impl Scheduler { user_stack: ::UserStack, stack_pointer: VirtAddr, ) -> Result { - let mut inner = self.inner.lock(); + let (_guard, mut inner) = self.lock_inner(); if !inner.initialised { return Err(SpawnError::SchedulerNotReady); } @@ -178,7 +208,7 @@ impl Scheduler { context: ::Context, user_stack: ::UserStack, ) -> Result { - let mut inner = self.inner.lock(); + let (_guard, mut inner) = self.lock_inner(); if !inner.initialised { return Err(SpawnError::SchedulerNotReady); } @@ -190,38 +220,45 @@ impl Scheduler { &self, user_stack: ::UserStack, ) -> Result<(), SpawnError> { - let mut inner = self.inner.lock(); - if !inner.initialised { - return Err(SpawnError::SchedulerNotReady); - } + let old_stack = { + let (_guard, mut inner) = self.lock_inner(); + if !inner.initialised { + return Err(SpawnError::SchedulerNotReady); + } - let current = inner.current.ok_or(SpawnError::SchedulerNotReady)?; - let thread = inner - .thread_mut(current) - .ok_or(SpawnError::SchedulerNotReady)?; - if !thread.is_user() { - return Err(SpawnError::SchedulerNotReady); - } - thread.user_stack = Some(user_stack); + let current = inner.current.ok_or(SpawnError::SchedulerNotReady)?; + let thread = inner + .thread_mut(current) + .ok_or(SpawnError::SchedulerNotReady)?; + if !thread.is_user() { + return Err(SpawnError::SchedulerNotReady); + } + thread.user_stack.replace(user_stack) + }; + drop(old_stack); Ok(()) } - pub fn clear_current_user_stack(&self) -> Result<(), SpawnError> { - let mut inner = self.inner.lock(); - if !inner.initialised { - return Err(SpawnError::SchedulerNotReady); - } + pub fn take_current_user_stack( + &self, + ) -> Result::UserStack>, SpawnError> { + let old_stack = { + let (_guard, mut inner) = self.lock_inner(); + if !inner.initialised { + return Err(SpawnError::SchedulerNotReady); + } - let current = inner.current.ok_or(SpawnError::SchedulerNotReady)?; - let thread = inner - .thread_mut(current) - .ok_or(SpawnError::SchedulerNotReady)?; - if !thread.is_user() { - return Err(SpawnError::SchedulerNotReady); - } + let current = inner.current.ok_or(SpawnError::SchedulerNotReady)?; + let thread = inner + .thread_mut(current) + .ok_or(SpawnError::SchedulerNotReady)?; + if !thread.is_user() { + return Err(SpawnError::SchedulerNotReady); + } - thread.user_stack = None; - Ok(()) + thread.user_stack.take() + }; + Ok(old_stack) } fn spawn_thread_locked( @@ -248,7 +285,7 @@ impl Scheduler { pub fn start(&self) -> Result<(), SchedulerError> { { - let inner = self.inner.lock(); + let (_guard, inner) = self.lock_inner(); if !inner.initialised { return Err(SchedulerError::NotInitialised); } @@ -313,7 +350,7 @@ impl Scheduler { next_process, detach_target, ) = { - let mut inner = self.inner.lock(); + let (_guard, mut inner) = self.lock_inner(); let current_id = match inner.current { Some(id) => id, None => return, From 163157726f53ae36fb2e18ddb2036f61b48d5b2d Mon Sep 17 00:00:00 2001 From: n4mlz Date: Thu, 29 Jan 2026 14:28:45 +0900 Subject: [PATCH 10/25] refactor: enhance trap layer documentation and improve error handling in stubs --- kernel/src/arch/x86_64/trap/DESIGN.md | 10 ++++++---- kernel/src/arch/x86_64/trap/context.rs | 1 + kernel/src/arch/x86_64/trap/gdt.rs | 13 +++++++++++++ kernel/src/arch/x86_64/trap/handlers.rs | 19 +++++++++++-------- kernel/src/arch/x86_64/trap/idt.rs | 2 ++ kernel/src/arch/x86_64/trap/stubs.rs | 9 +++++++++ 6 files changed, 42 insertions(+), 12 deletions(-) diff --git a/kernel/src/arch/x86_64/trap/DESIGN.md b/kernel/src/arch/x86_64/trap/DESIGN.md index 69f9960..f814c9c 100644 --- a/kernel/src/arch/x86_64/trap/DESIGN.md +++ b/kernel/src/arch/x86_64/trap/DESIGN.md @@ -5,15 +5,17 @@ - Own the GDT/TSS, IDT, and the hand-written trap stubs that translate hardware events into the architecture-neutral trap dispatcher. ## Descriptor Tables -- `gdt` installs both ring-0 and ring-3 segment descriptors alongside the TSS entry, allocating three IST stacks (NMI, double-fault, machine-check) from a statically mapped buffer. -- `set_privilege_stack` updates `TSS.rsp0` on every context switch so user→kernel transitions enter on the scheduled thread's kernel stack, while a fallback ring-0 stack remains available for bootstrap paths. +- `gdt` installs both ring-0 and ring-3 segment descriptors alongside the TSS entry, allocating three IST stacks (NMI, double-fault, machine-check) from a statically mapped buffer (single-CPU only until per-CPU storage exists). +- `set_privilege_stack` updates `TSS.rsp0` on every context switch so user→kernel transitions enter on the scheduled thread's kernel stack, while a fallback ring-0 stack remains available for bootstrap paths. Updates must occur with interrupts disabled on the current CPU. - `idt` populates exception vectors with dedicated stubs and assigns IST indices where architectural guidance recommends hardened stacks. - The IDT exposes vector `0x80` with DPL=3, providing an initial software interrupt entry point for user mode before the syscall MSRs are wired up. The vector is registered with the syscall dispatcher so `int 0x80` routes into ABI-aware handling. - `init()` loads both tables during early boot and must run per-CPU prior to enabling interrupts. - The syscall software interrupt (vector `0x80`) is registered in `arch/x86_64/syscall.rs`, which forwards to the generic syscall dispatcher to keep ABI logic architecture-neutral. ## Trap Stubs -- Naked assembly routines in `stubs` save general-purpose registers, normalise error codes, maintain stack alignment for `call`, and end with `iretq`. +- Naked assembly routines in `stubs` clear DF, save general-purpose registers, normalise error codes, maintain stack alignment for `call`, and end with `iretq`. +- The exception table must match the architectural error-code list; a mismatch corrupts the stack frame. +- The kernel is built with red-zone disabled, and SIMD/FPU usage in kernel mode is currently prohibited until save/restore support is added. - Each stub invokes `dispatch_trap`, which constructs a `TrapInfo` (vector, origin, description) and hands control to `ArchTrap::dispatch_trap` so the architecture layer can route through the generic trap handler without hard-wiring the entry point. - Timer interrupts reuse the same mechanism, so the scheduler observes consistent metadata regardless of source. @@ -27,7 +29,7 @@ - `thread::Context` derives from a trap frame after interrupts, allowing seamless handoff between interrupt context and scheduled threads. ## Exception Handlers -- `handlers` provides the architecture-specific fast path for #PF/#GP/#DF, decoding hardware error codes and emitting structured diagnostics before panicking. +- `handlers` provides the architecture-specific fast path for #PF/#GP/#DF, decoding hardware error codes and emitting structured diagnostics before panicking. Double-fault handling avoids logging and halts to reduce re-entrancy risk. - The dispatcher in `mod.rs` delegates to these helpers via `ArchTrap::handle_exception`; returning `true` suppresses the generic logging path. - Page-fault handling records the CR2 fault address and access type bits so future user-mode recovery logic has the required context. diff --git a/kernel/src/arch/x86_64/trap/context.rs b/kernel/src/arch/x86_64/trap/context.rs index 119f10e..051aeef 100644 --- a/kernel/src/arch/x86_64/trap/context.rs +++ b/kernel/src/arch/x86_64/trap/context.rs @@ -79,6 +79,7 @@ impl TrapFrame { impl TrapFrameTrait for TrapFrame { fn error_code(&self) -> Option { + // The stubs push a placeholder even for no-error exceptions, so `0` means "no error". Some(self.error_code) } } diff --git a/kernel/src/arch/x86_64/trap/gdt.rs b/kernel/src/arch/x86_64/trap/gdt.rs index a9a6464..b5f31d7 100644 --- a/kernel/src/arch/x86_64/trap/gdt.rs +++ b/kernel/src/arch/x86_64/trap/gdt.rs @@ -20,11 +20,13 @@ const PRIVILEGE_STACK_SIZE: usize = 32 * 1024; #[repr(align(16))] struct IstStackArea([u8; IST_STACK_SIZE * IST_STACK_COUNT]); +// NOTE: These stacks are global, so SMP will require per-CPU allocations. static mut IST_STACKS: IstStackArea = IstStackArea([0u8; IST_STACK_SIZE * IST_STACK_COUNT]); #[repr(align(16))] struct PrivilegeStack([u8; PRIVILEGE_STACK_SIZE]); +// NOTE: This is a single fallback ring-0 stack; SMP must provide per-CPU stacks. static mut PRIV_STACK: PrivilegeStack = PrivilegeStack([0u8; PRIVILEGE_STACK_SIZE]); pub(crate) struct GdtSelectors { @@ -49,6 +51,11 @@ static GDT: LazyLock = LazyLock::new_const(build_gdt); /// /// Must be called on each CPU before enabling interrupts so that IST stacks and /// privilege transitions reference valid descriptors. +/// +/// # SMP limitation +/// +/// The current implementation uses global IST/privilege stacks. This is only +/// safe for a single CPU and must be replaced with per-CPU storage. pub(super) fn load() { let (gdt, selectors) = &*GDT; gdt.load(); @@ -65,6 +72,12 @@ pub(crate) fn selectors() -> &'static GdtSelectors { selectors } +/// Updates the ring-0 stack used on CPL3->CPL0 transitions. +/// +/// # Safety contract +/// +/// Must be called for the current CPU while its interrupts are disabled, and +/// on SMP this must update the per-CPU TSS instance instead of the global one. pub(crate) fn set_privilege_stack(stack_top: KernelVirtAddr) { let mut tss = TSS.lock(); let top = VirtAddr::new(stack_top.as_raw() as u64); diff --git a/kernel/src/arch/x86_64/trap/handlers.rs b/kernel/src/arch/x86_64/trap/handlers.rs index 5dfd75e..0017377 100644 --- a/kernel/src/arch/x86_64/trap/handlers.rs +++ b/kernel/src/arch/x86_64/trap/handlers.rs @@ -1,3 +1,4 @@ +use x86_64::instructions::interrupts; use x86_64::registers::control::Cr2; use crate::println; @@ -8,6 +9,8 @@ use crate::trap::TrapInfo; use super::TrapFrame; pub fn handle_exception(info: TrapInfo, frame: &mut TrapFrame) -> bool { + // Be careful with logging/locking here: traps can occur while locks are held or + // interrupts are disabled, and double faults must avoid re-entrancy entirely. match info.vector { 14 => { handle_page_fault(frame) @@ -20,10 +23,7 @@ pub fn handle_exception(info: TrapInfo, frame: &mut TrapFrame) -> bool { handle_general_protection(frame); true } - 8 => { - handle_double_fault(frame); - true - } + 8 => handle_double_fault(frame), _ => false, } } @@ -73,10 +73,13 @@ fn handle_invalid_opcode(frame: &TrapFrame) { panic!("invalid opcode"); } -fn handle_double_fault(frame: &TrapFrame) { - println!("[#DF] double fault encountered"); - println!("[#DF] frame={:#?}", frame); - panic!("double fault"); +fn handle_double_fault(_frame: &TrapFrame) -> bool { + // Double faults are fatal; avoid logging/locking to reduce the chance of + // re-faulting and triggering a triple fault. + interrupts::disable(); + loop { + x86_64::instructions::hlt(); + } } #[cfg(test)] diff --git a/kernel/src/arch/x86_64/trap/idt.rs b/kernel/src/arch/x86_64/trap/idt.rs index 331da11..154f081 100644 --- a/kernel/src/arch/x86_64/trap/idt.rs +++ b/kernel/src/arch/x86_64/trap/idt.rs @@ -76,6 +76,8 @@ fn build_idt() -> InterruptDescriptorTable { idt[super::SYSCALL_VECTOR] .set_handler_addr(VirtAddr::from_ptr(software_interrupt_syscall as *const ())) .set_privilege_level(PrivilegeLevel::Ring3); + // External interrupt handlers must issue EOI to PIC/APIC in their + // dispatch path; the stubs themselves do not handle it. for (offset, stub) in EXTERNAL_INTERRUPT_STUBS.iter().enumerate() { let vector = crate::interrupt::DEVICE_VECTOR_BASE + offset as u8; idt[vector].set_handler_addr(VirtAddr::from_ptr(*stub as *const ())); diff --git a/kernel/src/arch/x86_64/trap/stubs.rs b/kernel/src/arch/x86_64/trap/stubs.rs index 8b614b3..568ac3c 100644 --- a/kernel/src/arch/x86_64/trap/stubs.rs +++ b/kernel/src/arch/x86_64/trap/stubs.rs @@ -2,12 +2,18 @@ use super::build_trap_info; use super::context::{ORIGINAL_ERROR_OFFSET, TrapFrame}; use crate::arch::{Arch, api::ArchTrap}; +// Trap entry stubs assume: +// - Direction flag is cleared on entry (`cld`) before any Rust code runs. +// - The kernel is built with red-zone disabled (SysV ABI requires `-mno-red-zone`). +// - SIMD/FPU registers are not touched in the kernel until proper save/restore exists. + macro_rules! define_trap_stub_no_error { ($name:ident, $vector:expr) => { #[unsafe(naked)] pub(super) unsafe extern "C" fn $name() -> ! { core::arch::naked_asm!( r#" + cld push r15 push r14 push r13 @@ -75,6 +81,7 @@ macro_rules! define_trap_stub_with_error { pub(super) unsafe extern "C" fn $name() -> ! { core::arch::naked_asm!( r#" + cld push r15 push r14 push r13 @@ -142,6 +149,8 @@ macro_rules! define_trap_stub_with_error { }; } +// NOTE: Exception vectors with error codes are fixed by the CPU specification. +// A mismatch here will corrupt the stack frame and usually triple-fault on `iretq`. define_trap_stub_no_error!(exception_0, 0); define_trap_stub_no_error!(exception_1, 1); define_trap_stub_no_error!(exception_2, 2); From a75a165bbb8e7e9251cf1b86a877d9ff53731dd0 Mon Sep 17 00:00:00 2001 From: n4mlz Date: Thu, 29 Jan 2026 14:38:52 +0900 Subject: [PATCH 11/25] refactor: improve ELF loader documentation and enhance stack initialization for test binaries --- kernel/src/loader/DESIGN.md | 12 ++++++------ kernel/src/loader/linux/map.rs | 16 ++++++++++------ kernel/src/loader/linux/mod.rs | 4 ++++ kernel/src/loader/linux/patch.rs | 8 +++++++- kernel/src/loader/linux/reloc.rs | 23 ++++++++--------------- kernel/src/loader/linux/stack.rs | 1 + 6 files changed, 36 insertions(+), 28 deletions(-) diff --git a/kernel/src/loader/DESIGN.md b/kernel/src/loader/DESIGN.md index 2c488e0..a9dd4bf 100644 --- a/kernel/src/loader/DESIGN.md +++ b/kernel/src/loader/DESIGN.md @@ -6,15 +6,15 @@ ## Linux ELF Loader - Supports ELF64, little-endian, `ET_EXEC` and `ET_DYN` (PIE), `EM_X86_64`, and `PT_LOAD` segments. -- Parses `PT_DYNAMIC` for relocation metadata and applies `R_X86_64_RELATIVE` relocations; full dynamic linking remains out of scope. -- Applies `GNU_RELRO` by dropping write permissions after relocations. +- Parses `PT_DYNAMIC` for relocation metadata and applies only `R_X86_64_RELATIVE`/`RELR` relocations; symbol resolution (`GLOB_DAT`, `JUMP_SLOT`, etc.) is intentionally rejected until a linker is implemented. +- Applies `GNU_RELRO` by dropping write permissions after relocations, rounding to page boundaries (over-protecting is acceptable for now). - Parsing, mapping, syscall patching, and stack construction are separated into dedicated loader submodules (`elf`, `map`, `patch`, `stack`) for easier evolution. - Maps each loadable segment into the target process address space with `USER` permissions derived from ELF `p_flags` (R/W/X). Backing frames are freshly allocated via the global frame allocator. - Copies file-backed bytes to the mapped region and zero-fills the remaining `p_memsz - p_filesz` portion for `.bss`. -- Rewrites `syscall` instructions (`0x0f 0x05`) in executable segments into `int 0x80` so we can reuse the existing software-interrupt path until proper `SYSCALL/SYSRET` MSR plumbing is added. -- Before mapping a new image, any existing mappings in the target segment range are unmapped and frames are returned to the allocator. This allows repeated loads in the shared kernel address space without colliding at fixed ELF virtual addresses. -- Builds a minimal SysV-style stack (when requested): `argc=0`, `argv[0]=NULL`, `envp[0]=NULL`, `AT_NULL` terminator. The stack pointer is 16-byte aligned before pushing. -- When the caller supplies argv/envp, the loader can also populate `AT_PAGESZ`, `AT_PHDR`, `AT_PHENT`, `AT_PHNUM`, and `AT_ENTRY` to support PIE and libc expectations. +- Rewrites `syscall` instructions (`0x0f 0x05`) in executable segments into `int 0x80` so we can reuse the existing software-interrupt path until proper `SYSCALL/SYSRET` MSR plumbing is added. The scan is heuristic (no instruction decoding), so it may yield false positives/negatives. +- Before mapping a new image, any existing mappings in the target segment range are unmapped and frames are returned to the allocator. This assumes the process address space is fresh and that the loader owns those ranges. +- Builds a minimal stack that is not fully Linux-ABI compliant; it exists only to bring up simple test binaries. Full ABI stacks are constructed by the argv/envp helpers. +- When the caller supplies argv/envp, the loader can also populate `AT_PAGESZ`, `AT_PHDR`, `AT_PHENT`, `AT_PHNUM`, and `AT_ENTRY` to support PIE and libc expectations. Additional auxv entries are still missing for full libc compatibility. - Segments are initially mapped writable to populate contents, then write permission is dropped if the ELF flags omit it so CR0.WP=1 でもロード時に落ちない。 - The loader copies bytes by translating target virtual addresses to physical frames and writing through the physical mapper, so it no longer depends on the target address space being active. diff --git a/kernel/src/loader/linux/map.rs b/kernel/src/loader/linux/map.rs index 989fff3..2fb2c75 100644 --- a/kernel/src/loader/linux/map.rs +++ b/kernel/src/loader/linux/map.rs @@ -76,6 +76,8 @@ fn map_single_segment seg.file_size { @@ -103,7 +105,7 @@ fn map_single_segment 0 { @@ -122,6 +124,7 @@ fn copy_into_mapped( table: &T, dst: VirtAddr, src: &[u8], + page_size: usize, ) -> Result<(), LinuxLoadError> { let mapper = manager::phys_mapper(); let mut offset = 0usize; @@ -132,8 +135,8 @@ fn copy_into_mapped( .ok_or(LinuxLoadError::SizeOverflow)?; let virt = VirtAddr::new(addr); let phys = table.translate(virt).map_err(LinuxLoadError::from)?; - let page_offset = addr % PageSize::SIZE_4K.bytes(); - let len = (PageSize::SIZE_4K.bytes() - page_offset).min(src.len() - offset); + let page_offset = addr % page_size; + let len = (page_size - page_offset).min(src.len() - offset); unsafe { let ptr = mapper.phys_to_virt(phys); core::ptr::copy_nonoverlapping(src[offset..].as_ptr(), ptr.into_mut_ptr(), len); @@ -147,6 +150,7 @@ fn zero_mapped( table: &T, dst: VirtAddr, len: usize, + page_size: usize, ) -> Result<(), LinuxLoadError> { let mapper = manager::phys_mapper(); let mut offset = 0usize; @@ -157,8 +161,8 @@ fn zero_mapped( .ok_or(LinuxLoadError::SizeOverflow)?; let virt = VirtAddr::new(addr); let phys = table.translate(virt).map_err(LinuxLoadError::from)?; - let page_offset = addr % PageSize::SIZE_4K.bytes(); - let chunk = (PageSize::SIZE_4K.bytes() - page_offset).min(len - offset); + let page_offset = addr % page_size; + let chunk = (page_size - page_offset).min(len - offset); unsafe { let ptr = mapper.phys_to_virt(phys); core::ptr::write_bytes(ptr.into_mut_ptr(), 0, chunk); diff --git a/kernel/src/loader/linux/mod.rs b/kernel/src/loader/linux/mod.rs index 81dc6de..92ddcae 100644 --- a/kernel/src/loader/linux/mod.rs +++ b/kernel/src/loader/linux/mod.rs @@ -112,6 +112,8 @@ where .address_space(pid) .ok_or(LinuxLoadError::Process(ProcessError::NotFound))?; + // NOTE: The loader does not validate user address ranges; it assumes a fresh + // address space (or equivalent) before mapping the ELF image. let mapped = map::map_segments::

(&space, &elf, &elf_bytes, load_bias)?; if let Some(dynamic) = elf.dynamic.as_ref() { space.with_page_table(|table, _| { @@ -223,6 +225,8 @@ fn compute_phdr_address( Ok(phdr) } +/// Build a minimal auxv set sufficient for static/PIE test binaries. +/// Dynamic runtimes may require additional entries (AT_BASE/AT_RANDOM/etc.). pub fn build_auxv(program: &LinuxProgram, page_size: usize) -> Vec { alloc::vec![ AuxvEntry { diff --git a/kernel/src/loader/linux/patch.rs b/kernel/src/loader/linux/patch.rs index 0603649..3798491 100644 --- a/kernel/src/loader/linux/patch.rs +++ b/kernel/src/loader/linux/patch.rs @@ -4,6 +4,9 @@ use crate::mem::paging::{PageTableOps, PhysMapper, TranslationError}; /// Translate Linux `syscall` instructions to `int 0x80` so we can reuse the existing software /// interrupt handler until `SYSCALL/SYSRET` is wired up. +/// +/// This is a heuristic scan that may produce false positives/negatives because it does not +/// decode instruction boundaries. It is intended as a temporary compatibility workaround. pub fn rewrite_syscalls_in_table( base: VirtAddr, size: usize, @@ -12,7 +15,10 @@ pub fn rewrite_syscalls_in_table( let mapper = manager::phys_mapper(); let mut offset = 0usize; while offset + 1 < size { - let addr = base.as_raw() + offset; + let addr = match base.as_raw().checked_add(offset) { + Some(addr) => addr, + None => return Err(TranslationError::NotMapped), + }; let virt = VirtAddr::new(addr); let phys = table.translate(virt)?; let ptr = unsafe { mapper.phys_to_virt(phys).into_mut_ptr() }; diff --git a/kernel/src/loader/linux/reloc.rs b/kernel/src/loader/linux/reloc.rs index 19c21ff..e4d7cf1 100644 --- a/kernel/src/loader/linux/reloc.rs +++ b/kernel/src/loader/linux/reloc.rs @@ -21,9 +21,6 @@ const DT_RELRSZ: i64 = 35; const DT_RELR: i64 = 36; const DT_RELRENT: i64 = 37; -const R_X86_64_64: u32 = 1; -const R_X86_64_GLOB_DAT: u32 = 6; -const R_X86_64_JUMP_SLOT: u32 = 7; const R_X86_64_RELATIVE: u32 = 8; @@ -422,7 +419,7 @@ fn resolve_dynamic_ptr( if table.translate(abs_addr).is_ok() { return Ok(abs_addr); } - Ok(base_addr) + Err(LinuxLoadError::InvalidElf("dynamic pointer out of range")) } fn resolve_relative_value( @@ -456,13 +453,12 @@ fn resolve_symbol_reloc( addend: i64, segments: &[MappedSegment], ) -> Result { + // Symbol resolution is not implemented yet; only RELATIVE/RELR are supported. if sym != 0 { return Err(LinuxLoadError::UnsupportedRelocation(reloc_type)); } match reloc_type { - R_X86_64_RELATIVE | R_X86_64_64 | R_X86_64_GLOB_DAT | R_X86_64_JUMP_SLOT => { - resolve_relative_value(base, addend, segments) - } + R_X86_64_RELATIVE => resolve_relative_value(base, addend, segments), _ => Err(LinuxLoadError::UnsupportedRelocation(reloc_type)), } } @@ -481,18 +477,15 @@ pub fn apply_gnu_relro( let relro_end = relro_start .checked_add(relro.mem_size) .ok_or(LinuxLoadError::SizeOverflow)?; - let page_size = PageSize::SIZE_4K.bytes(); + let page_size = segments + .first() + .map(|seg| seg.page_size) + .unwrap_or(PageSize::SIZE_4K.bytes()); let start = align_down(relro_start.as_raw(), page_size); let end = align_up(relro_end.as_raw(), page_size); + // Apply RELRO at page granularity: any overlapping page becomes read-only. for addr in (start..end).step_by(page_size) { - let page_start = addr; - let page_end = addr - .checked_add(page_size) - .ok_or(LinuxLoadError::SizeOverflow)?; - if relro_start.as_raw() > page_start || relro_end.as_raw() < page_end { - continue; - } let page = Page::new(VirtAddr::new(addr), PageSize(page_size)); let perms = segment_perms_for(VirtAddr::new(addr), segments) .ok_or(LinuxLoadError::RelocationTargetOutOfRange)?; diff --git a/kernel/src/loader/linux/stack.rs b/kernel/src/loader/linux/stack.rs index 5ab8e4e..d254722 100644 --- a/kernel/src/loader/linux/stack.rs +++ b/kernel/src/loader/linux/stack.rs @@ -22,6 +22,7 @@ pub enum StackBuildError { UnsupportedPageSize, } +/// Build a minimal stack that is not Linux ABI-complete; intended for simple tests only. pub fn initialise_minimal_stack( table: &T, stack_top: VirtAddr, From 0036e4055f0bfc9eac2923737dd895596a84c783 Mon Sep 17 00:00:00 2001 From: n4mlz Date: Thu, 29 Jan 2026 14:49:56 +0900 Subject: [PATCH 12/25] refactor: enhance documentation for syscall handling and improve error mapping --- kernel/src/syscall/linux.rs | 56 ++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/kernel/src/syscall/linux.rs b/kernel/src/syscall/linux.rs index 548b518..61cfde4 100644 --- a/kernel/src/syscall/linux.rs +++ b/kernel/src/syscall/linux.rs @@ -24,6 +24,10 @@ use crate::util::stream::{ControlAccess, ControlError, ControlRequest}; const DEBUG_LS_SYSCALL: bool = true; const DEBUG_LINUX_ENOSYS: bool = true; +// NOTE: Error mapping is intentionally coarse right now (many failures collapse +// to InvalidArgument/BadAddress). This keeps the syscall surface minimal but is +// not Linux-accurate. + #[repr(u16)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum LinuxErrno { @@ -128,6 +132,9 @@ impl LinuxSyscall { } /// Minimal Linux syscall table supporting write/getpid/exit placeholders. +/// +/// NOTE: `openat` and related syscalls are not implemented yet; userland that +/// relies on them will see ENOSYS. pub fn dispatch( invocation: &SyscallInvocation, frame: Option<&mut CurrentTrapFrame>, @@ -174,7 +181,8 @@ pub fn dispatch( Some(LinuxSyscall::Wait4) => DispatchResult::Completed(handle_wait4(invocation)), Some(LinuxSyscall::ArchPrctl) => DispatchResult::Completed(handle_arch_prctl(invocation)), Some(LinuxSyscall::Ioctl) => DispatchResult::Completed(handle_ioctl(invocation)), - // These syscalls are currently stubbed and always report success (0). + // These syscalls are stubbed and always report success (0). This is risky + // because userland may rely on signals/TID state that is not tracked yet. Some(LinuxSyscall::RtSigaction) => DispatchResult::Completed(Ok(0)), Some(LinuxSyscall::RtSigprocmask) => DispatchResult::Completed(Ok(0)), Some(LinuxSyscall::SetTidAddress) => DispatchResult::Completed(Ok(0)), @@ -185,10 +193,14 @@ pub fn dispatch( Some(LinuxSyscall::SetUid) => DispatchResult::Completed(Ok(0)), Some(LinuxSyscall::SetGid) => DispatchResult::Completed(Ok(0)), Some(LinuxSyscall::GetPid) => DispatchResult::Completed(handle_getpid(invocation)), - Some(LinuxSyscall::GetTimeOfDay) => DispatchResult::Completed(handle_gettimeofday(invocation)), + Some(LinuxSyscall::GetTimeOfDay) => { + DispatchResult::Completed(handle_gettimeofday(invocation)) + } Some(LinuxSyscall::GetPgid) => DispatchResult::Completed(handle_getpgid(invocation)), Some(LinuxSyscall::GetSid) => DispatchResult::Completed(handle_getsid(invocation)), - Some(LinuxSyscall::ClockGetTime) => DispatchResult::Completed(handle_clock_gettime(invocation)), + Some(LinuxSyscall::ClockGetTime) => { + DispatchResult::Completed(handle_clock_gettime(invocation)) + } Some(LinuxSyscall::Exit) => handle_exit(invocation), Some(LinuxSyscall::ExitGroup) => handle_exit(invocation), None => { @@ -544,13 +556,14 @@ fn handle_getcwd(invocation: &SyscallInvocation) -> SysResult { let text = cwd.to_string(); let bytes = text.as_bytes(); if bytes.len().saturating_add(1) > size { + // NOTE: Linux returns ERANGE here; we currently map to InvalidArgument. return Err(SysError::InvalidArgument); } let user_ptr = VirtAddr::new(buf_ptr as usize); copy_to_user(user_ptr, bytes).map_err(|_| SysError::InvalidArgument)?; let nul = VirtAddr::new(user_ptr.as_raw() + bytes.len()); copy_to_user(nul, &[0]).map_err(|_| SysError::InvalidArgument)?; - Ok(buf_ptr) + Ok((bytes.len() + 1) as u64) } fn handle_chdir(invocation: &SyscallInvocation) -> SysResult { @@ -615,15 +628,18 @@ fn handle_poll(invocation: &SyscallInvocation) -> SysResult { return Ok(ready as u64); } // NOTE: Timeout handling is intentionally simplified; any non-zero timeout blocks - // until an event arrives. We spin here instead of halting because syscalls may run - // with interrupts disabled, making `halt` non-resumable. + // until an event arrives. Poll semantics are coarse (TTY readiness only, others are + // treated as immediately readable). + // + // Syscalls may run with interrupts disabled; enable them so the scheduler can + // make progress while we wait. + INTERRUPTS.enable(); core::hint::spin_loop(); } } fn handle_brk(invocation: &SyscallInvocation) -> SysResult { - // TODO: This is a grow-only brk; shrinking does not unmap pages and no heap upper bound - // is enforced yet. The behavior is enough for busybox's basic allocator path. + // TODO: This is a minimal brk: no heap upper bound is enforced and errors are coarse. let requested = invocation.arg(0).unwrap_or(0); let pid = current_pid()?; let process = PROCESS_TABLE @@ -672,12 +688,10 @@ fn handle_brk(invocation: &SyscallInvocation) -> SysResult { return Err(SysError::InvalidArgument); } } + let phys = table.translate(page.start).map_err(|_| SysError::InvalidArgument)?; + let mapper = manager::phys_mapper(); unsafe { - core::ptr::write_bytes( - VirtAddr::new(addr).into_mut_ptr(), - 0, - PageSize::SIZE_4K.bytes(), - ); + core::ptr::write_bytes(mapper.phys_to_virt(phys).into_mut_ptr(), 0, page_size); } } Ok::<_, SysError>(()) @@ -772,7 +786,10 @@ fn handle_fork( let child_top = ::user_stack_top(&child_stack); let parent_rsp = frame.rsp as usize; - let offset = parent_rsp.saturating_sub(parent_stack.base.as_raw()); + if parent_rsp < parent_stack.base.as_raw() { + return DispatchResult::Completed(Err(SysError::InvalidArgument)); + } + let offset = parent_rsp - parent_stack.base.as_raw(); let child_rsp = child_base.checked_add(offset).unwrap_or(child_top).as_raw() as u64; let mut ctx = ::save_context(frame); @@ -818,6 +835,7 @@ fn handle_execve( let argv_ptr = invocation.arg(1).unwrap_or(0); let envp_ptr = invocation.arg(2).unwrap_or(0); + // NOTE: Limit argv/envp length to avoid unbounded user input scans. let argv = match process.address_space().with_page_table(|table, _| { let user = UserMemoryAccess::new(table); read_cstring_array_with_user(&user, argv_ptr, 128) @@ -959,6 +977,7 @@ fn handle_arch_prctl(invocation: &SyscallInvocation) -> SysResult { return Err(SysError::InvalidArgument); } + // NOTE: This assumes FS base is part of the thread context and restored on context switches. crate::arch::x86_64::set_fs_base(value); Ok(0) } @@ -1071,6 +1090,8 @@ fn handle_mmap(invocation: &SyscallInvocation) -> SysResult { return Err(SysError::InvalidArgument); } } else if target == 0 { + // NOTE: This reuses the brk state as a simple bump allocator; it can + // collide with future brk growth and does not model Linux VMAs. let brk = process.brk_state(); target = align_up(brk.current.as_raw(), page_size); let next = VirtAddr::new(target + len); @@ -1361,6 +1382,8 @@ fn poll_once( pollin: i16, pollnval: i16, ) -> usize { + // NOTE: Non-TTY FDs are treated as immediately readable; this is a + // compatibility shortcut for regular files, not pipes/sockets. let mut wants_input = false; let mut tty_flags = Vec::with_capacity(fds.len()); for fd in fds.iter_mut() { @@ -1524,6 +1547,7 @@ impl LinuxUtsName { struct KernelControlAccess; impl ControlAccess for KernelControlAccess { + // NOTE: Kernel-only access for ioctl helpers; never expose this to user pointers. fn read(&self, addr: u64, dst: &mut [u8]) -> Result<(), ControlError> { if dst.is_empty() { return Ok(()); @@ -1738,8 +1762,8 @@ mod tests { [buf.as_mut_ptr() as u64, buf.len() as u64, 0, 0, 0, 0], ); match dispatch(&invocation, None) { - DispatchResult::Completed(Ok(ptr)) => { - assert_eq!(ptr, buf.as_ptr() as u64); + DispatchResult::Completed(Ok(len)) => { + assert_eq!(len, 2); assert_eq!(buf[0], b'/'); assert_eq!(buf[1], 0); } From 29296531dfba0c073a869ebba716a7c5b0bb5376 Mon Sep 17 00:00:00 2001 From: n4mlz Date: Thu, 29 Jan 2026 15:05:28 +0900 Subject: [PATCH 13/25] refactor: update process subsystem documentation and enhance thread state management --- kernel/src/process/DESIGN.md | 2 +- kernel/src/process/mod.rs | 28 ++++---- kernel/src/thread/mod.rs | 123 +++++++++++++++-------------------- 3 files changed, 67 insertions(+), 86 deletions(-) diff --git a/kernel/src/process/DESIGN.md b/kernel/src/process/DESIGN.md index 321e7fe..2a99f9b 100644 --- a/kernel/src/process/DESIGN.md +++ b/kernel/src/process/DESIGN.md @@ -2,7 +2,7 @@ ## Role and Scope - Manage the set of threads and minimal metadata associated with each process. -- Currently limited to kernel-space execution; the structure is intentionally skeletal so it can expand once userland support arrives. +- Supports kernel and user processes with per-process address spaces, while keeping the model intentionally skeletal for future expansion. - Exposed as a single global instance (`PROCESS_TABLE`), which higher-level orchestration (at present the scheduler) manipulates through its API. ## Entities diff --git a/kernel/src/process/mod.rs b/kernel/src/process/mod.rs index c3aa32f..359862d 100644 --- a/kernel/src/process/mod.rs +++ b/kernel/src/process/mod.rs @@ -18,9 +18,9 @@ pub type ProcessHandle = Arc; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ProcessError { - AlreadyInitialised, NotInitialised, NotFound, + Terminated, DuplicateThread, ThreadNotAttached, AddressSpace(crate::arch::api::UserAddressSpaceError), @@ -42,6 +42,7 @@ impl ProcessFs { } fn install_stdio(&self) { + // NOTE: stdin/stdout/stderr share the same open file description (dup-like). let tty = crate::fs::devfs::global_tty_node(); let tty_file = tty .clone() @@ -84,8 +85,8 @@ impl Default for ProcessFs { /// /// # Implementation note /// -/// At this point, each process does not have an individual address space and all share the kernel's address space. -/// When implementing userland in the future, it will be necessary to properly duplicate and isolate `ArchThread::AddressSpace` here. +/// Kernel processes share the current address space; user processes receive their own +/// address spaces from `ArchThread::create_user_address_space`. pub struct ProcessTable { inner: SpinLock, initialised: AtomicBool, @@ -100,27 +101,18 @@ impl ProcessTable { } pub fn init_kernel(&self) -> Result { - if self - .initialised - .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) - .is_err() - { - return Ok(self - .inner - .lock() - .kernel_pid - .expect("kernel process must exist")); - } - + // NOTE: Idempotent initialization; returns PID 0 after the first call. let mut inner = self.inner.lock(); if inner.kernel_pid.is_some() { - return Err(ProcessError::AlreadyInitialised); + self.initialised.store(true, Ordering::Release); + return Ok(inner.kernel_pid.expect("kernel process must exist")); } let process = Arc::new(Process::kernel(0, "kernel", Abi::Host)); inner.kernel_pid = Some(process.id()); inner.next_pid = 1; inner.processes.push(process); + self.initialised.store(true, Ordering::Release); Ok(0) } @@ -255,6 +247,7 @@ impl Default for ProcessTable { pub static PROCESS_TABLE: ProcessTable = ProcessTable::new(); struct ProcessTableInner { + // TODO: Implement process reaping/removal; the vector grows monotonically. processes: Vec, kernel_pid: Option, next_pid: ProcessId, @@ -441,6 +434,9 @@ impl Process { } pub fn attach_thread(&self, tid: ThreadId) -> Result<(), ProcessError> { + if matches!(self.state(), ProcessState::Terminated) { + return Err(ProcessError::Terminated); + } let mut guard = self.threads.lock(); if guard.contains(&tid) { return Err(ProcessError::DuplicateThread); diff --git a/kernel/src/thread/mod.rs b/kernel/src/thread/mod.rs index 4224ed0..e740dc4 100644 --- a/kernel/src/thread/mod.rs +++ b/kernel/src/thread/mod.rs @@ -340,81 +340,64 @@ impl Scheduler { } fn switch_current(&self, frame: &mut CurrentTrapFrame, mode: SwitchMode) { - let ( - _next_id, - next_ctx, - next_space, - next_stack, - next_is_user, - next_abi, - next_process, - detach_target, - ) = { - let (_guard, mut inner) = self.lock_inner(); - let current_id = match inner.current { - Some(id) => id, - None => return, - }; - let idle_id = inner.idle.expect("idle thread must exist"); - let mut detach_target = None; - - if let Some(thread) = inner.thread_mut(current_id) { - let saved = ::save_context(frame); - thread.context = saved; - let process = thread.process().clone(); - - match mode { - SwitchMode::Requeue => { - if current_id != idle_id { - thread.state = ThreadState::Ready; - inner.ready.push_back(current_id); - process.mark_ready(); - } else { - thread.state = ThreadState::Idle; - } - } - SwitchMode::Terminate => { - detach_target = Some((process, current_id)); - thread.state = ThreadState::Terminated; + // NOTE: Interrupts must remain masked until the context is restored to + // avoid re-entrancy during the switch. + let (_guard, mut inner) = self.lock_inner(); + let current_id = match inner.current { + Some(id) => id, + None => return, + }; + let idle_id = inner.idle.expect("idle thread must exist"); + let mut detach_target = None; + + if let Some(thread) = inner.thread_mut(current_id) { + let saved = ::save_context(frame); + thread.context = saved; + let process = thread.process().clone(); + + match mode { + SwitchMode::Requeue => { + if current_id != idle_id { + thread.state = ThreadState::Ready; + inner.ready.push_back(current_id); + process.mark_ready(); + } else { + thread.state = ThreadState::Idle; } } + SwitchMode::Terminate => { + detach_target = Some((process, current_id)); + thread.state = ThreadState::Terminated; + } } + } - let next_id = inner.next_runnable(idle_id); - inner.current = Some(next_id); - - let (ctx, space, stack_top, is_user, abi, process) = inner - .thread_mut(next_id) - .map(|thread| { - thread.state = if next_id == idle_id { - ThreadState::Idle - } else { - ThreadState::Running - }; - ( - thread.context.clone(), - thread.address_space.clone(), - thread.kernel_stack_top(), - thread.is_user(), - thread.process().abi(), - thread.process().clone(), - ) - }) - .expect("next thread must exist"); - - ( - next_id, - ctx, - space, - stack_top, - is_user, - abi, - process, - detach_target, - ) - }; + let next_id = inner.next_runnable(idle_id); + inner.current = Some(next_id); + + let (next_ctx, next_space, next_stack, next_is_user, next_abi, next_process) = inner + .thread_mut(next_id) + .map(|thread| { + thread.state = if next_id == idle_id { + ThreadState::Idle + } else { + ThreadState::Running + }; + ( + thread.context.clone(), + thread.address_space.clone(), + thread.kernel_stack_top(), + thread.is_user(), + thread.process().abi(), + thread.process().clone(), + ) + }) + .expect("next thread must exist"); + drop(inner); if let Some((process, tid)) = detach_target { + // NOTE: Scheduler reaches into Process directly; this should route + // through ProcessTable once it tracks thread membership globally. let _ = process.detach_thread(tid); } @@ -463,6 +446,7 @@ pub enum SpawnError { } struct SchedulerInner { + // TODO: Reap terminated threads; the vector grows monotonically. threads: Vec, ready: VecDeque, current: Option, @@ -800,6 +784,7 @@ struct KernelStack { impl KernelStack { fn allocate(size: usize) -> Result { + // TODO: Add guard pages to catch kernel stack overflows. let layout = Layout::from_size_align(size, KERNEL_STACK_ALIGN) .map_err(|_| SpawnError::OutOfMemory)?; let ptr = unsafe { alloc::alloc::alloc(layout) }; From 641db66e88403d5b1f9042a59774da5792310a54 Mon Sep 17 00:00:00 2001 From: n4mlz Date: Thu, 29 Jan 2026 15:36:46 +0900 Subject: [PATCH 14/25] tmp: enhance logging in loader, process, syscall, and thread management for better traceability --- kernel/src/loader/linux/mod.rs | 29 +++++++++++++++++++++++++++++ kernel/src/process/mod.rs | 4 ++++ kernel/src/syscall/linux.rs | 15 +++++++++++++++ kernel/src/syscall/mod.rs | 21 ++++++++++++++++++++- kernel/src/thread/mod.rs | 26 ++++++++++++++++++++++++++ 5 files changed, 94 insertions(+), 1 deletion(-) diff --git a/kernel/src/loader/linux/mod.rs b/kernel/src/loader/linux/mod.rs index 92ddcae..e7c7229 100644 --- a/kernel/src/loader/linux/mod.rs +++ b/kernel/src/loader/linux/mod.rs @@ -103,10 +103,18 @@ pub fn load_elf_with_platform

( where P: ArchLinuxElfPlatform, { + crate::println!("[loader] load_elf pid={} path={}", pid, raw_path); let abs = resolve_path(pid, raw_path)?; let elf_bytes = proc_fs::read_to_end_at(pid, &abs)?; let elf = elf::ElfFile::parse::

(&elf_bytes)?; let load_bias = choose_load_bias::

(&elf)?; + crate::println!( + "[loader] elf type={:?} entry={:#x} phnum={} load_bias={:#x}", + elf.elf_type, + elf.entry.as_raw(), + elf.ph_count, + load_bias.as_raw() + ); let space: P::AddressSpace = PROCESS_TABLE .address_space(pid) @@ -114,10 +122,19 @@ where // NOTE: The loader does not validate user address ranges; it assumes a fresh // address space (or equivalent) before mapping the ELF image. + crate::println!("[loader] mapping segments count={}", elf.segments.len()); let mapped = map::map_segments::

(&space, &elf, &elf_bytes, load_bias)?; if let Some(dynamic) = elf.dynamic.as_ref() { + crate::println!("[loader] dynamic segment present"); space.with_page_table(|table, _| { let info = reloc::read_dynamic_info(table, load_bias, dynamic)?; + crate::println!( + "[loader] relocations rela={} rel={} relr={} jmprel={}", + info.rela_size, + info.rel_size, + info.relr_size, + info.jmprel_size + ); reloc::apply_relocations(table, load_bias, &info, &mapped)?; Ok::<(), LinuxLoadError>(()) })?; @@ -126,6 +143,7 @@ where map::apply_segment_permissions(table, &mapped)?; if elf.interp.is_some() { if let Some(relro) = elf.relro.as_ref() { + crate::println!("[loader] applying GNU_RELRO"); reloc::apply_gnu_relro(table, load_bias, relro, &mapped)?; } } else if elf.relro.is_some() { @@ -136,11 +154,22 @@ where let user_stack = P::allocate_user_stack(&space, 32 * 1024)?; let stack_top = P::user_stack_top(&user_stack); + crate::println!( + "[loader] allocated user stack top={:#x}", + stack_top.as_raw() + ); let stack_pointer = space .with_page_table(|table, _| stack::initialise_minimal_stack(table, stack_top)) .map_err(LinuxLoadError::from)?; let heap_base = compute_heap_base::

(load_bias, &elf)?; let phdr = compute_phdr_address(load_bias, &elf, &mapped)?; + crate::println!( + "[loader] entry={:#x} stack={:#x} heap_base={:#x} phdr={:#x}", + add_base(load_bias, elf.entry)?.as_raw(), + stack_pointer.as_raw(), + heap_base.as_raw(), + phdr.as_raw() + ); Ok(LinuxProgram { entry: add_base(load_bias, elf.entry)?, diff --git a/kernel/src/process/mod.rs b/kernel/src/process/mod.rs index 359862d..79dc9b3 100644 --- a/kernel/src/process/mod.rs +++ b/kernel/src/process/mod.rs @@ -105,6 +105,7 @@ impl ProcessTable { let mut inner = self.inner.lock(); if inner.kernel_pid.is_some() { self.initialised.store(true, Ordering::Release); + crate::println!("[process] init_kernel already initialised"); return Ok(inner.kernel_pid.expect("kernel process must exist")); } @@ -113,6 +114,7 @@ impl ProcessTable { inner.next_pid = 1; inner.processes.push(process); self.initialised.store(true, Ordering::Release); + crate::println!("[process] init_kernel created pid=0"); Ok(0) } @@ -137,6 +139,7 @@ impl ProcessTable { let process = Arc::new(Process::kernel(pid, name, Abi::Host)); inner.next_pid = pid.checked_add(1).expect("process id overflow"); inner.processes.push(process); + crate::println!("[process] create_kernel_process pid={} name={}", pid, name); Ok(pid) } @@ -169,6 +172,7 @@ impl ProcessTable { let process = Arc::new(Process::user(pid, name, space, domain)); inner.next_pid = pid.checked_add(1).expect("process id overflow"); inner.processes.push(process); + crate::println!("[process] create_user_process pid={} name={}", pid, name); Ok(pid) } diff --git a/kernel/src/syscall/linux.rs b/kernel/src/syscall/linux.rs index 61cfde4..6c90614 100644 --- a/kernel/src/syscall/linux.rs +++ b/kernel/src/syscall/linux.rs @@ -735,6 +735,7 @@ fn handle_fork( Ok(pid) => pid, Err(err) => return DispatchResult::Completed(Err(err)), }; + crate::println!("[fork] parent_pid={}", pid); let parent_stack = match SCHEDULER.current_user_stack_info() { Some(info) => info, @@ -761,6 +762,7 @@ fn handle_fork( Ok(pid) => pid, Err(_) => return DispatchResult::Completed(Err(SysError::InvalidArgument)), }; + crate::println!("[fork] created child_pid={}", child_pid); if let Ok(child_proc) = PROCESS_TABLE.process_handle(child_pid) { child_proc.set_brk_state(parent_proc.brk_state()); @@ -802,6 +804,7 @@ fn handle_fork( return DispatchResult::Completed(Err(spawn_error_to_sys(err))); } + crate::println!("[fork] spawned child thread for pid={}", child_pid); DispatchResult::Completed(Ok(child_pid)) } @@ -832,6 +835,7 @@ fn handle_execve( Ok(path) => path, Err(err) => return DispatchResult::Completed(Err(err)), }; + crate::println!("[execve] pid={} path={}", pid, path); let argv_ptr = invocation.arg(1).unwrap_or(0); let envp_ptr = invocation.arg(2).unwrap_or(0); @@ -850,6 +854,11 @@ fn handle_execve( Ok(list) => list, Err(err) => return DispatchResult::Completed(Err(err)), }; + crate::println!( + "[execve] argv_count={} envp_count={}", + argv.len(), + envp.len() + ); // Drop the previous user stack before clearing mappings so we do not unmap the // freshly allocated stack on replacement. @@ -869,6 +878,11 @@ fn handle_execve( Ok(program) => program, Err(_err) => return DispatchResult::Completed(Err(SysError::InvalidArgument)), }; + crate::println!( + "[execve] entry={:#x} stack_top={:#x}", + program.entry.as_raw(), + ::user_stack_top(&program.user_stack).as_raw() + ); let auxv = crate::loader::linux::build_auxv(&program, PageSize::SIZE_4K.bytes()); let argv_refs: alloc::vec::Vec<&str> = argv.iter().map(|s| s.as_str()).collect(); @@ -897,6 +911,7 @@ fn handle_execve( frame.rip = program.entry.as_raw() as u64; frame.rsp = stack_pointer.as_raw() as u64; frame.regs.rax = 0; + crate::println!("[execve] new rsp={:#x}", frame.rsp); DispatchResult::Completed(Ok(0)) } diff --git a/kernel/src/syscall/mod.rs b/kernel/src/syscall/mod.rs index a57ec41..6b0ebea 100644 --- a/kernel/src/syscall/mod.rs +++ b/kernel/src/syscall/mod.rs @@ -63,10 +63,29 @@ pub fn dispatch_with_frame( invocation: &SyscallInvocation, frame: Option<&mut crate::trap::CurrentTrapFrame>, ) -> DispatchResult { - match abi { + crate::println!( + "[syscall] abi={:?} nr={} args=[{:x}, {:x}, {:x}, {:x}, {:x}, {:x}]", + abi, + invocation.number, + invocation.args[0], + invocation.args[1], + invocation.args[2], + invocation.args[3], + invocation.args[4], + invocation.args[5], + ); + let result = match abi { Abi::Host => DispatchResult::Completed(host::dispatch(invocation)), Abi::Linux => linux::dispatch(invocation, frame), + }; + if matches!(result, DispatchResult::Completed(Err(SysError::NotImplemented))) { + crate::println!( + "[syscall] unimplemented abi={:?} nr={}", + abi, + invocation.number + ); } + result } /// Encode a syscall result into an ABI-specific return value. diff --git a/kernel/src/thread/mod.rs b/kernel/src/thread/mod.rs index e740dc4..91e0ec2 100644 --- a/kernel/src/thread/mod.rs +++ b/kernel/src/thread/mod.rs @@ -94,6 +94,7 @@ impl Scheduler { .init_kernel() .map_err(SchedulerError::Process)?; inner.kernel_process = Some(kernel_pid); + crate::println!("[thread] scheduler init kernel_pid={}", kernel_pid); let kernel_process = PROCESS_TABLE .process_handle(kernel_pid) @@ -108,6 +109,7 @@ impl Scheduler { inner.current = Some(bootstrap.id); inner.threads.push(bootstrap); kernel_process.mark_running(); + crate::println!("[thread] bootstrap thread id=0"); let idle_id = inner.next_tid; let idle = ThreadControl::idle(idle_id, kernel_process.clone(), kernel_space.clone()) @@ -117,6 +119,7 @@ impl Scheduler { .map_err(SchedulerError::Process)?; inner.next_tid = idle_id.checked_add(1).expect("thread id overflow"); inner.idle = Some(idle.id); + crate::println!("[thread] idle thread id={}", idle.id); inner.threads.push(idle); inner.initialised = true; syscall::set_current_abi(kernel_process.abi()); @@ -167,6 +170,11 @@ impl Scheduler { return Err(SpawnError::SchedulerNotReady); } + crate::println!( + "[thread] spawn kernel thread name={} pid={}", + name, + process + ); self.spawn_thread_locked(&mut inner, process, name, entry) } @@ -182,6 +190,12 @@ impl Scheduler { return Err(SpawnError::SchedulerNotReady); } + crate::println!( + "[thread] spawn user thread name={} pid={} entry={:#x}", + name, + process, + entry.as_raw() + ); inner.spawn_user_thread(process, name, entry, stack_size) } @@ -198,6 +212,13 @@ impl Scheduler { return Err(SpawnError::SchedulerNotReady); } + crate::println!( + "[thread] spawn user thread (stack) name={} pid={} entry={:#x} sp={:#x}", + name, + process, + entry.as_raw(), + stack_pointer.as_raw() + ); inner.spawn_user_thread_with_stack(process, name, entry, user_stack, stack_pointer) } @@ -213,6 +234,11 @@ impl Scheduler { return Err(SpawnError::SchedulerNotReady); } + crate::println!( + "[thread] spawn user thread (ctx) name={} pid={}", + name, + process + ); inner.spawn_user_thread_with_context(process, name, context, user_stack) } From 642a27685330b1e4a9bb5e15fbb605f7ec527a17 Mon Sep 17 00:00:00 2001 From: n4mlz Date: Thu, 29 Jan 2026 15:37:36 +0900 Subject: [PATCH 15/25] fix: relocation processes for better traceability --- kernel/src/loader/linux/map.rs | 58 ++++++++++++++++++++++++++------ kernel/src/loader/linux/reloc.rs | 21 ++++++------ 2 files changed, 58 insertions(+), 21 deletions(-) diff --git a/kernel/src/loader/linux/map.rs b/kernel/src/loader/linux/map.rs index 2fb2c75..54a4e97 100644 --- a/kernel/src/loader/linux/map.rs +++ b/kernel/src/loader/linux/map.rs @@ -1,5 +1,7 @@ use crate::arch::api::{ArchLinuxElfPlatform, ArchPageTableAccess}; -use crate::mem::addr::{MemPerm, Page, PageSize, VirtAddr, VirtIntoPtr, align_down, align_up}; +use crate::mem::addr::{ + MemPerm, Page, PageSize, PhysAddr, VirtAddr, VirtIntoPtr, align_down, align_up, +}; use crate::mem::manager; use crate::mem::paging::{FrameAllocator, PageTableOps}; use crate::mem::paging::{MapError, PhysMapper, TranslationError}; @@ -66,6 +68,13 @@ fn map_single_segment seg.file_size { + crate::println!( + "[loader] zero bss vaddr={:#x} len={:#x}", + seg_vaddr.as_raw() + seg.file_size, + seg.mem_size - seg.file_size + ); let bss_start = seg_vaddr .as_raw() .checked_add(seg.file_size) @@ -109,6 +133,7 @@ fn map_single_segment 0 { + crate::println!("[loader] rewrite syscalls in executable segment"); rewrite_syscalls_in_table(seg_vaddr, seg.file_size, table).map_err(LinuxLoadError::from)?; } @@ -126,20 +151,17 @@ fn copy_into_mapped( src: &[u8], page_size: usize, ) -> Result<(), LinuxLoadError> { - let mapper = manager::phys_mapper(); let mut offset = 0usize; while offset < src.len() { let addr = dst .as_raw() .checked_add(offset) .ok_or(LinuxLoadError::SizeOverflow)?; - let virt = VirtAddr::new(addr); - let phys = table.translate(virt).map_err(LinuxLoadError::from)?; let page_offset = addr % page_size; let len = (page_size - page_offset).min(src.len() - offset); + let ptr = mapped_ptr(table, addr, page_size)?; unsafe { - let ptr = mapper.phys_to_virt(phys); - core::ptr::copy_nonoverlapping(src[offset..].as_ptr(), ptr.into_mut_ptr(), len); + core::ptr::copy_nonoverlapping(src[offset..].as_ptr(), ptr, len); } offset += len; } @@ -152,26 +174,40 @@ fn zero_mapped( len: usize, page_size: usize, ) -> Result<(), LinuxLoadError> { - let mapper = manager::phys_mapper(); let mut offset = 0usize; while offset < len { let addr = dst .as_raw() .checked_add(offset) .ok_or(LinuxLoadError::SizeOverflow)?; - let virt = VirtAddr::new(addr); - let phys = table.translate(virt).map_err(LinuxLoadError::from)?; let page_offset = addr % page_size; let chunk = (page_size - page_offset).min(len - offset); + let ptr = mapped_ptr(table, addr, page_size)?; unsafe { - let ptr = mapper.phys_to_virt(phys); - core::ptr::write_bytes(ptr.into_mut_ptr(), 0, chunk); + core::ptr::write_bytes(ptr, 0, chunk); } offset += chunk; } Ok(()) } +fn mapped_ptr( + table: &T, + addr: usize, + page_size: usize, +) -> Result<*mut u8, LinuxLoadError> { + let virt = VirtAddr::new(addr); + let phys = table.translate(virt).map_err(LinuxLoadError::from)?; + let page_offset = addr % page_size; + let base = align_down(phys.as_raw(), page_size); + let phys_addr = base + .checked_add(page_offset) + .ok_or(LinuxLoadError::SizeOverflow)?; + let mapper = manager::phys_mapper(); + let ptr = unsafe { mapper.phys_to_virt(PhysAddr::new(phys_addr)).into_mut_ptr() }; + Ok(ptr) +} + impl From for LinuxLoadError { fn from(err: TranslationError) -> Self { let map_err = match err { diff --git a/kernel/src/loader/linux/reloc.rs b/kernel/src/loader/linux/reloc.rs index e4d7cf1..04976d8 100644 --- a/kernel/src/loader/linux/reloc.rs +++ b/kernel/src/loader/linux/reloc.rs @@ -150,6 +150,10 @@ pub fn apply_relocations( info: &DynamicInfo, segments: &[MappedSegment], ) -> Result<(), LinuxLoadError> { + crate::println!( + "[loader] apply relocations base={:#x}", + base.as_raw() + ); apply_rel(table, base, info, segments)?; apply_rela(table, base, info, segments)?; apply_relr(table, base, info, segments)?; @@ -398,10 +402,6 @@ fn resolve_reloc_target( if is_mapped(base_target, segments) { return Ok(base_target); } - let abs_target = VirtAddr::new(raw); - if is_mapped(abs_target, segments) { - return Ok(abs_target); - } Err(LinuxLoadError::RelocationTargetOutOfRange) } @@ -436,12 +436,8 @@ fn resolve_relative_value( .checked_add(addend_usize) .ok_or(LinuxLoadError::SizeOverflow)?; let base_addr = VirtAddr::new(base_value); - if is_mapped(base_addr, segments) { - return Ok(base_value as u64); - } - let abs_addr = VirtAddr::new(addend_usize); - if is_mapped(abs_addr, segments) { - return Ok(addend_usize as u64); + if !is_mapped(base_addr, segments) { + return Err(LinuxLoadError::RelocationTargetOutOfRange); } Ok(base_value as u64) } @@ -477,6 +473,11 @@ pub fn apply_gnu_relro( let relro_end = relro_start .checked_add(relro.mem_size) .ok_or(LinuxLoadError::SizeOverflow)?; + crate::println!( + "[loader] relro range={:#x}-{:#x}", + relro_start.as_raw(), + relro_end.as_raw() + ); let page_size = segments .first() .map(|seg| seg.page_size) From 76d500a6828837a1fe33d757814afdf71a7a22b1 Mon Sep 17 00:00:00 2001 From: n4mlz Date: Thu, 29 Jan 2026 16:55:34 +0900 Subject: [PATCH 16/25] refactor: enhance page fault handling and logging for better traceability --- kernel/src/arch/x86_64/trap/handlers.rs | 189 +++++++++++++++++++++++- kernel/src/arch/x86_64/trap/stubs.rs | 23 +++ kernel/src/loader/linux/reloc.rs | 49 ++++++ kernel/src/syscall/linux.rs | 3 + 4 files changed, 262 insertions(+), 2 deletions(-) diff --git a/kernel/src/arch/x86_64/trap/handlers.rs b/kernel/src/arch/x86_64/trap/handlers.rs index 0017377..fbb31e8 100644 --- a/kernel/src/arch/x86_64/trap/handlers.rs +++ b/kernel/src/arch/x86_64/trap/handlers.rs @@ -1,10 +1,18 @@ use x86_64::instructions::interrupts; use x86_64::registers::control::Cr2; +use crate::arch::api::ArchPageTableAccess; +use crate::mem::paging::{PageTableOps, PhysMapper}; use crate::println; use crate::process::PROCESS_TABLE; use crate::thread::SCHEDULER; use crate::trap::TrapInfo; +use crate::{ + mem::{ + addr::{VirtAddr, VirtIntoPtr}, + manager, + }, +}; use super::TrapFrame; @@ -31,6 +39,8 @@ pub fn handle_exception(info: TrapInfo, frame: &mut TrapFrame) -> bool { fn handle_page_fault(frame: &mut TrapFrame) -> bool { let fault_addr = Cr2::read().expect("CR2 must contain a canonical address"); let code = frame.error_code; + let fs_base = x86_64::registers::model_specific::FsBase::read().as_u64(); + let cpl = (frame.cs & 3) as u8; let present = (code & 1) != 0; let write = (code & 1 << 1) != 0; @@ -39,14 +49,50 @@ fn handle_page_fault(frame: &mut TrapFrame) -> bool { let instruction = (code & 1 << 4) != 0; println!( - "[#PF] fault_addr={:#x} present={} write={} user={} reserved={} instruction={}", + "[#PF] fault_addr={:#x} present={} write={} user={} reserved={} instruction={} fs_base={:#x}", fault_addr.as_u64(), present, write, user, reserved, - instruction + instruction, + fs_base ); + println!( + "[#PF] rip={:#x} cs={:#x} rsp={:#x} ss={:#x} cpl={}", + frame.rip, + frame.cs, + frame.rsp, + frame.ss, + cpl + ); + dump_trap_frame_qwords(frame); + dump_frame_field_hints(frame); + if fault_addr.as_u64() >= fs_base { + println!( + "[#PF] fault_addr-fs_base={:#x}", + fault_addr.as_u64() - fs_base + ); + } + if let Some(pid) = SCHEDULER.current_process_id() { + if let Ok(process) = PROCESS_TABLE.process_handle(pid) { + let brk = process.brk_state(); + println!( + "[#PF] pid={} brk_base={:#x} brk_current={:#x}", + pid, + brk.base.as_raw(), + brk.current.as_raw() + ); + } + } + if let Some(stack) = SCHEDULER.current_user_stack_info() { + println!( + "[#PF] user_stack base={:#x} size={:#x}", + stack.base.as_raw(), + stack.size + ); + } + dump_faulting_bytes(frame.rip as usize); println!("[#PF] frame={:#?}", frame); if user { if let Some(pid) = SCHEDULER.current_process_id() { @@ -82,6 +128,145 @@ fn handle_double_fault(_frame: &TrapFrame) -> bool { } } +fn dump_faulting_bytes(rip: usize) { + let pid = match SCHEDULER.current_process_id() { + Some(pid) => pid, + None => return, + }; + let process = match PROCESS_TABLE.process_handle(pid) { + Ok(proc) => proc, + Err(_) => return, + }; + let mut bytes = [0u8; 16]; + let mut ok = true; + process.address_space().with_page_table(|table, _| { + for (idx, out) in bytes.iter_mut().enumerate() { + let addr = match rip.checked_add(idx) { + Some(addr) => addr, + None => { + ok = false; + break; + } + }; + let virt = VirtAddr::new(addr); + let phys = match table.translate(virt) { + Ok(phys) => phys, + Err(_) => { + ok = false; + break; + } + }; + let mapper = manager::phys_mapper(); + unsafe { + let ptr = mapper.phys_to_virt(phys).into_ptr(); + *out = core::ptr::read(ptr); + } + } + }); + if ok { + println!( + "[#PF] bytes @ rip: {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x}", + bytes[0], + bytes[1], + bytes[2], + bytes[3], + bytes[4], + bytes[5], + bytes[6], + bytes[7], + bytes[8], + bytes[9], + bytes[10], + bytes[11], + bytes[12], + bytes[13], + bytes[14], + bytes[15] + ); + } else { + println!("[#PF] bytes @ rip: "); + } +} + +fn dump_trap_frame_qwords(frame: &TrapFrame) { + let base = frame as *const TrapFrame as *const u64; + let mut slots = [0u64; 12]; + for (idx, slot) in slots.iter_mut().enumerate() { + unsafe { + *slot = core::ptr::read_volatile(base.add(idx)); + } + } + println!( + "[#PF] frame qwords: {:016x} {:016x} {:016x} {:016x} {:016x} {:016x} {:016x} {:016x} {:016x} {:016x} {:016x} {:016x}", + slots[0], + slots[1], + slots[2], + slots[3], + slots[4], + slots[5], + slots[6], + slots[7], + slots[8], + slots[9], + slots[10], + slots[11] + ); +} + +fn dump_frame_field_hints(frame: &TrapFrame) { + let pid = SCHEDULER.current_process_id(); + let (brk_base, brk_current) = pid + .and_then(|pid| PROCESS_TABLE.process_handle(pid).ok()) + .map(|proc| { + let brk = proc.brk_state(); + (Some(brk.base.as_raw()), Some(brk.current.as_raw())) + }) + .unwrap_or((None, None)); + let (stack_base, stack_size) = SCHEDULER + .current_user_stack_info() + .map(|stack| (Some(stack.base.as_raw()), Some(stack.size))) + .unwrap_or((None, None)); + + dump_field_hint("rip", frame.rip, brk_base, brk_current, stack_base, stack_size); + dump_field_hint("cs", frame.cs, brk_base, brk_current, stack_base, stack_size); + dump_field_hint("rflags", frame.rflags, brk_base, brk_current, stack_base, stack_size); + dump_field_hint("rsp", frame.rsp, brk_base, brk_current, stack_base, stack_size); + dump_field_hint("ss", frame.ss, brk_base, brk_current, stack_base, stack_size); +} + +fn dump_field_hint( + name: &str, + value: u64, + brk_base: Option, + brk_current: Option, + stack_base: Option, + stack_size: Option, +) { + let is_small = value <= 0xffff; + let in_brk = match (brk_base, brk_current) { + (Some(base), Some(current)) => { + let addr = value as usize; + addr >= base && addr < current + } + _ => false, + }; + let in_stack = match (stack_base, stack_size) { + (Some(base), Some(size)) => { + let addr = value as usize; + addr >= base && addr < base.saturating_add(size) + } + _ => false, + }; + println!( + "[#PF] {}={:#x} small={} in_brk={} in_stack={}", + name, + value, + is_small, + in_brk, + in_stack + ); +} + #[cfg(test)] mod tests { use super::*; diff --git a/kernel/src/arch/x86_64/trap/stubs.rs b/kernel/src/arch/x86_64/trap/stubs.rs index 568ac3c..dbc997c 100644 --- a/kernel/src/arch/x86_64/trap/stubs.rs +++ b/kernel/src/arch/x86_64/trap/stubs.rs @@ -261,6 +261,29 @@ pub(super) const EXTERNAL_INTERRUPT_STUBS: [unsafe extern "C" fn() -> !; #[unsafe(no_mangle)] pub(super) unsafe extern "C" fn dispatch_trap(vector: u8, frame: *mut TrapFrame, has_error: u8) { + if vector == 14 { + let frame_ptr = frame as *const u64; + let cpu_frame_ptr = (frame as *const u8).wrapping_add(ORIGINAL_ERROR_OFFSET) as *const u64; + let mut cpu_words = [0u64; 6]; + for (idx, slot) in cpu_words.iter_mut().enumerate() { + *slot = core::ptr::read_volatile(cpu_frame_ptr.add(idx)); + } + crate::println!( + "[#PF] frame_ptr={:#x} cpu_frame_ptr={:#x} has_error={}", + frame_ptr as usize, + cpu_frame_ptr as usize, + has_error + ); + crate::println!( + "[#PF] cpu_frame qwords: {:016x} {:016x} {:016x} {:016x} {:016x} {:016x}", + cpu_words[0], + cpu_words[1], + cpu_words[2], + cpu_words[3], + cpu_words[4], + cpu_words[5] + ); + } let frame = unsafe { &mut *frame }; let info = build_trap_info(vector, has_error != 0); ::dispatch_trap(info, frame); diff --git a/kernel/src/loader/linux/reloc.rs b/kernel/src/loader/linux/reloc.rs index 04976d8..b3f4a2c 100644 --- a/kernel/src/loader/linux/reloc.rs +++ b/kernel/src/loader/linux/reloc.rs @@ -141,6 +141,44 @@ pub fn read_dynamic_info( if info.relr_size > 0 && info.relr_addr.is_none() { return Err(LinuxLoadError::InvalidElf("DT_RELR missing")); } + if let (Some(rela_addr), true) = (info.rela_addr, info.rela_size > 0) { + crate::println!( + "[loader] rela addr={:#x} size={:#x} ent={:#x}", + rela_addr.as_raw(), + info.rela_size, + info.rela_ent + ); + let entry_size = core::mem::size_of::(); + if entry_size > 0 && info.rela_size.is_multiple_of(entry_size) { + let count = (info.rela_size / entry_size).min(3); + for idx in 0..count { + let entry_addr = match rela_addr.checked_add(idx * entry_size) { + Some(addr) => addr, + None => break, + }; + let r_offset = match user.read_u64(entry_addr) { + Ok(val) => val, + Err(_) => break, + }; + let r_info = match user.read_u64(entry_addr.checked_add(8).unwrap_or(entry_addr)) { + Ok(val) => val, + Err(_) => break, + }; + let r_addend = + match user.read_u64(entry_addr.checked_add(16).unwrap_or(entry_addr)) { + Ok(val) => val as i64, + Err(_) => break, + }; + crate::println!( + "[loader] rela[{}] r_offset={:#x} r_info={:#x} r_addend={:#x}", + idx, + r_offset, + r_info, + r_addend + ); + } + } + } Ok(info) } @@ -265,6 +303,17 @@ fn apply_rela_table( let raw_offset = usize::try_from(r_offset).map_err(|_| LinuxLoadError::SizeOverflow)?; let target = resolve_reloc_target(base, raw_offset, segments)?; let value_raw = resolve_symbol_reloc(base, reloc_type, sym, r_addend, segments)?; + let value_addr = VirtAddr::new(value_raw as usize); + if !is_mapped(value_addr, segments) { + crate::println!( + "[loader] rela[{}] unmapped value r_offset={:#x} target={:#x} addend={:#x} value={:#x}", + idx, + r_offset, + target.as_raw(), + r_addend, + value_raw + ); + } user.write_u64(target, value_raw)?; } diff --git a/kernel/src/syscall/linux.rs b/kernel/src/syscall/linux.rs index 6c90614..212e5a3 100644 --- a/kernel/src/syscall/linux.rs +++ b/kernel/src/syscall/linux.rs @@ -993,7 +993,10 @@ fn handle_arch_prctl(invocation: &SyscallInvocation) -> SysResult { } // NOTE: This assumes FS base is part of the thread context and restored on context switches. + crate::println!("[arch_prctl] ARCH_SET_FS requested={:#x}", value); crate::arch::x86_64::set_fs_base(value); + let fs_base = x86_64::registers::model_specific::FsBase::read().as_u64(); + crate::println!("[arch_prctl] FS base now={:#x}", fs_base); Ok(0) } From 68bc31eea9391e4ee8d8bb0a7c6c7b0c07b9b5f7 Mon Sep 17 00:00:00 2001 From: n4mlz Date: Thu, 29 Jan 2026 16:56:46 +0900 Subject: [PATCH 17/25] test: add user-mode page fault fixture and validation tests --- kernel/src/arch/x86_64/mod.rs | 13 +++ kernel/src/arch/x86_64/trap/context.rs | 1 + kernel/src/arch/x86_64/trap/handlers.rs | 71 ++++++++++++++++ kernel/src/arch/x86_64/trap/mod.rs | 13 +++ kernel/src/kernel_proc/linux_box.rs | 81 +++++++++++++++++++ .../fixtures/linux-page-fault/DESIGN.md | 14 ++++ xtask-assets/fixtures/linux-page-fault/main.c | 9 +++ xtask-assets/src/lib.rs | 23 ++++++ xtask/src/lib.rs | 2 + 9 files changed, 227 insertions(+) create mode 100644 xtask-assets/fixtures/linux-page-fault/DESIGN.md create mode 100644 xtask-assets/fixtures/linux-page-fault/main.c diff --git a/kernel/src/arch/x86_64/mod.rs b/kernel/src/arch/x86_64/mod.rs index 59ff720..2bf4dc7 100644 --- a/kernel/src/arch/x86_64/mod.rs +++ b/kernel/src/arch/x86_64/mod.rs @@ -98,6 +98,19 @@ pub fn halt() { x86_64::instructions::hlt(); } +#[cfg(test)] +pub(crate) fn arm_user_pf_frame_check( + pid: crate::process::ProcessId, + expected_fault_addr: u64, +) { + trap::arm_user_pf_frame_check(pid, expected_fault_addr); +} + +#[cfg(test)] +pub(crate) fn user_pf_frame_check_passed() -> bool { + trap::user_pf_frame_check_passed() +} + impl ArchInterrupt for X86_64 { type Timer = interrupt::LocalApicTimer; diff --git a/kernel/src/arch/x86_64/trap/context.rs b/kernel/src/arch/x86_64/trap/context.rs index 051aeef..0557ad9 100644 --- a/kernel/src/arch/x86_64/trap/context.rs +++ b/kernel/src/arch/x86_64/trap/context.rs @@ -1,6 +1,7 @@ use crate::trap::TrapFrame as TrapFrameTrait; pub(super) const GENERAL_REGS_SIZE: usize = core::mem::size_of::(); +/// Byte offset from the trap frame base to the CPU-pushed error code slot. pub(super) const ORIGINAL_ERROR_OFFSET: usize = 8 + GENERAL_REGS_SIZE; #[repr(C)] diff --git a/kernel/src/arch/x86_64/trap/handlers.rs b/kernel/src/arch/x86_64/trap/handlers.rs index fbb31e8..27b087e 100644 --- a/kernel/src/arch/x86_64/trap/handlers.rs +++ b/kernel/src/arch/x86_64/trap/handlers.rs @@ -15,6 +15,31 @@ use crate::{ }; use super::TrapFrame; +#[cfg(test)] +use super::context::ORIGINAL_ERROR_OFFSET; +#[cfg(test)] +use core::sync::atomic::{AtomicU64, AtomicU8, Ordering}; +#[cfg(test)] +use crate::process::ProcessId; + +#[cfg(test)] +static USER_PF_FRAME_CHECK_STATE: AtomicU8 = AtomicU8::new(0); +#[cfg(test)] +static USER_PF_FRAME_CHECK_PID: AtomicU64 = AtomicU64::new(0); +#[cfg(test)] +static USER_PF_FRAME_CHECK_ADDR: AtomicU64 = AtomicU64::new(0); + +#[cfg(test)] +pub(crate) fn arm_user_pf_frame_check(pid: ProcessId, expected_fault_addr: u64) { + USER_PF_FRAME_CHECK_PID.store(pid, Ordering::SeqCst); + USER_PF_FRAME_CHECK_ADDR.store(expected_fault_addr, Ordering::SeqCst); + USER_PF_FRAME_CHECK_STATE.store(1, Ordering::SeqCst); +} + +#[cfg(test)] +pub(crate) fn user_pf_frame_check_passed() -> bool { + USER_PF_FRAME_CHECK_STATE.load(Ordering::SeqCst) == 2 +} pub fn handle_exception(info: TrapInfo, frame: &mut TrapFrame) -> bool { // Be careful with logging/locking here: traps can occur while locks are held or @@ -48,6 +73,9 @@ fn handle_page_fault(frame: &mut TrapFrame) -> bool { let reserved = (code & 1 << 3) != 0; let instruction = (code & 1 << 4) != 0; + #[cfg(test)] + maybe_check_user_pf_frame(frame, fault_addr.as_u64()); + println!( "[#PF] fault_addr={:#x} present={} write={} user={} reserved={} instruction={} fs_base={:#x}", fault_addr.as_u64(), @@ -267,6 +295,49 @@ fn dump_field_hint( ); } +#[cfg(test)] +fn maybe_check_user_pf_frame(frame: &TrapFrame, fault_addr: u64) { + if USER_PF_FRAME_CHECK_STATE.load(Ordering::SeqCst) != 1 { + return; + } + + let expected_pid = USER_PF_FRAME_CHECK_PID.load(Ordering::SeqCst); + let expected_addr = USER_PF_FRAME_CHECK_ADDR.load(Ordering::SeqCst); + if SCHEDULER.current_process_id().unwrap_or(0) != expected_pid { + return; + } + if fault_addr != expected_addr { + return; + } + + // Read the CPU-pushed exception frame starting at ORIGINAL_ERROR_OFFSET. + let base = frame as *const TrapFrame as *const u8; + let cpu_frame_ptr = + unsafe { base.add(ORIGINAL_ERROR_OFFSET) as *const u64 }; + let mut cpu = [0u64; 6]; + for idx in 0..4 { + unsafe { + cpu[idx] = core::ptr::read_volatile(cpu_frame_ptr.add(idx)); + } + } + let cpl = (cpu[2] & 3) as u8; + assert!(cpl != 0, "expected user-mode page fault"); + for idx in 4..6 { + unsafe { + cpu[idx] = core::ptr::read_volatile(cpu_frame_ptr.add(idx)); + } + } + + assert_eq!(cpu[0], frame.error_code, "error_code mismatch"); + assert_eq!(cpu[1], frame.rip, "rip mismatch"); + assert_eq!(cpu[2], frame.cs, "cs mismatch"); + assert_eq!(cpu[3], frame.rflags, "rflags mismatch"); + assert_eq!(cpu[4], frame.rsp, "rsp mismatch"); + assert_eq!(cpu[5], frame.ss, "ss mismatch"); + + USER_PF_FRAME_CHECK_STATE.store(2, Ordering::SeqCst); +} + #[cfg(test)] mod tests { use super::*; diff --git a/kernel/src/arch/x86_64/trap/mod.rs b/kernel/src/arch/x86_64/trap/mod.rs index 1aa3d8f..ce1b5f2 100644 --- a/kernel/src/arch/x86_64/trap/mod.rs +++ b/kernel/src/arch/x86_64/trap/mod.rs @@ -78,3 +78,16 @@ pub(super) fn build_trap_info(vector: u8, has_error: bool) -> TrapInfo { has_error_code: has_error, } } + +#[cfg(test)] +pub(crate) fn arm_user_pf_frame_check( + pid: crate::process::ProcessId, + expected_fault_addr: u64, +) { + handlers::arm_user_pf_frame_check(pid, expected_fault_addr); +} + +#[cfg(test)] +pub(crate) fn user_pf_frame_check_passed() -> bool { + handlers::user_pf_frame_check_passed() +} diff --git a/kernel/src/kernel_proc/linux_box.rs b/kernel/src/kernel_proc/linux_box.rs index c221cee..d995888 100644 --- a/kernel/src/kernel_proc/linux_box.rs +++ b/kernel/src/kernel_proc/linux_box.rs @@ -120,10 +120,15 @@ mod tests { use crate::fs::force_replace_root; use crate::fs::memfs::MemDirectory; use crate::interrupt::{INTERRUPTS, SYSTEM_TIMER, TimerTicks}; + use crate::loader::linux; use crate::println; use crate::process::PROCESS_TABLE; use crate::test::kernel_test_case; use crate::thread::{SCHEDULER, SchedulerError}; + use crate::arch::api::ArchThread; + use crate::arch::x86_64::{ + arm_user_pf_frame_check, user_pf_frame_check_passed, + }; /// ELF fixture generated by `xtask` (via `xtask-assets`) under `target/xtask-assets`. const LINUX_SYSCALL_ELF: &[u8] = include_bytes!(concat!( env!("CARGO_MANIFEST_DIR"), @@ -137,6 +142,11 @@ mod tests { env!("CARGO_MANIFEST_DIR"), "/../target/xtask-assets/linux-syscall-child.elf" )); + const LINUX_PAGE_FAULT_ELF: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../target/xtask-assets/linux-page-fault.elf" + )); + const LINUX_PAGE_FAULT_ADDR: u64 = 0xdeadbeef000; #[kernel_test_case] fn linux_binary_reads_stdin_and_file() { @@ -227,4 +237,75 @@ mod tests { SCHEDULER.shutdown(); } } + + #[kernel_test_case] + fn linux_page_fault_trap_frame_matches_cpu_frame() { + println!("[test] linux_page_fault_trap_frame_matches_cpu_frame"); + + let _ = PROCESS_TABLE.init_kernel(); + SCHEDULER.init().expect("scheduler init"); + let started = match SCHEDULER.start() { + Ok(()) => true, + Err(SchedulerError::AlreadyStarted) => false, + Err(err) => panic!("scheduler start failed: {:?}", err), + }; + + let root = MemDirectory::new(); + force_replace_root(root.clone()); + + let bin = root + .create_file("pf") + .expect("create page-fault fixture"); + let handle = bin.open(crate::fs::OpenOptions::new(0)).expect("open pf"); + let _ = handle + .write(LINUX_PAGE_FAULT_ELF) + .expect("write page-fault fixture"); + + let pid = PROCESS_TABLE + .create_user_process("linux-proc", crate::process::ProcessDomain::HostLinux) + .expect("create user process"); + arm_user_pf_frame_check(pid, LINUX_PAGE_FAULT_ADDR); + + let program = linux::load_elf(pid, "/pf").expect("load page-fault fixture"); + if let Ok(process) = PROCESS_TABLE.process_handle(pid) { + process.set_brk_base(program.heap_base); + } + let argv_refs = ["/pf"]; + let envp_refs: [&str; 0] = []; + let auxv = linux::build_auxv(&program, crate::mem::addr::PageSize::SIZE_4K.bytes()); + let stack_top = + ::user_stack_top(&program.user_stack); + let stack_pointer = PROCESS_TABLE + .address_space(pid) + .expect("user address space") + .with_page_table(|table, _| { + linux::initialise_stack_with_args_in_table( + table, stack_top, &argv_refs, &envp_refs, &auxv, + ) + }) + .expect("init page-fault stack"); + SCHEDULER + .spawn_user_thread_with_stack( + pid, + "linux-main", + program.entry, + program.user_stack, + stack_pointer, + ) + .expect("spawn page-fault thread"); + super::wait_for_exit(pid); + + assert!( + user_pf_frame_check_passed(), + "page-fault trap frame check did not complete" + ); + + if started { + SCHEDULER.shutdown(); + SYSTEM_TIMER + .start_periodic(TimerTicks::new(10_000_000)) + .expect("failed to restart system timer after page-fault test"); + INTERRUPTS.enable(); + } + } } diff --git a/xtask-assets/fixtures/linux-page-fault/DESIGN.md b/xtask-assets/fixtures/linux-page-fault/DESIGN.md new file mode 100644 index 0000000..79cc497 --- /dev/null +++ b/xtask-assets/fixtures/linux-page-fault/DESIGN.md @@ -0,0 +1,14 @@ +# Linux Page Fault Fixture Design Notes + +## Role and Scope +- Defines a minimal Linux userspace program that intentionally triggers a user-mode page fault. +- Used by kernel tests to validate trap-frame decoding for error-code exceptions. + +## Build +- Built by `xtask-assets` using the host C toolchain (`cc`/`gcc`/`clang`). +- Compiled as static, no-PIE, and libc-free; no syscalls are issued. +- Output is written to `target/xtask-assets/linux-page-fault.elf`. + +## Fault Contract +- Reads from an unmapped canonical address (`0xdeadbeef000`), which should raise #PF in ring 3. +- No normal exit path is required because the kernel terminates the faulting thread. diff --git a/xtask-assets/fixtures/linux-page-fault/main.c b/xtask-assets/fixtures/linux-page-fault/main.c new file mode 100644 index 0000000..002fd4b --- /dev/null +++ b/xtask-assets/fixtures/linux-page-fault/main.c @@ -0,0 +1,9 @@ +#include + +__attribute__((noreturn)) void _start(void) { + volatile uint64_t *ptr = (uint64_t *)0xdeadbeef000ULL; + (void)*ptr; + for (;;) { + __asm__ __volatile__("pause"); + } +} diff --git a/xtask-assets/src/lib.rs b/xtask-assets/src/lib.rs index e7f6a26..ecc7711 100644 --- a/xtask-assets/src/lib.rs +++ b/xtask-assets/src/lib.rs @@ -108,6 +108,29 @@ pub fn ensure_linux_syscall_child_elf(out_dir: &Path) -> io::Result { Ok(out_path) } +pub fn ensure_linux_page_fault_elf(out_dir: &Path) -> io::Result { + fs::create_dir_all(out_dir)?; + let out_path = out_dir.join("linux-page-fault.elf"); + + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let src_dir = manifest_dir.join("fixtures").join("linux-page-fault"); + let src = src_dir.join("main.c"); + if !src.exists() { + return Err(io::Error::new( + io::ErrorKind::NotFound, + "linux-page-fault fixture missing", + )); + } + + if !needs_rebuild_multi(&[&src], &out_path)? { + return Ok(out_path); + } + + build_linux_syscall_elf(&[&src], &out_path)?; + + Ok(out_path) +} + pub fn run_linux_syscall_host_test(out_dir: &Path) -> io::Result<()> { let elf = ensure_linux_syscall_elf(out_dir)?; let test_dir = out_dir.join("host-test"); diff --git a/xtask/src/lib.rs b/xtask/src/lib.rs index 297ace6..f957ea5 100644 --- a/xtask/src/lib.rs +++ b/xtask/src/lib.rs @@ -702,6 +702,8 @@ fn ensure_xtask_assets_dir() -> Result { .with_context(|| format!("ensure linux syscall adv elf in {}", assets_dir.display()))?; xtask_assets::ensure_linux_syscall_child_elf(&assets_dir) .with_context(|| format!("ensure linux syscall child elf in {}", assets_dir.display()))?; + xtask_assets::ensure_linux_page_fault_elf(&assets_dir) + .with_context(|| format!("ensure linux page fault elf in {}", assets_dir.display()))?; xtask_assets::run_linux_syscall_host_test(&assets_dir) .with_context(|| "run linux syscall host test")?; Ok(assets_dir) From d4c46cb8ebef4535fe9f6080d629f0ce84fa32bb Mon Sep 17 00:00:00 2001 From: n4mlz Date: Thu, 29 Jan 2026 17:50:26 +0900 Subject: [PATCH 18/25] fix: correct relocation address in user-mode tests for accuracy --- kernel/src/arch/x86_64/trap/handlers.rs | 23 +++++++---------- kernel/src/arch/x86_64/trap/stubs.rs | 33 ++++++++++++++++++++----- kernel/src/loader/linux/mod.rs | 4 +-- 3 files changed, 38 insertions(+), 22 deletions(-) diff --git a/kernel/src/arch/x86_64/trap/handlers.rs b/kernel/src/arch/x86_64/trap/handlers.rs index 27b087e..24cabc1 100644 --- a/kernel/src/arch/x86_64/trap/handlers.rs +++ b/kernel/src/arch/x86_64/trap/handlers.rs @@ -314,26 +314,21 @@ fn maybe_check_user_pf_frame(frame: &TrapFrame, fault_addr: u64) { let base = frame as *const TrapFrame as *const u8; let cpu_frame_ptr = unsafe { base.add(ORIGINAL_ERROR_OFFSET) as *const u64 }; - let mut cpu = [0u64; 6]; - for idx in 0..4 { + let mut cpu = [0u64; 5]; + for idx in 0..5 { unsafe { cpu[idx] = core::ptr::read_volatile(cpu_frame_ptr.add(idx)); } } - let cpl = (cpu[2] & 3) as u8; + let cpl = (cpu[1] & 3) as u8; assert!(cpl != 0, "expected user-mode page fault"); - for idx in 4..6 { - unsafe { - cpu[idx] = core::ptr::read_volatile(cpu_frame_ptr.add(idx)); - } - } - assert_eq!(cpu[0], frame.error_code, "error_code mismatch"); - assert_eq!(cpu[1], frame.rip, "rip mismatch"); - assert_eq!(cpu[2], frame.cs, "cs mismatch"); - assert_eq!(cpu[3], frame.rflags, "rflags mismatch"); - assert_eq!(cpu[4], frame.rsp, "rsp mismatch"); - assert_eq!(cpu[5], frame.ss, "ss mismatch"); + assert_eq!(frame.error_code, 4, "pf error_code unexpected"); + assert_eq!(cpu[0], frame.rip, "rip mismatch"); + assert_eq!(cpu[1], frame.cs, "cs mismatch"); + assert_eq!(cpu[2], frame.rflags, "rflags mismatch"); + assert_eq!(cpu[3], frame.rsp, "rsp mismatch"); + assert_eq!(cpu[4], frame.ss, "ss mismatch"); USER_PF_FRAME_CHECK_STATE.store(2, Ordering::SeqCst); } diff --git a/kernel/src/arch/x86_64/trap/stubs.rs b/kernel/src/arch/x86_64/trap/stubs.rs index dbc997c..4ff357d 100644 --- a/kernel/src/arch/x86_64/trap/stubs.rs +++ b/kernel/src/arch/x86_64/trap/stubs.rs @@ -103,16 +103,39 @@ macro_rules! define_trap_stub_with_error { mov rsi, rsp mov rax, [rsi + {orig_error_offset}] mov [rsi], rax + lea rdi, [rsi + {orig_error_offset}] + mov rbx, [rdi + 16] + test bx, 3 + jz 1f + mov rax, [rdi + 8] + mov rcx, [rdi + 16] + mov rdx, [rdi + 24] + mov r8, [rdi + 32] + mov r9, [rdi + 40] + mov [rdi], rax + mov [rdi + 8], rcx + mov [rdi + 16], rdx + mov [rdi + 24], r8 + mov [rdi + 32], r9 + jmp 2f + 1: + mov rax, [rdi + 8] + mov rcx, [rdi + 16] + mov rdx, [rdi + 24] + mov [rdi], rax + mov [rdi + 8], rcx + mov [rdi + 16], rdx + 2: mov r12, rsp and r12, 0xF - jz 1f + jz 3f sub rsp, 8 mov r12, 8 - jmp 2f - 1: + jmp 4f + 3: xor r12, r12 - 2: + 4: mov edi, {vector} mov edx, 1 @@ -137,8 +160,6 @@ macro_rules! define_trap_stub_with_error { pop r14 pop r15 - add rsp, 8 - iretq "#, vector = const $vector, diff --git a/kernel/src/loader/linux/mod.rs b/kernel/src/loader/linux/mod.rs index e7c7229..480c25e 100644 --- a/kernel/src/loader/linux/mod.rs +++ b/kernel/src/loader/linux/mod.rs @@ -425,7 +425,7 @@ mod tests { .address_space(pid) .expect("user address space"); let relocated = read_user_u64(&space, VirtAddr::new(0x400000 + 0x400)).expect("read reloc"); - assert_eq!(relocated, 0x400000 + 0x1234); + assert_eq!(relocated, 0x400000 + 0x234); } fn test_elf_image() -> Vec { @@ -535,7 +535,7 @@ mod tests { write_dyn(&mut buf[0x220..0x230], 9, 24); write_dyn(&mut buf[0x230..0x240], 0, 0); - write_rela(&mut buf[0x300..0x318], 0x400, 8, 0x1234); + write_rela(&mut buf[0x300..0x318], 0x400, 8, 0x234); buf } From 4987262d9f4b3fa5507690db71fb87eb1c18f05d Mon Sep 17 00:00:00 2001 From: n4mlz Date: Thu, 29 Jan 2026 17:51:47 +0900 Subject: [PATCH 19/25] tmp: enhance syscall rewrite logging and coverage verification --- kernel/src/loader/linux/map.rs | 32 +++- kernel/src/loader/linux/mod.rs | 2 + kernel/src/loader/linux/patch.rs | 317 +++++++++++++++++++++++++++++++ 3 files changed, 350 insertions(+), 1 deletion(-) diff --git a/kernel/src/loader/linux/map.rs b/kernel/src/loader/linux/map.rs index 54a4e97..dab57bc 100644 --- a/kernel/src/loader/linux/map.rs +++ b/kernel/src/loader/linux/map.rs @@ -9,7 +9,7 @@ use crate::mem::paging::{MapError, PhysMapper, TranslationError}; use super::LinuxLoadError; use super::add_base; use super::elf::{ElfFile, ProgramSegment}; -use super::patch::rewrite_syscalls_in_table; +use super::patch::{dump_range, rewrite_syscalls_in_table, verify_rewrite_coverage}; use alloc::vec::Vec; pub struct MappedSegment { @@ -134,7 +134,10 @@ fn map_single_segment 0 { crate::println!("[loader] rewrite syscalls in executable segment"); + debug_dump_rip_window(seg_vaddr, seg.file_size, table, "pre-rewrite"); rewrite_syscalls_in_table(seg_vaddr, seg.file_size, table).map_err(LinuxLoadError::from)?; + verify_rewrite_coverage(seg_vaddr, seg.file_size, table).map_err(LinuxLoadError::from)?; + debug_dump_rip_window(seg_vaddr, seg.file_size, table, "post-rewrite"); } Ok(Some(MappedSegment { @@ -145,6 +148,33 @@ fn map_single_segment( + seg_start: VirtAddr, + seg_size: usize, + table: &T, + label: &str, +) { + const RIP_DUMP_START: usize = 0x4d7390; + const RIP_DUMP_END: usize = 0x4d73d0; + if RIP_DUMP_START >= RIP_DUMP_END { + return; + } + let seg_end = match seg_start.as_raw().checked_add(seg_size) { + Some(end) => end, + None => return, + }; + if seg_start.as_raw() <= RIP_DUMP_START && seg_end >= RIP_DUMP_END { + let len = RIP_DUMP_END - RIP_DUMP_START; + if let Err(err) = dump_range(VirtAddr::new(RIP_DUMP_START), len, table, label) { + crate::println!( + "[loader] dump {label} failed start={:#x} err={:?}", + RIP_DUMP_START, + err + ); + } + } +} + fn copy_into_mapped( table: &T, dst: VirtAddr, diff --git a/kernel/src/loader/linux/mod.rs b/kernel/src/loader/linux/mod.rs index 480c25e..4dd72f7 100644 --- a/kernel/src/loader/linux/mod.rs +++ b/kernel/src/loader/linux/mod.rs @@ -115,6 +115,7 @@ where elf.ph_count, load_bias.as_raw() ); + patch::begin_rewrite_report(); let space: P::AddressSpace = PROCESS_TABLE .address_space(pid) @@ -170,6 +171,7 @@ where heap_base.as_raw(), phdr.as_raw() ); + patch::emit_rewrite_summary(); Ok(LinuxProgram { entry: add_base(load_bias, elf.entry)?, diff --git a/kernel/src/loader/linux/patch.rs b/kernel/src/loader/linux/patch.rs index 3798491..20e7ca9 100644 --- a/kernel/src/loader/linux/patch.rs +++ b/kernel/src/loader/linux/patch.rs @@ -1,6 +1,9 @@ use crate::mem::addr::{VirtAddr, VirtIntoPtr}; use crate::mem::manager; use crate::mem::paging::{PageTableOps, PhysMapper, TranslationError}; +use crate::util::spinlock::SpinLock; +use core::fmt; +use core::mem::MaybeUninit; /// Translate Linux `syscall` instructions to `int 0x80` so we can reuse the existing software /// interrupt handler until `SYSCALL/SYSRET` is wired up. @@ -25,12 +28,326 @@ pub fn rewrite_syscalls_in_table( let opcode = unsafe { core::ptr::read(ptr) }; let next = unsafe { core::ptr::read(ptr.add(1)) }; if opcode == 0x0F && next == 0x05 { + let context_start = addr.saturating_sub(CONTEXT_BYTES); + let mut before = [0u8; WINDOW_BYTES]; + let mut after = [0u8; WINDOW_BYTES]; + let before_ok = + read_bytes(table, VirtAddr::new(context_start), &mut before).is_ok(); + + let prev_byte = if addr > 0 { + read_byte(table, VirtAddr::new(addr - 1)).ok() + } else { + None + }; + let next_byte = read_byte(table, VirtAddr::new(addr + 2)).ok(); + let boundary_hint = boundary_hint(prev_byte); + unsafe { core::ptr::write(ptr, 0xCD); core::ptr::write(ptr.add(1), 0x80); } + + let after_ok = read_bytes(table, VirtAddr::new(context_start), &mut after).is_ok(); + record_rewrite(RewriteEntry { + vaddr: addr, + before, + after, + opcode_ok: opcode == 0x0F && next == 0x05, + before_ok, + after_ok, + prev: prev_byte.unwrap_or(0), + next: next_byte.unwrap_or(0), + hint: boundary_hint, + }); } offset = offset.saturating_add(1); } Ok(()) } + +pub fn begin_rewrite_report() { + let mut tracker = REWRITE_TRACKER.lock(); + tracker.reset(); +} + +pub fn verify_rewrite_coverage( + base: VirtAddr, + size: usize, + table: &T, +) -> Result<(), TranslationError> { + const PROBE_START: usize = 0x4d7300; + const PROBE_END: usize = 0x4d7400; + + let tracker = REWRITE_TRACKER.lock(); + if tracker.overflowed() { + panic!("[loader] rewrite log overflow; cannot verify cd 80 coverage"); + } + let mut offset = 0usize; + while offset + 1 < size { + let addr = match base.as_raw().checked_add(offset) { + Some(addr) => addr, + None => return Err(TranslationError::NotMapped), + }; + if addr < PROBE_START || addr + 1 >= PROBE_END { + offset = offset.saturating_add(1); + continue; + } + let opcode = read_byte(table, VirtAddr::new(addr))?; + let next = read_byte(table, VirtAddr::new(addr + 1))?; + if opcode == 0xCD && next == 0x80 && !tracker.contains(addr) { + panic!( + "[loader] unexpected cd 80 at vaddr={:#x} (not in rewrite log)", + addr + ); + } + offset = offset.saturating_add(1); + } + Ok(()) +} + +pub fn emit_rewrite_summary() { + const PROBE_START: usize = 0x4d7300; + const PROBE_END: usize = 0x4d7400; + let tracker = REWRITE_TRACKER.lock(); + crate::println!( + "[loader] rewrite summary total={} stored={} min={:#x} max={:#x}", + tracker.total, + tracker.stored, + tracker.min_addr(), + tracker.max_addr() + ); + + crate::print!("[loader] rewrite vaddrs:"); + for idx in 0..tracker.stored { + let entry = tracker.entry(idx); + crate::print!(" {:#x}", entry.vaddr); + } + if tracker.overflowed() { + crate::print!(" ... (truncated)"); + } + crate::print!("\n"); + + for idx in 0..tracker.stored { + let entry = tracker.entry(idx); + if entry.vaddr >= PROBE_START && entry.vaddr < PROBE_END { + crate::println!( + "[loader] rewrite probe vaddr={:#x} opcode_ok={} before_ok={} after_ok={} prev={:#04x} next={:#04x} hint={}", + entry.vaddr, + entry.opcode_ok, + entry.before_ok, + entry.after_ok, + entry.prev, + entry.next, + entry.hint + ); + crate::println!( + "[loader] rewrite probe before=[{}] after=[{}]", + HexBytes(&entry.before), + HexBytes(&entry.after) + ); + } + } +} + +pub fn dump_range( + start: VirtAddr, + len: usize, + table: &T, + label: &str, +) -> Result<(), TranslationError> { + const LINE_BYTES: usize = 16; + if len == 0 { + return Ok(()); + } + crate::println!( + "[loader] dump {label} start={:#x} len={:#x}", + start.as_raw(), + len + ); + let mut offset = 0usize; + while offset < len { + let line_len = core::cmp::min(LINE_BYTES, len - offset); + let mut buf = [0u8; LINE_BYTES]; + read_bytes( + table, + VirtAddr::new(start.as_raw().saturating_add(offset)), + &mut buf[..line_len], + )?; + crate::println!( + "[loader] dump {:#x}: {}", + start.as_raw().saturating_add(offset), + HexBytes(&buf[..line_len]) + ); + offset = offset.saturating_add(line_len); + } + Ok(()) +} + +const CONTEXT_BYTES: usize = 8; +const WINDOW_BYTES: usize = 16; +const REWRITE_LOG_CAPACITY: usize = 4096; + +static REWRITE_TRACKER: SpinLock = SpinLock::new(RewriteTracker::new()); + +fn read_byte(table: &T, addr: VirtAddr) -> Result { + let mapper = manager::phys_mapper(); + let phys = table.translate(addr)?; + let ptr = unsafe { mapper.phys_to_virt(phys).into_ptr() }; + Ok(unsafe { core::ptr::read(ptr) }) +} + +fn read_bytes( + table: &T, + start: VirtAddr, + out: &mut [u8], +) -> Result<(), TranslationError> { + for (idx, slot) in out.iter_mut().enumerate() { + let addr = start + .as_raw() + .checked_add(idx) + .ok_or(TranslationError::NotMapped)?; + *slot = read_byte(table, VirtAddr::new(addr))?; + } + Ok(()) +} + +fn boundary_hint(prev: Option) -> BoundaryHint { + match prev { + Some(0xC3) | Some(0xC2) | Some(0xCB) | Some(0xCA) | Some(0xCF) => BoundaryHint::Ret, + Some(0x90) => BoundaryHint::Nop, + Some(0xCC) => BoundaryHint::Int3, + Some(0x0F) => BoundaryHint::Overlap, + Some(0xE8) | Some(0xE9) | Some(0xEB) => BoundaryHint::Control, + _ => BoundaryHint::Unknown, + } +} + +#[derive(Copy, Clone)] +enum BoundaryHint { + Ret, + Nop, + Int3, + Overlap, + Control, + Unknown, +} + +impl fmt::Display for BoundaryHint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let text = match self { + BoundaryHint::Ret => "prev=ret", + BoundaryHint::Nop => "prev=nop", + BoundaryHint::Int3 => "prev=int3", + BoundaryHint::Overlap => "prev=0f (overlap?)", + BoundaryHint::Control => "prev=ctrl", + BoundaryHint::Unknown => "prev=unknown", + }; + f.write_str(text) + } +} + +struct HexBytes<'a>(&'a [u8]); + +impl fmt::Display for HexBytes<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for (idx, byte) in self.0.iter().enumerate() { + if idx > 0 { + f.write_str(" ")?; + } + write!(f, "{:02x}", byte)?; + } + Ok(()) + } +} + +#[derive(Copy, Clone)] +struct RewriteEntry { + vaddr: usize, + before: [u8; WINDOW_BYTES], + after: [u8; WINDOW_BYTES], + opcode_ok: bool, + before_ok: bool, + after_ok: bool, + prev: u8, + next: u8, + hint: BoundaryHint, +} + +struct RewriteTracker { + entries: [MaybeUninit; REWRITE_LOG_CAPACITY], + stored: usize, + total: usize, + min: usize, + max: usize, +} + +impl RewriteTracker { + const fn new() -> Self { + Self { + entries: [MaybeUninit::uninit(); REWRITE_LOG_CAPACITY], + stored: 0, + total: 0, + min: usize::MAX, + max: 0, + } + } + + fn reset(&mut self) { + self.stored = 0; + self.total = 0; + self.min = usize::MAX; + self.max = 0; + } + + fn record(&mut self, entry: RewriteEntry) { + self.total = self.total.saturating_add(1); + if entry.vaddr < self.min { + self.min = entry.vaddr; + } + if entry.vaddr > self.max { + self.max = entry.vaddr; + } + if self.stored < REWRITE_LOG_CAPACITY { + self.entries[self.stored].write(entry); + self.stored += 1; + } + } + + fn entry(&self, idx: usize) -> &RewriteEntry { + unsafe { self.entries[idx].assume_init_ref() } + } + + fn contains(&self, vaddr: usize) -> bool { + for idx in 0..self.stored { + if self.entry(idx).vaddr == vaddr { + return true; + } + } + false + } + + fn overflowed(&self) -> bool { + self.total > self.stored + } + + fn min_addr(&self) -> usize { + if self.total == 0 { + 0 + } else { + self.min + } + } + + fn max_addr(&self) -> usize { + if self.total == 0 { + 0 + } else { + self.max + } + } +} + +fn record_rewrite(entry: RewriteEntry) { + let mut tracker = REWRITE_TRACKER.lock(); + tracker.record(entry); +} From 29dd90083cb91a4f08e0b34224896c4e59495a71 Mon Sep 17 00:00:00 2001 From: n4mlz Date: Thu, 29 Jan 2026 18:24:10 +0900 Subject: [PATCH 20/25] fix: implement syscall emulation for #UD in user mode and enhance syscall rewrite logging --- kernel/src/arch/x86_64/trap/DESIGN.md | 1 + kernel/src/arch/x86_64/trap/handlers.rs | 81 +++++++- kernel/src/loader/linux/patch.rs | 237 +++++++++++++++++++++++- 3 files changed, 316 insertions(+), 3 deletions(-) diff --git a/kernel/src/arch/x86_64/trap/DESIGN.md b/kernel/src/arch/x86_64/trap/DESIGN.md index f814c9c..d9063be 100644 --- a/kernel/src/arch/x86_64/trap/DESIGN.md +++ b/kernel/src/arch/x86_64/trap/DESIGN.md @@ -32,6 +32,7 @@ - `handlers` provides the architecture-specific fast path for #PF/#GP/#DF, decoding hardware error codes and emitting structured diagnostics before panicking. Double-fault handling avoids logging and halts to reduce re-entrancy risk. - The dispatcher in `mod.rs` delegates to these helpers via `ArchTrap::handle_exception`; returning `true` suppresses the generic logging path. - Page-fault handling records the CR2 fault address and access type bits so future user-mode recovery logic has the required context. +- While `SYSCALL/SYSRET` is not wired up, #UD in user mode checks for the `0f 05` opcode and emulates a syscall as a temporary workaround; kernel-mode #UD remains fatal. - Expand syscall handling beyond `int 0x80` by enabling `SYSCALL/SYSRET` once MSR management is implemented. - Incorporate per-CPU IST buffers to support SMP and avoid contention on the global static region. diff --git a/kernel/src/arch/x86_64/trap/handlers.rs b/kernel/src/arch/x86_64/trap/handlers.rs index 24cabc1..f970db8 100644 --- a/kernel/src/arch/x86_64/trap/handlers.rs +++ b/kernel/src/arch/x86_64/trap/handlers.rs @@ -5,6 +5,7 @@ use crate::arch::api::ArchPageTableAccess; use crate::mem::paging::{PageTableOps, PhysMapper}; use crate::println; use crate::process::PROCESS_TABLE; +use crate::syscall::{self, SyscallInvocation}; use crate::thread::SCHEDULER; use crate::trap::TrapInfo; use crate::{ @@ -141,7 +142,10 @@ fn handle_general_protection(frame: &TrapFrame) { panic!("general protection fault"); } -fn handle_invalid_opcode(frame: &TrapFrame) { +fn handle_invalid_opcode(frame: &mut TrapFrame) { + if emulate_syscall_from_ud(frame) { + return; + } println!("[#UD] invalid opcode"); println!("[#UD] frame={:#?}", frame); panic!("invalid opcode"); @@ -216,6 +220,81 @@ fn dump_faulting_bytes(rip: usize) { } } +/// Emulate `syscall` on #UD in user mode as a compatibility workaround. +/// +/// # Implicit dependencies +/// - Relies on the current process address space being active and readable. +/// - Assumes the syscall ABI register layout matches the Linux `syscall` calling convention +/// (rax, rdi, rsi, rdx, r10, r8, r9). +/// - Assumes #UD is raised because `SYSCALL/SYSRET` MSRs are not wired yet. +fn emulate_syscall_from_ud(frame: &mut TrapFrame) -> bool { + let cpl = (frame.cs & 3) as u8; + if cpl == 0 { + return false; + } + let pid = match SCHEDULER.current_process_id() { + Some(pid) => pid, + None => return false, + }; + let process = match PROCESS_TABLE.process_handle(pid) { + Ok(proc) => proc, + Err(_) => return false, + }; + let rip = frame.rip as usize; + let mut bytes = [0u8; 2]; + let mut ok = true; + process.address_space().with_page_table(|table, _| { + for (idx, out) in bytes.iter_mut().enumerate() { + let addr = match rip.checked_add(idx) { + Some(addr) => addr, + None => { + ok = false; + break; + } + }; + let virt = VirtAddr::new(addr); + let phys = match table.translate(virt) { + Ok(phys) => phys, + Err(_) => { + ok = false; + break; + } + }; + let mapper = manager::phys_mapper(); + unsafe { + let ptr = mapper.phys_to_virt(phys).into_ptr(); + *out = core::ptr::read(ptr); + } + } + }); + if !ok || bytes != [0x0f, 0x05] { + return false; + } + + let abi = syscall::current_abi(); + let invocation = SyscallInvocation::new( + frame.regs.rax, + [ + frame.regs.rdi, + frame.regs.rsi, + frame.regs.rdx, + frame.regs.r10, + frame.regs.r8, + frame.regs.r9, + ], + ); + match syscall::dispatch_with_frame(abi, &invocation, Some(frame)) { + syscall::DispatchResult::Completed(result) => { + frame.regs.rax = syscall::encode_result(abi, result); + frame.rip = frame.rip.wrapping_add(2); + } + syscall::DispatchResult::Terminate(_code) => { + SCHEDULER.terminate_current(frame); + } + } + true +} + fn dump_trap_frame_qwords(frame: &TrapFrame) { let base = frame as *const TrapFrame as *const u64; let mut slots = [0u64; 12]; diff --git a/kernel/src/loader/linux/patch.rs b/kernel/src/loader/linux/patch.rs index 20e7ca9..ed04bfa 100644 --- a/kernel/src/loader/linux/patch.rs +++ b/kernel/src/loader/linux/patch.rs @@ -9,7 +9,8 @@ use core::mem::MaybeUninit; /// interrupt handler until `SYSCALL/SYSRET` is wired up. /// /// This is a heuristic scan that may produce false positives/negatives because it does not -/// decode instruction boundaries. It is intended as a temporary compatibility workaround. +/// decode instruction boundaries. It uses simple byte patterns to reduce the chance of +/// rewriting immediate or displacement data. pub fn rewrite_syscalls_in_table( base: VirtAddr, size: usize, @@ -28,6 +29,16 @@ pub fn rewrite_syscalls_in_table( let opcode = unsafe { core::ptr::read(ptr) }; let next = unsafe { core::ptr::read(ptr.add(1)) }; if opcode == 0x0F && next == 0x05 { + record_syscall_found(); + if !matches_syscall_pattern(table, addr) { + let context_start = addr.saturating_sub(CONTEXT_BYTES); + let mut context = [0u8; WINDOW_BYTES]; + let context_ok = + read_bytes(table, VirtAddr::new(context_start), &mut context).is_ok(); + record_syscall_skipped(addr, context, context_ok); + offset = offset.saturating_add(1); + continue; + } let context_start = addr.saturating_sub(CONTEXT_BYTES); let mut before = [0u8; WINDOW_BYTES]; let mut after = [0u8; WINDOW_BYTES]; @@ -110,7 +121,10 @@ pub fn emit_rewrite_summary() { const PROBE_END: usize = 0x4d7400; let tracker = REWRITE_TRACKER.lock(); crate::println!( - "[loader] rewrite summary total={} stored={} min={:#x} max={:#x}", + "[loader] rewrite summary found={} replaced={} skipped={} total={} stored={} min={:#x} max={:#x}", + tracker.found_total, + tracker.replaced_total, + tracker.skipped_total, tracker.total, tracker.stored, tracker.min_addr(), @@ -127,6 +141,27 @@ pub fn emit_rewrite_summary() { } crate::print!("\n"); + if tracker.skipped_total > 0 { + crate::print!("[loader] rewrite skipped:"); + for idx in 0..tracker.skipped_stored { + let entry = tracker.skipped_entry(idx); + crate::print!(" {:#x}", entry.vaddr); + } + if tracker.skipped_overflowed() { + crate::print!(" ... (truncated)"); + } + crate::print!("\n"); + for idx in 0..tracker.skipped_stored { + let entry = tracker.skipped_entry(idx); + crate::println!( + "[loader] rewrite skipped context vaddr={:#x} ok={} bytes=[{}]", + entry.vaddr, + entry.context_ok, + HexBytes(&entry.context) + ); + } + } + for idx in 0..tracker.stored { let entry = tracker.entry(idx); if entry.vaddr >= PROBE_START && entry.vaddr < PROBE_END { @@ -186,6 +221,7 @@ pub fn dump_range( const CONTEXT_BYTES: usize = 8; const WINDOW_BYTES: usize = 16; const REWRITE_LOG_CAPACITY: usize = 4096; +const SKIPPED_LOG_CAPACITY: usize = 64; static REWRITE_TRACKER: SpinLock = SpinLock::new(RewriteTracker::new()); @@ -279,6 +315,11 @@ struct RewriteTracker { total: usize, min: usize, max: usize, + found_total: usize, + replaced_total: usize, + skipped_total: usize, + skipped_entries: [MaybeUninit; SKIPPED_LOG_CAPACITY], + skipped_stored: usize, } impl RewriteTracker { @@ -289,6 +330,11 @@ impl RewriteTracker { total: 0, min: usize::MAX, max: 0, + found_total: 0, + replaced_total: 0, + skipped_total: 0, + skipped_entries: [MaybeUninit::uninit(); SKIPPED_LOG_CAPACITY], + skipped_stored: 0, } } @@ -297,6 +343,10 @@ impl RewriteTracker { self.total = 0; self.min = usize::MAX; self.max = 0; + self.found_total = 0; + self.replaced_total = 0; + self.skipped_total = 0; + self.skipped_stored = 0; } fn record(&mut self, entry: RewriteEntry) { @@ -317,6 +367,10 @@ impl RewriteTracker { unsafe { self.entries[idx].assume_init_ref() } } + fn skipped_entry(&self, idx: usize) -> &SkippedEntry { + unsafe { self.skipped_entries[idx].assume_init_ref() } + } + fn contains(&self, vaddr: usize) -> bool { for idx in 0..self.stored { if self.entry(idx).vaddr == vaddr { @@ -330,6 +384,10 @@ impl RewriteTracker { self.total > self.stored } + fn skipped_overflowed(&self) -> bool { + self.skipped_total > self.skipped_stored + } + fn min_addr(&self) -> usize { if self.total == 0 { 0 @@ -349,5 +407,180 @@ impl RewriteTracker { fn record_rewrite(entry: RewriteEntry) { let mut tracker = REWRITE_TRACKER.lock(); + tracker.replaced_total = tracker.replaced_total.saturating_add(1); tracker.record(entry); } + +fn record_syscall_found() { + let mut tracker = REWRITE_TRACKER.lock(); + tracker.found_total = tracker.found_total.saturating_add(1); +} + +fn record_syscall_skipped(vaddr: usize, context: [u8; WINDOW_BYTES], context_ok: bool) { + let mut tracker = REWRITE_TRACKER.lock(); + tracker.skipped_total = tracker.skipped_total.saturating_add(1); + if tracker.skipped_stored < SKIPPED_LOG_CAPACITY { + let idx = tracker.skipped_stored; + tracker.skipped_entries[idx].write(SkippedEntry { + vaddr, + context, + context_ok, + }); + tracker.skipped_stored = idx + 1; + } +} + +fn matches_syscall_pattern(table: &T, addr: usize) -> bool { + // Patterns anchored to the 0f 05 at `addr`. + // - b8 imm32 0f 05 + // - 48 c7 c0 imm32 0f 05 + // - 49 c7 c0 imm32 0f 05 + // - 31 c0 b0 imm8 0f 05 + if matches_b8_imm32_syscall(table, addr) { + return true; + } + if matches_rex_c7_c0_imm32_syscall(table, addr, 0x48) + || matches_rex_c7_c0_imm32_syscall(table, addr, 0x49) + { + return true; + } + if matches_xor_eax_mov_al_syscall(table, addr) { + return true; + } + if matches_syscall_followed_by_ret(table, addr) { + return true; + } + if matches_stack_arg_syscall(table, addr) { + return true; + } + if matches_store_load_syscall(table, addr) { + return true; + } + if matches_load_to_rdi_syscall(table, addr) { + return true; + } + if matches_nop_padded_syscall(table, addr) { + return true; + } + false +} + +fn matches_b8_imm32_syscall(table: &T, addr: usize) -> bool { + if addr < 5 { + return false; + } + read_byte(table, VirtAddr::new(addr - 5)) + .map(|b| b == 0xB8) + .unwrap_or(false) +} + +fn matches_rex_c7_c0_imm32_syscall( + table: &T, + addr: usize, + rex: u8, +) -> bool { + if addr < 7 { + return false; + } + let b0 = read_byte(table, VirtAddr::new(addr - 7)); + let b1 = read_byte(table, VirtAddr::new(addr - 6)); + let b2 = read_byte(table, VirtAddr::new(addr - 5)); + matches!(b0, Ok(v) if v == rex) + && matches!(b1, Ok(v) if v == 0xC7) + && matches!(b2, Ok(v) if v == 0xC0) +} + +fn matches_xor_eax_mov_al_syscall(table: &T, addr: usize) -> bool { + if addr < 4 { + return false; + } + let b0 = read_byte(table, VirtAddr::new(addr - 4)); + let b1 = read_byte(table, VirtAddr::new(addr - 3)); + let b2 = read_byte(table, VirtAddr::new(addr - 2)); + matches!(b0, Ok(v) if v == 0x31) + && matches!(b1, Ok(v) if v == 0xC0) + && matches!(b2, Ok(v) if v == 0xB0) +} + +fn matches_syscall_followed_by_ret(table: &T, addr: usize) -> bool { + let next = read_byte(table, VirtAddr::new(addr + 2)); + matches!(next, Ok(v) if v == 0xC3 || v == 0xC2) +} + +fn matches_stack_arg_syscall(table: &T, addr: usize) -> bool { + if addr < 8 { + return false; + } + let b0 = read_byte(table, VirtAddr::new(addr - 8)); + let b1 = read_byte(table, VirtAddr::new(addr - 7)); + let b2 = read_byte(table, VirtAddr::new(addr - 6)); + let b4 = read_byte(table, VirtAddr::new(addr - 4)); + let b5 = read_byte(table, VirtAddr::new(addr - 3)); + let b6 = read_byte(table, VirtAddr::new(addr - 2)); + matches!(b0, Ok(0x48)) + && matches!(b1, Ok(0x8b)) + && matches!(b2, Ok(0x75)) + && matches!(b4, Ok(0x48)) + && matches!(b5, Ok(0x8b)) + && matches!(b6, Ok(0x55)) +} + +fn matches_store_load_syscall(table: &T, addr: usize) -> bool { + if addr < 8 { + return false; + } + let b0 = read_byte(table, VirtAddr::new(addr - 8)); + let b1 = read_byte(table, VirtAddr::new(addr - 7)); + let b2 = read_byte(table, VirtAddr::new(addr - 6)); + let b4 = read_byte(table, VirtAddr::new(addr - 4)); + let b5 = read_byte(table, VirtAddr::new(addr - 3)); + let b6 = read_byte(table, VirtAddr::new(addr - 2)); + matches!(b0, Ok(0x48)) + && matches!(b1, Ok(0x89)) + && matches!(b2, Ok(0x7d)) + && matches!(b4, Ok(0x48)) + && matches!(b5, Ok(0x8b)) + && matches!(b6, Ok(0x45)) +} + +fn matches_load_to_rdi_syscall(table: &T, addr: usize) -> bool { + if addr < 7 { + return false; + } + let b0 = read_byte(table, VirtAddr::new(addr - 7)); + let b1 = read_byte(table, VirtAddr::new(addr - 6)); + let b2 = read_byte(table, VirtAddr::new(addr - 5)); + let b4 = read_byte(table, VirtAddr::new(addr - 3)); + let b5 = read_byte(table, VirtAddr::new(addr - 2)); + let b6 = read_byte(table, VirtAddr::new(addr - 1)); + matches!(b0, Ok(0x48)) + && matches!(b1, Ok(0x8b)) + && matches!(b2, Ok(v) if v == 0x55 || v == 0x75) + && matches!(b4, Ok(0x48)) + && matches!(b5, Ok(0x89)) + && matches!(b6, Ok(0xd7)) +} + +fn matches_nop_padded_syscall(table: &T, addr: usize) -> bool { + if addr < 8 { + return false; + } + let next = read_byte(table, VirtAddr::new(addr + 2)); + if !matches!(next, Ok(0x90)) { + return false; + } + for back in 1..=8 { + let byte = read_byte(table, VirtAddr::new(addr - back)); + if !matches!(byte, Ok(0x00)) { + return false; + } + } + true +} + +#[derive(Copy, Clone)] +struct SkippedEntry { + vaddr: usize, + context: [u8; WINDOW_BYTES], + context_ok: bool, +} From 4632672b90b0c4fe345bf11a183c9bb747e49631 Mon Sep 17 00:00:00 2001 From: n4mlz Date: Thu, 29 Jan 2026 18:36:11 +0900 Subject: [PATCH 21/25] feat: add support for getdents64 syscall and enhance directory handling in VFS --- kernel/src/fs/DESIGN.md | 2 + kernel/src/fs/fd.rs | 27 ++++ kernel/src/kernel_proc/linux_box.rs | 2 +- kernel/src/process/fs.rs | 16 +++ kernel/src/syscall/DESIGN.md | 8 +- kernel/src/syscall/linux.rs | 124 +++++++++++++++++- .../fixtures/linux-syscall-adv/DESIGN.md | 3 +- .../fixtures/linux-syscall-adv/libsyscall.c | 12 ++ .../fixtures/linux-syscall-adv/libsyscall.h | 4 + .../fixtures/linux-syscall-adv/main.c | 63 +++++++++ 10 files changed, 254 insertions(+), 7 deletions(-) diff --git a/kernel/src/fs/DESIGN.md b/kernel/src/fs/DESIGN.md index 794e81e..8e151e0 100644 --- a/kernel/src/fs/DESIGN.md +++ b/kernel/src/fs/DESIGN.md @@ -33,6 +33,8 @@ at the root so callers (process VFS ops, tar extraction, loader) share consistent behaviour. - Process FDs advance offsets on successful reads/writes. Write support is provided by filesystems that opt in (e.g. memfs); read-only filesystems return `ReadOnly`. +- Directory handles store a per-FD directory offset used by the Linux `getdents64` syscall; the + kernel advances the cursor after emitting each entry. - `File::seek` allows per-open offsets to be repositioned; regular file handles implement it while character devices and directories report `NotFile`. - Control-plane operations (ioctl-style) are routed through `File::ioctl` and only device-backed diff --git a/kernel/src/fs/fd.rs b/kernel/src/fs/fd.rs index 162138f..224e3e5 100644 --- a/kernel/src/fs/fd.rs +++ b/kernel/src/fs/fd.rs @@ -11,6 +11,7 @@ pub type Fd = u32; pub struct FdEntry { file: Arc, close_on_exec: bool, + dir_offset: u64, } impl FdEntry { @@ -18,6 +19,7 @@ impl FdEntry { Self { file, close_on_exec: false, + dir_offset: 0, } } @@ -32,6 +34,14 @@ impl FdEntry { pub fn set_close_on_exec(&mut self, value: bool) { self.close_on_exec = value; } + + pub fn dir_offset(&self) -> u64 { + self.dir_offset + } + + pub fn set_dir_offset(&mut self, value: u64) { + self.dir_offset = value; + } } pub struct FdTable { @@ -112,6 +122,23 @@ impl FdTable { guard.get(fd).cloned() } + pub fn dir_offset(&self, fd: Fd) -> Result { + let guard = self.inner.lock(); + let entry = guard.get(fd)?; + Ok(entry.dir_offset()) + } + + pub fn set_dir_offset(&self, fd: Fd, offset: u64) -> Result<(), VfsError> { + let mut guard = self.inner.lock(); + let entry = guard + .slots + .get_mut(fd as usize) + .and_then(|slot| slot.as_mut()) + .ok_or(VfsError::NotFound)?; + entry.set_dir_offset(offset); + Ok(()) + } + pub fn clone_from(&self, other: &FdTable) { let other_guard = other.inner.lock(); let mut guard = self.inner.lock(); diff --git a/kernel/src/kernel_proc/linux_box.rs b/kernel/src/kernel_proc/linux_box.rs index d995888..e35aa13 100644 --- a/kernel/src/kernel_proc/linux_box.rs +++ b/kernel/src/kernel_proc/linux_box.rs @@ -230,7 +230,7 @@ mod tests { let output = tty.drain_output(); assert_eq!( output, - b"WRITEV\nSTAT:OK\nIOCTL:OK\nMMAP:OK\nBRK:OK\nARCH:OK\nFORK:CHILD\nEXEC:CHILD\nWAIT:42\n" + b"WRITEV\nSTAT:OK\nDENTS:OK\nIOCTL:OK\nMMAP:OK\nBRK:OK\nARCH:OK\nFORK:CHILD\nEXEC:CHILD\nWAIT:42\n" ); if started { diff --git a/kernel/src/process/fs.rs b/kernel/src/process/fs.rs index 16159ae..6258abe 100644 --- a/kernel/src/process/fs.rs +++ b/kernel/src/process/fs.rs @@ -66,6 +66,22 @@ pub fn read_fd(pid: ProcessId, fd: Fd, buf: &mut [u8]) -> Result Result, VfsError> { + let process = process_handle(pid)?; + let entry = process.fd_table().entry(fd)?; + entry.file().readdir() +} + +pub fn dir_offset(pid: ProcessId, fd: Fd) -> Result { + let process = process_handle(pid)?; + process.fd_table().dir_offset(fd) +} + +pub fn set_dir_offset(pid: ProcessId, fd: Fd, offset: u64) -> Result<(), VfsError> { + let process = process_handle(pid)?; + process.fd_table().set_dir_offset(fd, offset) +} + pub fn write_fd(pid: ProcessId, fd: Fd, data: &[u8]) -> Result { let process = process_handle(pid)?; process.fd_table().write(fd, data) diff --git a/kernel/src/syscall/DESIGN.md b/kernel/src/syscall/DESIGN.md index 4cb9bfd..09f82b6 100644 --- a/kernel/src/syscall/DESIGN.md +++ b/kernel/src/syscall/DESIGN.md @@ -20,10 +20,10 @@ userland separation exists. - Linux dispatch implements a minimal set of process/syscall plumbing needed by static busybox: `read`, `write`, `open`, `close`, `writev`, `stat`, `brk`, `poll` (TTY-only, blocking until input), - `lseek` (currently reports `ESPIPE`), `getcwd`, `chdir`, `fork`, `execve`, `wait4`, `arch_prctl`, - `ioctl` (routed through `ControlOps`), `fcntl` (dup + FD_CLOEXEC), and basic process/session - metadata (`getppid`, `getpgrp`, `getpgid`, `setpgid`, `getsid`, `setsid`), plus `uname`, - `geteuid`, and stubbed signal + `lseek` (currently reports `ESPIPE`), `getcwd`, `getdents64`, `chdir`, `fork`, `execve`, `wait4`, + `arch_prctl`, `ioctl` (routed through `ControlOps`), `fcntl` (dup + FD_CLOEXEC), and basic + process/session metadata (`getppid`, `getpgrp`, `getpgid`, `setpgid`, `getsid`, `setsid`), plus + `uname`, `geteuid`, and stubbed signal calls. Unsupported numbers map to `ENOSYS`, while unsupported ioctls map to `ENOTTY`. - `/dev/tty` open assigns the global controlling TTY when the caller is a session leader and no controlling TTY is present yet; this is a minimal bridge until full tty/session semantics land. diff --git a/kernel/src/syscall/linux.rs b/kernel/src/syscall/linux.rs index 212e5a3..7c2e8eb 100644 --- a/kernel/src/syscall/linux.rs +++ b/kernel/src/syscall/linux.rs @@ -5,7 +5,7 @@ use alloc::vec::Vec; use crate::arch::Arch; use crate::arch::api::{ArchPageTableAccess, ArchThread}; -use crate::fs::NodeKind; +use crate::fs::{DirEntry, NodeKind}; use crate::interrupt::INTERRUPTS; use crate::mem::addr::{ Addr, MemPerm, Page, PageSize, VirtAddr, VirtIntoPtr, align_down, align_up, @@ -79,6 +79,7 @@ pub enum LinuxSyscall { GetPgid = 121, GetSid = 124, ArchPrctl = 158, + GetDents64 = 217, SetTidAddress = 218, ClockGetTime = 228, } @@ -124,6 +125,7 @@ impl LinuxSyscall { 121 => Some(Self::GetPgid), 124 => Some(Self::GetSid), 158 => Some(Self::ArchPrctl), + 217 => Some(Self::GetDents64), 218 => Some(Self::SetTidAddress), 228 => Some(Self::ClockGetTime), _ => None, @@ -198,6 +200,7 @@ pub fn dispatch( } Some(LinuxSyscall::GetPgid) => DispatchResult::Completed(handle_getpgid(invocation)), Some(LinuxSyscall::GetSid) => DispatchResult::Completed(handle_getsid(invocation)), + Some(LinuxSyscall::GetDents64) => DispatchResult::Completed(handle_getdents64(invocation)), Some(LinuxSyscall::ClockGetTime) => { DispatchResult::Completed(handle_clock_gettime(invocation)) } @@ -566,6 +569,66 @@ fn handle_getcwd(invocation: &SyscallInvocation) -> SysResult { Ok((bytes.len() + 1) as u64) } +/// Encode directory entries for `getdents64` using the per-fd cursor in `FdTable`. +/// +/// Implicit dependencies: +/// - The backing `File::readdir` returns entries in a stable order for the lifetime of the open +/// directory handle so the offset cursor remains valid across calls. +/// - Directory entries fit in the caller-provided buffer; if the buffer is too small for the +/// next entry, the syscall returns `EINVAL` instead of looping forever. +fn handle_getdents64(invocation: &SyscallInvocation) -> SysResult { + const ALIGN: usize = 8; + const HEADER_LEN: usize = 8 + 8 + 2 + 1; + + let pid = current_pid()?; + let fd = invocation.arg(0).ok_or(SysError::InvalidArgument)?; + let dir_ptr = invocation.arg(1).ok_or(SysError::InvalidArgument)?; + let count = invocation.arg(2).ok_or(SysError::InvalidArgument)?; + let count = usize::try_from(count).map_err(|_| SysError::InvalidArgument)?; + if count == 0 { + return Ok(0); + } + + let entries = proc_fs::read_dir_fd(pid, fd as u32).map_err(|_| SysError::InvalidArgument)?; + let mut index = proc_fs::dir_offset(pid, fd as u32) + .map_err(|_| SysError::InvalidArgument)? as usize; + if index >= entries.len() { + return Ok(0); + } + + let base = VirtAddr::new(dir_ptr as usize); + let mut written = 0usize; + while index < entries.len() { + let entry = &entries[index]; + let reclen = dirent64_reclen(entry.name.as_bytes().len(), HEADER_LEN, ALIGN)?; + if written + reclen > count { + break; + } + write_dirent64( + base, + written, + entry, + (index + 1) as u64, + reclen, + HEADER_LEN, + )?; + written += reclen; + index += 1; + } + + if written == 0 && index < entries.len() { + let entry = &entries[index]; + let min = dirent64_reclen(entry.name.as_bytes().len(), HEADER_LEN, ALIGN)?; + if min > count { + return Err(SysError::InvalidArgument); + } + } + + proc_fs::set_dir_offset(pid, fd as u32, index as u64) + .map_err(|_| SysError::InvalidArgument)?; + Ok(written as u64) +} + fn handle_chdir(invocation: &SyscallInvocation) -> SysResult { let pid = current_pid()?; let ptr = invocation.arg(0).ok_or(SysError::InvalidArgument)?; @@ -580,6 +643,65 @@ fn handle_chdir(invocation: &SyscallInvocation) -> SysResult { Ok(0) } +fn dirent64_reclen(name_len: usize, header_len: usize, align: usize) -> Result { + let base = header_len + .checked_add(name_len) + .and_then(|value| value.checked_add(1)) + .ok_or(SysError::InvalidArgument)?; + let reclen = align_up(base, align); + if reclen > u16::MAX as usize { + return Err(SysError::InvalidArgument); + } + Ok(reclen) +} + +fn write_dirent64( + base: VirtAddr, + offset: usize, + entry: &DirEntry, + next_offset: u64, + reclen: usize, + header_len: usize, +) -> Result<(), SysError> { + let dst = base.checked_add(offset).ok_or(SysError::BadAddress)?; + let mut header = [0u8; 8 + 8 + 2 + 1]; + header[0..8].copy_from_slice(&0u64.to_ne_bytes()); + header[8..16].copy_from_slice(&next_offset.to_ne_bytes()); + header[16..18].copy_from_slice(&(reclen as u16).to_ne_bytes()); + header[18] = dirent_type(entry.stat.kind); + copy_to_user(dst, &header).map_err(|_| SysError::BadAddress)?; + + let name = entry.name.as_bytes(); + let name_dst = dst + .checked_add(header_len) + .ok_or(SysError::BadAddress)?; + copy_to_user(name_dst, name).map_err(|_| SysError::BadAddress)?; + let nul_dst = name_dst + .checked_add(name.len()) + .ok_or(SysError::BadAddress)?; + copy_to_user(nul_dst, &[0]).map_err(|_| SysError::BadAddress)?; + + let pad = reclen.saturating_sub(header_len + name.len() + 1); + if pad > 0 { + let pad_dst = nul_dst.checked_add(1).ok_or(SysError::BadAddress)?; + let zeros = [0u8; 8]; + copy_to_user(pad_dst, &zeros[..pad]).map_err(|_| SysError::BadAddress)?; + } + Ok(()) +} + +fn dirent_type(kind: NodeKind) -> u8 { + match kind { + NodeKind::Regular => 8, + NodeKind::Directory => 4, + NodeKind::Symlink => 10, + NodeKind::CharDevice => 2, + NodeKind::BlockDevice => 6, + NodeKind::Pipe => 1, + NodeKind::Socket => 12, + } +} + fn handle_uname(invocation: &SyscallInvocation) -> SysResult { let pid = current_pid()?; let addr = invocation.arg(0).ok_or(SysError::InvalidArgument)?; diff --git a/xtask-assets/fixtures/linux-syscall-adv/DESIGN.md b/xtask-assets/fixtures/linux-syscall-adv/DESIGN.md index 80477d5..6c8e0c7 100644 --- a/xtask-assets/fixtures/linux-syscall-adv/DESIGN.md +++ b/xtask-assets/fixtures/linux-syscall-adv/DESIGN.md @@ -2,7 +2,8 @@ ## Purpose - Exercise Linux-compatible syscalls beyond the minimal read/write/open/close path. -- Cover `writev`, `stat`, `ioctl` (TIOCGWINSZ), `mmap`, `munmap`, `brk`, `arch_prctl`, `fork`, `execve`, and `wait4` in one deterministic run. +- Cover `writev`, `stat`, `getdents64`, `ioctl` (TIOCGWINSZ), `mmap`, `munmap`, `brk`, `arch_prctl`, + `fork`, `execve`, and `wait4` in one deterministic run. ## Notes - Built as a static/PIE ELF with no libc dependency. diff --git a/xtask-assets/fixtures/linux-syscall-adv/libsyscall.c b/xtask-assets/fixtures/linux-syscall-adv/libsyscall.c index a3381a4..c506381 100644 --- a/xtask-assets/fixtures/linux-syscall-adv/libsyscall.c +++ b/xtask-assets/fixtures/linux-syscall-adv/libsyscall.c @@ -72,6 +72,14 @@ isize sys_writev(int fd, const struct iovec *iov, int iovcnt) { return sys_call3(SYS_writev, fd, (isize)iov, iovcnt); } +isize sys_open(const char *path, int flags, int mode) { + return sys_call3(SYS_open, (isize)path, flags, mode); +} + +isize sys_close(int fd) { + return sys_call1(SYS_close, fd); +} + isize sys_stat(const char *path, struct linux_stat *statbuf) { return sys_call3(SYS_stat, (isize)path, (isize)statbuf, 0); } @@ -108,6 +116,10 @@ isize sys_wait4(isize pid, int *status, int options, void *rusage) { return sys_call4(SYS_wait4, pid, (isize)status, options, (isize)rusage); } +isize sys_getdents64(int fd, void *dirp, usize count) { + return sys_call3(SYS_getdents64, fd, (isize)dirp, (isize)count); +} + __attribute__((noreturn)) void sys_exit(int code) { sys_call1(SYS_exit, code); for (;;) { diff --git a/xtask-assets/fixtures/linux-syscall-adv/libsyscall.h b/xtask-assets/fixtures/linux-syscall-adv/libsyscall.h index bbbb371..d613990 100644 --- a/xtask-assets/fixtures/linux-syscall-adv/libsyscall.h +++ b/xtask-assets/fixtures/linux-syscall-adv/libsyscall.h @@ -24,6 +24,7 @@ enum { SYS_exit = 60, SYS_wait4 = 61, SYS_arch_prctl = 158, + SYS_getdents64 = 217, }; enum { @@ -65,6 +66,8 @@ isize sys_call6(isize num, isize arg1, isize arg2, isize arg3, isize arg4, isize isize sys_write(int fd, const void *buf, usize len); isize sys_writev(int fd, const struct iovec *iov, int iovcnt); +isize sys_open(const char *path, int flags, int mode); +isize sys_close(int fd); isize sys_stat(const char *path, struct linux_stat *statbuf); isize sys_ioctl(int fd, isize request, void *argp); isize sys_mmap(void *addr, usize len, int prot, int flags, int fd, isize offset); @@ -74,6 +77,7 @@ isize sys_arch_prctl(isize code, isize addr); isize sys_fork(void); isize sys_execve(const char *path, const char *const *argv, const char *const *envp); isize sys_wait4(isize pid, int *status, int options, void *rusage); +isize sys_getdents64(int fd, void *dirp, usize count); __attribute__((noreturn)) void sys_exit(int code); #endif diff --git a/xtask-assets/fixtures/linux-syscall-adv/main.c b/xtask-assets/fixtures/linux-syscall-adv/main.c index e1ba087..9fee251 100644 --- a/xtask-assets/fixtures/linux-syscall-adv/main.c +++ b/xtask-assets/fixtures/linux-syscall-adv/main.c @@ -34,6 +34,41 @@ static void write_u32(u32 value) { } } +static int str_eq(const char *a, const char *b) { + usize i = 0; + while (a[i] || b[i]) { + if (a[i] != b[i]) { + return 0; + } + i++; + } + return 1; +} + +struct linux_dirent64 { + u64 d_ino; + u64 d_off; + u16 d_reclen; + unsigned char d_type; + char d_name[]; +} __attribute__((packed)); + +static int scan_dirents(const char *buf, usize len, const char *target) { + usize off = 0; + int found = 0; + while (off + 19 <= len) { + const struct linux_dirent64 *ent = (const struct linux_dirent64 *)(buf + off); + if (ent->d_reclen < 19 || off + ent->d_reclen > len) { + break; + } + if (str_eq(ent->d_name, target)) { + found = 1; + } + off += ent->d_reclen; + } + return found; +} + void _start(void) { enum { PROT_READ = 0x1, @@ -58,6 +93,34 @@ void _start(void) { write_str("STAT:BAD\n"); } + int dir_fd = (int)sys_open("/", 0, 0); + if (dir_fd < 0) { + write_str("DENTS:BAD\n"); + } else { + char dents[512]; + int found = 0; + int ok = 1; + for (int i = 0; i < 8; i++) { + isize read = sys_getdents64(dir_fd, dents, sizeof(dents)); + if (read == 0) { + break; + } + if (read < 0) { + ok = 0; + break; + } + if (scan_dirents(dents, (usize)read, stat_path)) { + found = 1; + } + } + sys_close(dir_fd); + if (ok && found) { + write_str("DENTS:OK\n"); + } else { + write_str("DENTS:BAD\n"); + } + } + struct { u16 ws_row; u16 ws_col; From 876996009b5b1294a361b560278b7dbba78f2af2 Mon Sep 17 00:00:00 2001 From: n4mlz Date: Thu, 29 Jan 2026 18:55:51 +0900 Subject: [PATCH 22/25] feat: add lstat syscall implementation and enhance related tests --- kernel/src/kernel_proc/linux_box.rs | 5 ++- kernel/src/process/fs.rs | 23 +++++++++++++ kernel/src/syscall/DESIGN.md | 7 ++-- kernel/src/syscall/linux.rs | 32 +++++++++++++++++++ .../fixtures/linux-syscall-adv/DESIGN.md | 4 +-- .../fixtures/linux-syscall-adv/libsyscall.c | 4 +++ .../fixtures/linux-syscall-adv/libsyscall.h | 2 ++ .../fixtures/linux-syscall-adv/main.c | 7 ++++ 8 files changed, 78 insertions(+), 6 deletions(-) diff --git a/kernel/src/kernel_proc/linux_box.rs b/kernel/src/kernel_proc/linux_box.rs index e35aa13..897f97e 100644 --- a/kernel/src/kernel_proc/linux_box.rs +++ b/kernel/src/kernel_proc/linux_box.rs @@ -210,6 +210,9 @@ mod tests { .open(crate::fs::OpenOptions::new(0)) .expect("open stat.txt"); let _ = handle.write(b"STATDATA").expect("write stat.txt"); + let _ = root + .create_symlink("stat-link", "stat.txt") + .expect("create stat-link"); let adv = root.create_file("adv").expect("create adv"); let handle = adv.open(crate::fs::OpenOptions::new(0)).expect("open adv"); @@ -230,7 +233,7 @@ mod tests { let output = tty.drain_output(); assert_eq!( output, - b"WRITEV\nSTAT:OK\nDENTS:OK\nIOCTL:OK\nMMAP:OK\nBRK:OK\nARCH:OK\nFORK:CHILD\nEXEC:CHILD\nWAIT:42\n" + b"WRITEV\nSTAT:OK\nLSTAT:OK\nDENTS:OK\nIOCTL:OK\nMMAP:OK\nBRK:OK\nARCH:OK\nFORK:CHILD\nEXEC:CHILD\nWAIT:42\n" ); if started { diff --git a/kernel/src/process/fs.rs b/kernel/src/process/fs.rs index 6258abe..4af2bcd 100644 --- a/kernel/src/process/fs.rs +++ b/kernel/src/process/fs.rs @@ -162,6 +162,29 @@ pub fn stat_path(pid: ProcessId, raw_path: &str) -> Result Result { + let process = process_handle(pid)?; + let abs = Path::resolve(raw_path, &process.cwd())?; + if abs.components().is_empty() { + return with_process_vfs(&process, |vfs| vfs.stat_absolute(&abs)); + } + let parent = abs.parent().ok_or(VfsError::InvalidPath)?; + let name = abs + .components() + .last() + .ok_or(VfsError::InvalidPath)? + .clone(); + with_process_vfs(&process, |vfs| { + let dir = vfs.resolve_node(&parent)?; + let dir_view = dir.as_dir().ok_or(VfsError::NotDirectory)?; + let node = dir_view.lookup(&name)?; + node.stat() + }) +} + pub fn remove_path(pid: ProcessId, raw_path: &str) -> Result<(), VfsError> { let process = process_handle(pid)?; let abs = Path::resolve(raw_path, &process.cwd())?; diff --git a/kernel/src/syscall/DESIGN.md b/kernel/src/syscall/DESIGN.md index 09f82b6..a09605d 100644 --- a/kernel/src/syscall/DESIGN.md +++ b/kernel/src/syscall/DESIGN.md @@ -19,9 +19,10 @@ process using the container VFS. Host pointers are treated as kernel-mapped addresses until userland separation exists. - Linux dispatch implements a minimal set of process/syscall plumbing needed by static busybox: - `read`, `write`, `open`, `close`, `writev`, `stat`, `brk`, `poll` (TTY-only, blocking until input), - `lseek` (currently reports `ESPIPE`), `getcwd`, `getdents64`, `chdir`, `fork`, `execve`, `wait4`, - `arch_prctl`, `ioctl` (routed through `ControlOps`), `fcntl` (dup + FD_CLOEXEC), and basic + `read`, `write`, `open`, `close`, `writev`, `stat`, `lstat`, `brk`, `poll` (TTY-only, blocking + until input), `lseek` (currently reports `ESPIPE`), `getcwd`, `getdents64`, `chdir`, `fork`, + `execve`, `wait4`, `arch_prctl`, `ioctl` (routed through `ControlOps`), `fcntl` (dup + FD_CLOEXEC), + and basic process/session metadata (`getppid`, `getpgrp`, `getpgid`, `setpgid`, `getsid`, `setsid`), plus `uname`, `geteuid`, and stubbed signal calls. Unsupported numbers map to `ENOSYS`, while unsupported ioctls map to `ENOTTY`. diff --git a/kernel/src/syscall/linux.rs b/kernel/src/syscall/linux.rs index 7c2e8eb..496a2fa 100644 --- a/kernel/src/syscall/linux.rs +++ b/kernel/src/syscall/linux.rs @@ -47,6 +47,7 @@ pub enum LinuxSyscall { Open = 2, Close = 3, Stat = 4, + Lstat = 6, Poll = 7, Lseek = 8, Mmap = 9, @@ -93,6 +94,7 @@ impl LinuxSyscall { 2 => Some(Self::Open), 3 => Some(Self::Close), 4 => Some(Self::Stat), + 6 => Some(Self::Lstat), 7 => Some(Self::Poll), 8 => Some(Self::Lseek), 9 => Some(Self::Mmap), @@ -165,6 +167,7 @@ pub fn dispatch( Some(LinuxSyscall::Close) => DispatchResult::Completed(handle_close(invocation)), Some(LinuxSyscall::Writev) => DispatchResult::Completed(handle_writev(invocation)), Some(LinuxSyscall::Stat) => DispatchResult::Completed(handle_stat(invocation)), + Some(LinuxSyscall::Lstat) => DispatchResult::Completed(handle_lstat(invocation)), Some(LinuxSyscall::Poll) => DispatchResult::Completed(handle_poll(invocation)), Some(LinuxSyscall::Lseek) => DispatchResult::Completed(handle_lseek(invocation)), Some(LinuxSyscall::Mmap) => DispatchResult::Completed(handle_mmap(invocation)), @@ -530,6 +533,35 @@ fn handle_stat(invocation: &SyscallInvocation) -> SysResult { Ok(0) } +fn handle_lstat(invocation: &SyscallInvocation) -> SysResult { + let path_ptr = invocation.arg(0).ok_or(SysError::InvalidArgument)?; + let stat_ptr = invocation.arg(1).ok_or(SysError::InvalidArgument)?; + + let pid = current_pid()?; + let process = PROCESS_TABLE + .process_handle(pid) + .map_err(|_| SysError::InvalidArgument)?; + let path = process.address_space().with_page_table(|table, _| { + let user = UserMemoryAccess::new(table); + read_cstring_with_user(&user, path_ptr) + })?; + let stat = proc_fs::stat_path_no_follow(pid, &path).map_err(|err| match err { + crate::fs::VfsError::NotFound => SysError::NotFound, + _ => SysError::InvalidArgument, + })?; + + let mode = mode_from_meta(stat.kind); + let stat = LinuxStat::from_meta(mode, stat.size); + let dst = VirtAddr::new(stat_ptr as usize); + process.address_space().with_page_table(|table, _| { + let user = UserMemoryAccess::new(table); + user.write_bytes(dst, stat.as_bytes()) + .map_err(|_| SysError::BadAddress)?; + Ok::<(), SysError>(()) + })?; + Ok(0) +} + fn handle_lseek(invocation: &SyscallInvocation) -> SysResult { let pid = current_pid()?; let fd = invocation.arg(0).ok_or(SysError::InvalidArgument)?; diff --git a/xtask-assets/fixtures/linux-syscall-adv/DESIGN.md b/xtask-assets/fixtures/linux-syscall-adv/DESIGN.md index 6c8e0c7..cdc5273 100644 --- a/xtask-assets/fixtures/linux-syscall-adv/DESIGN.md +++ b/xtask-assets/fixtures/linux-syscall-adv/DESIGN.md @@ -2,8 +2,8 @@ ## Purpose - Exercise Linux-compatible syscalls beyond the minimal read/write/open/close path. -- Cover `writev`, `stat`, `getdents64`, `ioctl` (TIOCGWINSZ), `mmap`, `munmap`, `brk`, `arch_prctl`, - `fork`, `execve`, and `wait4` in one deterministic run. +- Cover `writev`, `stat`, `lstat`, `getdents64`, `ioctl` (TIOCGWINSZ), `mmap`, `munmap`, `brk`, + `arch_prctl`, `fork`, `execve`, and `wait4` in one deterministic run. ## Notes - Built as a static/PIE ELF with no libc dependency. diff --git a/xtask-assets/fixtures/linux-syscall-adv/libsyscall.c b/xtask-assets/fixtures/linux-syscall-adv/libsyscall.c index c506381..71b404a 100644 --- a/xtask-assets/fixtures/linux-syscall-adv/libsyscall.c +++ b/xtask-assets/fixtures/linux-syscall-adv/libsyscall.c @@ -84,6 +84,10 @@ isize sys_stat(const char *path, struct linux_stat *statbuf) { return sys_call3(SYS_stat, (isize)path, (isize)statbuf, 0); } +isize sys_lstat(const char *path, struct linux_stat *statbuf) { + return sys_call3(SYS_lstat, (isize)path, (isize)statbuf, 0); +} + isize sys_ioctl(int fd, isize request, void *argp) { return sys_call3(SYS_ioctl, fd, request, (isize)argp); } diff --git a/xtask-assets/fixtures/linux-syscall-adv/libsyscall.h b/xtask-assets/fixtures/linux-syscall-adv/libsyscall.h index d613990..3f44c21 100644 --- a/xtask-assets/fixtures/linux-syscall-adv/libsyscall.h +++ b/xtask-assets/fixtures/linux-syscall-adv/libsyscall.h @@ -14,6 +14,7 @@ enum { SYS_open = 2, SYS_close = 3, SYS_stat = 4, + SYS_lstat = 6, SYS_mmap = 9, SYS_munmap = 11, SYS_brk = 12, @@ -69,6 +70,7 @@ isize sys_writev(int fd, const struct iovec *iov, int iovcnt); isize sys_open(const char *path, int flags, int mode); isize sys_close(int fd); isize sys_stat(const char *path, struct linux_stat *statbuf); +isize sys_lstat(const char *path, struct linux_stat *statbuf); isize sys_ioctl(int fd, isize request, void *argp); isize sys_mmap(void *addr, usize len, int prot, int flags, int fd, isize offset); isize sys_munmap(void *addr, usize len); diff --git a/xtask-assets/fixtures/linux-syscall-adv/main.c b/xtask-assets/fixtures/linux-syscall-adv/main.c index 9fee251..bbdcae2 100644 --- a/xtask-assets/fixtures/linux-syscall-adv/main.c +++ b/xtask-assets/fixtures/linux-syscall-adv/main.c @@ -1,6 +1,7 @@ #include "libsyscall.h" static const char stat_path[] = "stat.txt"; +static const char lstat_path[] = "stat-link"; static const char child_path[] = "/child"; static const usize page_size = 4096; @@ -93,6 +94,12 @@ void _start(void) { write_str("STAT:BAD\n"); } + if (sys_lstat(lstat_path, &st) == 0 && (st.st_mode & 0170000) == 0120000) { + write_str("LSTAT:OK\n"); + } else { + write_str("LSTAT:BAD\n"); + } + int dir_fd = (int)sys_open("/", 0, 0); if (dir_fd < 0) { write_str("DENTS:BAD\n"); From 6ddb0c6136e7465ce6574432d68054e392dd6217 Mon Sep 17 00:00:00 2001 From: n4mlz Date: Thu, 29 Jan 2026 19:25:51 +0900 Subject: [PATCH 23/25] refactor: kernel loader and syscall handling --- kernel/src/arch/x86_64/mod.rs | 5 +- kernel/src/arch/x86_64/trap/handlers.rs | 237 ++------------ kernel/src/arch/x86_64/trap/mod.rs | 5 +- kernel/src/arch/x86_64/trap/stubs.rs | 23 -- kernel/src/device/tty/mod.rs | 24 -- kernel/src/kernel_proc/linux_box.rs | 15 +- kernel/src/loader/linux/map.rs | 50 +-- kernel/src/loader/linux/mod.rs | 42 +-- kernel/src/loader/linux/patch.rs | 396 +----------------------- kernel/src/loader/linux/reloc.rs | 88 ++---- kernel/src/loader/linux/stack.rs | 8 +- kernel/src/process/fs.rs | 14 +- kernel/src/process/mod.rs | 4 - kernel/src/syscall/linux.rs | 96 +----- kernel/src/syscall/mod.rs | 21 +- kernel/src/thread/mod.rs | 26 -- 16 files changed, 74 insertions(+), 980 deletions(-) diff --git a/kernel/src/arch/x86_64/mod.rs b/kernel/src/arch/x86_64/mod.rs index 2bf4dc7..f7145d0 100644 --- a/kernel/src/arch/x86_64/mod.rs +++ b/kernel/src/arch/x86_64/mod.rs @@ -99,10 +99,7 @@ pub fn halt() { } #[cfg(test)] -pub(crate) fn arm_user_pf_frame_check( - pid: crate::process::ProcessId, - expected_fault_addr: u64, -) { +pub(crate) fn arm_user_pf_frame_check(pid: crate::process::ProcessId, expected_fault_addr: u64) { trap::arm_user_pf_frame_check(pid, expected_fault_addr); } diff --git a/kernel/src/arch/x86_64/trap/handlers.rs b/kernel/src/arch/x86_64/trap/handlers.rs index f970db8..a4f3d05 100644 --- a/kernel/src/arch/x86_64/trap/handlers.rs +++ b/kernel/src/arch/x86_64/trap/handlers.rs @@ -3,25 +3,22 @@ use x86_64::registers::control::Cr2; use crate::arch::api::ArchPageTableAccess; use crate::mem::paging::{PageTableOps, PhysMapper}; -use crate::println; +use crate::mem::{ + addr::{VirtAddr, VirtIntoPtr}, + manager, +}; use crate::process::PROCESS_TABLE; use crate::syscall::{self, SyscallInvocation}; use crate::thread::SCHEDULER; use crate::trap::TrapInfo; -use crate::{ - mem::{ - addr::{VirtAddr, VirtIntoPtr}, - manager, - }, -}; use super::TrapFrame; #[cfg(test)] use super::context::ORIGINAL_ERROR_OFFSET; #[cfg(test)] -use core::sync::atomic::{AtomicU64, AtomicU8, Ordering}; -#[cfg(test)] use crate::process::ProcessId; +#[cfg(test)] +use core::sync::atomic::{AtomicU8, AtomicU64, Ordering}; #[cfg(test)] static USER_PF_FRAME_CHECK_STATE: AtomicU8 = AtomicU8::new(0); @@ -46,9 +43,7 @@ pub fn handle_exception(info: TrapInfo, frame: &mut TrapFrame) -> bool { // Be careful with logging/locking here: traps can occur while locks are held or // interrupts are disabled, and double faults must avoid re-entrancy entirely. match info.vector { - 14 => { - handle_page_fault(frame) - } + 14 => handle_page_fault(frame), 6 => { handle_invalid_opcode(frame); true @@ -65,89 +60,36 @@ pub fn handle_exception(info: TrapInfo, frame: &mut TrapFrame) -> bool { fn handle_page_fault(frame: &mut TrapFrame) -> bool { let fault_addr = Cr2::read().expect("CR2 must contain a canonical address"); let code = frame.error_code; - let fs_base = x86_64::registers::model_specific::FsBase::read().as_u64(); - let cpl = (frame.cs & 3) as u8; - - let present = (code & 1) != 0; - let write = (code & 1 << 1) != 0; let user = (code & 1 << 2) != 0 || (frame.cs & 3) != 0; - let reserved = (code & 1 << 3) != 0; - let instruction = (code & 1 << 4) != 0; #[cfg(test)] maybe_check_user_pf_frame(frame, fault_addr.as_u64()); - println!( - "[#PF] fault_addr={:#x} present={} write={} user={} reserved={} instruction={} fs_base={:#x}", - fault_addr.as_u64(), - present, - write, - user, - reserved, - instruction, - fs_base - ); - println!( - "[#PF] rip={:#x} cs={:#x} rsp={:#x} ss={:#x} cpl={}", - frame.rip, - frame.cs, - frame.rsp, - frame.ss, - cpl - ); - dump_trap_frame_qwords(frame); - dump_frame_field_hints(frame); - if fault_addr.as_u64() >= fs_base { - println!( - "[#PF] fault_addr-fs_base={:#x}", - fault_addr.as_u64() - fs_base - ); - } - if let Some(pid) = SCHEDULER.current_process_id() { - if let Ok(process) = PROCESS_TABLE.process_handle(pid) { - let brk = process.brk_state(); - println!( - "[#PF] pid={} brk_base={:#x} brk_current={:#x}", - pid, - brk.base.as_raw(), - brk.current.as_raw() - ); - } - } - if let Some(stack) = SCHEDULER.current_user_stack_info() { - println!( - "[#PF] user_stack base={:#x} size={:#x}", - stack.base.as_raw(), - stack.size - ); - } - dump_faulting_bytes(frame.rip as usize); - println!("[#PF] frame={:#?}", frame); if user { - if let Some(pid) = SCHEDULER.current_process_id() { - if let Ok(process) = PROCESS_TABLE.process_handle(pid) { - // Use a conventional non-zero status for user faults. - process.set_exit_code(139); - } + if let Some(pid) = SCHEDULER.current_process_id() + && let Ok(process) = PROCESS_TABLE.process_handle(pid) + { + // Use a conventional non-zero status for user faults. + process.set_exit_code(139); } SCHEDULER.terminate_current(frame); return true; } - panic!("page fault while executing in kernel context"); + panic!( + "page fault in kernel: addr={:#x} code={:#x}", + fault_addr.as_u64(), + code + ); } fn handle_general_protection(frame: &TrapFrame) { - println!("[#GP] error_code={:#x}", frame.error_code); - println!("[#GP] frame={:#?}", frame); - panic!("general protection fault"); + panic!("general protection fault: code={:#x}", frame.error_code); } fn handle_invalid_opcode(frame: &mut TrapFrame) { if emulate_syscall_from_ud(frame) { return; } - println!("[#UD] invalid opcode"); - println!("[#UD] frame={:#?}", frame); panic!("invalid opcode"); } @@ -160,66 +102,6 @@ fn handle_double_fault(_frame: &TrapFrame) -> bool { } } -fn dump_faulting_bytes(rip: usize) { - let pid = match SCHEDULER.current_process_id() { - Some(pid) => pid, - None => return, - }; - let process = match PROCESS_TABLE.process_handle(pid) { - Ok(proc) => proc, - Err(_) => return, - }; - let mut bytes = [0u8; 16]; - let mut ok = true; - process.address_space().with_page_table(|table, _| { - for (idx, out) in bytes.iter_mut().enumerate() { - let addr = match rip.checked_add(idx) { - Some(addr) => addr, - None => { - ok = false; - break; - } - }; - let virt = VirtAddr::new(addr); - let phys = match table.translate(virt) { - Ok(phys) => phys, - Err(_) => { - ok = false; - break; - } - }; - let mapper = manager::phys_mapper(); - unsafe { - let ptr = mapper.phys_to_virt(phys).into_ptr(); - *out = core::ptr::read(ptr); - } - } - }); - if ok { - println!( - "[#PF] bytes @ rip: {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x}", - bytes[0], - bytes[1], - bytes[2], - bytes[3], - bytes[4], - bytes[5], - bytes[6], - bytes[7], - bytes[8], - bytes[9], - bytes[10], - bytes[11], - bytes[12], - bytes[13], - bytes[14], - bytes[15] - ); - } else { - println!("[#PF] bytes @ rip: "); - } -} - /// Emulate `syscall` on #UD in user mode as a compatibility workaround. /// /// # Implicit dependencies @@ -295,85 +177,6 @@ fn emulate_syscall_from_ud(frame: &mut TrapFrame) -> bool { true } -fn dump_trap_frame_qwords(frame: &TrapFrame) { - let base = frame as *const TrapFrame as *const u64; - let mut slots = [0u64; 12]; - for (idx, slot) in slots.iter_mut().enumerate() { - unsafe { - *slot = core::ptr::read_volatile(base.add(idx)); - } - } - println!( - "[#PF] frame qwords: {:016x} {:016x} {:016x} {:016x} {:016x} {:016x} {:016x} {:016x} {:016x} {:016x} {:016x} {:016x}", - slots[0], - slots[1], - slots[2], - slots[3], - slots[4], - slots[5], - slots[6], - slots[7], - slots[8], - slots[9], - slots[10], - slots[11] - ); -} - -fn dump_frame_field_hints(frame: &TrapFrame) { - let pid = SCHEDULER.current_process_id(); - let (brk_base, brk_current) = pid - .and_then(|pid| PROCESS_TABLE.process_handle(pid).ok()) - .map(|proc| { - let brk = proc.brk_state(); - (Some(brk.base.as_raw()), Some(brk.current.as_raw())) - }) - .unwrap_or((None, None)); - let (stack_base, stack_size) = SCHEDULER - .current_user_stack_info() - .map(|stack| (Some(stack.base.as_raw()), Some(stack.size))) - .unwrap_or((None, None)); - - dump_field_hint("rip", frame.rip, brk_base, brk_current, stack_base, stack_size); - dump_field_hint("cs", frame.cs, brk_base, brk_current, stack_base, stack_size); - dump_field_hint("rflags", frame.rflags, brk_base, brk_current, stack_base, stack_size); - dump_field_hint("rsp", frame.rsp, brk_base, brk_current, stack_base, stack_size); - dump_field_hint("ss", frame.ss, brk_base, brk_current, stack_base, stack_size); -} - -fn dump_field_hint( - name: &str, - value: u64, - brk_base: Option, - brk_current: Option, - stack_base: Option, - stack_size: Option, -) { - let is_small = value <= 0xffff; - let in_brk = match (brk_base, brk_current) { - (Some(base), Some(current)) => { - let addr = value as usize; - addr >= base && addr < current - } - _ => false, - }; - let in_stack = match (stack_base, stack_size) { - (Some(base), Some(size)) => { - let addr = value as usize; - addr >= base && addr < base.saturating_add(size) - } - _ => false, - }; - println!( - "[#PF] {}={:#x} small={} in_brk={} in_stack={}", - name, - value, - is_small, - in_brk, - in_stack - ); -} - #[cfg(test)] fn maybe_check_user_pf_frame(frame: &TrapFrame, fault_addr: u64) { if USER_PF_FRAME_CHECK_STATE.load(Ordering::SeqCst) != 1 { @@ -391,8 +194,7 @@ fn maybe_check_user_pf_frame(frame: &TrapFrame, fault_addr: u64) { // Read the CPU-pushed exception frame starting at ORIGINAL_ERROR_OFFSET. let base = frame as *const TrapFrame as *const u8; - let cpu_frame_ptr = - unsafe { base.add(ORIGINAL_ERROR_OFFSET) as *const u64 }; + let cpu_frame_ptr = unsafe { base.add(ORIGINAL_ERROR_OFFSET) as *const u64 }; let mut cpu = [0u64; 5]; for idx in 0..5 { unsafe { @@ -416,6 +218,7 @@ fn maybe_check_user_pf_frame(frame: &TrapFrame, fault_addr: u64) { mod tests { use super::*; use crate::arch::x86_64::trap::context::GeneralRegisters; + use crate::println; use crate::test::kernel_test_case; use crate::trap::TrapOrigin; diff --git a/kernel/src/arch/x86_64/trap/mod.rs b/kernel/src/arch/x86_64/trap/mod.rs index ce1b5f2..a954a19 100644 --- a/kernel/src/arch/x86_64/trap/mod.rs +++ b/kernel/src/arch/x86_64/trap/mod.rs @@ -80,10 +80,7 @@ pub(super) fn build_trap_info(vector: u8, has_error: bool) -> TrapInfo { } #[cfg(test)] -pub(crate) fn arm_user_pf_frame_check( - pid: crate::process::ProcessId, - expected_fault_addr: u64, -) { +pub(crate) fn arm_user_pf_frame_check(pid: crate::process::ProcessId, expected_fault_addr: u64) { handlers::arm_user_pf_frame_check(pid, expected_fault_addr); } diff --git a/kernel/src/arch/x86_64/trap/stubs.rs b/kernel/src/arch/x86_64/trap/stubs.rs index 4ff357d..dc07864 100644 --- a/kernel/src/arch/x86_64/trap/stubs.rs +++ b/kernel/src/arch/x86_64/trap/stubs.rs @@ -282,29 +282,6 @@ pub(super) const EXTERNAL_INTERRUPT_STUBS: [unsafe extern "C" fn() -> !; #[unsafe(no_mangle)] pub(super) unsafe extern "C" fn dispatch_trap(vector: u8, frame: *mut TrapFrame, has_error: u8) { - if vector == 14 { - let frame_ptr = frame as *const u64; - let cpu_frame_ptr = (frame as *const u8).wrapping_add(ORIGINAL_ERROR_OFFSET) as *const u64; - let mut cpu_words = [0u64; 6]; - for (idx, slot) in cpu_words.iter_mut().enumerate() { - *slot = core::ptr::read_volatile(cpu_frame_ptr.add(idx)); - } - crate::println!( - "[#PF] frame_ptr={:#x} cpu_frame_ptr={:#x} has_error={}", - frame_ptr as usize, - cpu_frame_ptr as usize, - has_error - ); - crate::println!( - "[#PF] cpu_frame qwords: {:016x} {:016x} {:016x} {:016x} {:016x} {:016x}", - cpu_words[0], - cpu_words[1], - cpu_words[2], - cpu_words[3], - cpu_words[4], - cpu_words[5] - ); - } let frame = unsafe { &mut *frame }; let info = build_trap_info(vector, has_error != 0); ::dispatch_trap(info, frame); diff --git a/kernel/src/device/tty/mod.rs b/kernel/src/device/tty/mod.rs index 440679f..7e77913 100644 --- a/kernel/src/device/tty/mod.rs +++ b/kernel/src/device/tty/mod.rs @@ -11,8 +11,6 @@ use crate::util::lazylock::LazyLock; use crate::util::spinlock::SpinLock; use crate::util::stream::{ControlError, ControlOps, ControlRequest, ReadOps, WriteOps}; -const DEBUG_TTY: bool = true; - const TTY_BUFFER_LIMIT: usize = 4096; const IOCTL_TCGETS: u64 = 0x5401; @@ -303,14 +301,6 @@ impl TtyDevice { fn require_controlling_tty(&self) -> Result { let proc = self.current_process().ok_or(ControlError::Invalid)?; if !proc.has_controlling_tty() { - if DEBUG_TTY { - crate::println!( - "[tty] missing controlling tty pid={} pgrp={} sid={}", - proc.id(), - proc.pgrp_id(), - proc.session_id() - ); - } return Err(ControlError::Invalid); } Ok(proc) @@ -417,14 +407,6 @@ impl ControlOps for TtyDevice { IOCTL_TIOCSCTTY => { let proc = self.current_process().ok_or(ControlError::Invalid)?; let pid = proc.id(); - if DEBUG_TTY { - crate::println!( - "[tty] TIOCSCTTY pid={} sid={} has_ctty={}", - pid, - proc.session_id(), - proc.has_controlling_tty() - ); - } if proc.session_id() != pid { return Err(ControlError::Invalid); } @@ -444,9 +426,6 @@ impl ControlOps for TtyDevice { pgrp = current; self.set_pgrp(pgrp); } - if DEBUG_TTY { - crate::println!("[tty] TIOCGPGRP -> {}", pgrp); - } let pgrp = pgrp as i32; request.write_struct(&pgrp)?; Ok(0) @@ -454,9 +433,6 @@ impl ControlOps for TtyDevice { IOCTL_TIOCSPGRP => { let _ = self.require_controlling_tty()?; let pgrp = request.read_struct::()?; - if DEBUG_TTY { - crate::println!("[tty] TIOCSPGRP set pgrp {}", pgrp); - } if pgrp < 0 { return Err(ControlError::Invalid); } diff --git a/kernel/src/kernel_proc/linux_box.rs b/kernel/src/kernel_proc/linux_box.rs index 897f97e..923c5b4 100644 --- a/kernel/src/kernel_proc/linux_box.rs +++ b/kernel/src/kernel_proc/linux_box.rs @@ -4,10 +4,10 @@ use alloc::string::{String, ToString}; use crate::arch::api::ArchPageTableAccess; use crate::fs::{Path, VfsError}; +use crate::interrupt::INTERRUPTS; use crate::loader::linux::{self, LinuxLoadError}; use crate::process::fs as proc_fs; use crate::process::{PROCESS_TABLE, ProcessError, ProcessId}; -use crate::interrupt::INTERRUPTS; use crate::thread::{SCHEDULER, SpawnError}; /// Errors surfaced while launching or supervising a Linux guest process. @@ -115,6 +115,8 @@ fn absolute_path(origin_pid: ProcessId, raw: &str) -> Result { #[cfg(test)] mod tests { use super::*; + use crate::arch::api::ArchThread; + use crate::arch::x86_64::{arm_user_pf_frame_check, user_pf_frame_check_passed}; use crate::device::tty::global_tty; use crate::fs::DirNode; use crate::fs::force_replace_root; @@ -125,10 +127,6 @@ mod tests { use crate::process::PROCESS_TABLE; use crate::test::kernel_test_case; use crate::thread::{SCHEDULER, SchedulerError}; - use crate::arch::api::ArchThread; - use crate::arch::x86_64::{ - arm_user_pf_frame_check, user_pf_frame_check_passed, - }; /// ELF fixture generated by `xtask` (via `xtask-assets`) under `target/xtask-assets`. const LINUX_SYSCALL_ELF: &[u8] = include_bytes!(concat!( env!("CARGO_MANIFEST_DIR"), @@ -256,9 +254,7 @@ mod tests { let root = MemDirectory::new(); force_replace_root(root.clone()); - let bin = root - .create_file("pf") - .expect("create page-fault fixture"); + let bin = root.create_file("pf").expect("create page-fault fixture"); let handle = bin.open(crate::fs::OpenOptions::new(0)).expect("open pf"); let _ = handle .write(LINUX_PAGE_FAULT_ELF) @@ -276,8 +272,7 @@ mod tests { let argv_refs = ["/pf"]; let envp_refs: [&str; 0] = []; let auxv = linux::build_auxv(&program, crate::mem::addr::PageSize::SIZE_4K.bytes()); - let stack_top = - ::user_stack_top(&program.user_stack); + let stack_top = ::user_stack_top(&program.user_stack); let stack_pointer = PROCESS_TABLE .address_space(pid) .expect("user address space") diff --git a/kernel/src/loader/linux/map.rs b/kernel/src/loader/linux/map.rs index dab57bc..96a52fd 100644 --- a/kernel/src/loader/linux/map.rs +++ b/kernel/src/loader/linux/map.rs @@ -9,7 +9,7 @@ use crate::mem::paging::{MapError, PhysMapper, TranslationError}; use super::LinuxLoadError; use super::add_base; use super::elf::{ElfFile, ProgramSegment}; -use super::patch::{dump_range, rewrite_syscalls_in_table, verify_rewrite_coverage}; +use super::patch::rewrite_syscalls_in_table; use alloc::vec::Vec; pub struct MappedSegment { @@ -68,13 +68,6 @@ fn map_single_segment seg.file_size { - crate::println!( - "[loader] zero bss vaddr={:#x} len={:#x}", - seg_vaddr.as_raw() + seg.file_size, - seg.mem_size - seg.file_size - ); let bss_start = seg_vaddr .as_raw() .checked_add(seg.file_size) @@ -133,11 +116,7 @@ fn map_single_segment 0 { - crate::println!("[loader] rewrite syscalls in executable segment"); - debug_dump_rip_window(seg_vaddr, seg.file_size, table, "pre-rewrite"); rewrite_syscalls_in_table(seg_vaddr, seg.file_size, table).map_err(LinuxLoadError::from)?; - verify_rewrite_coverage(seg_vaddr, seg.file_size, table).map_err(LinuxLoadError::from)?; - debug_dump_rip_window(seg_vaddr, seg.file_size, table, "post-rewrite"); } Ok(Some(MappedSegment { @@ -148,33 +127,6 @@ fn map_single_segment( - seg_start: VirtAddr, - seg_size: usize, - table: &T, - label: &str, -) { - const RIP_DUMP_START: usize = 0x4d7390; - const RIP_DUMP_END: usize = 0x4d73d0; - if RIP_DUMP_START >= RIP_DUMP_END { - return; - } - let seg_end = match seg_start.as_raw().checked_add(seg_size) { - Some(end) => end, - None => return, - }; - if seg_start.as_raw() <= RIP_DUMP_START && seg_end >= RIP_DUMP_END { - let len = RIP_DUMP_END - RIP_DUMP_START; - if let Err(err) = dump_range(VirtAddr::new(RIP_DUMP_START), len, table, label) { - crate::println!( - "[loader] dump {label} failed start={:#x} err={:?}", - RIP_DUMP_START, - err - ); - } - } -} - fn copy_into_mapped( table: &T, dst: VirtAddr, diff --git a/kernel/src/loader/linux/mod.rs b/kernel/src/loader/linux/mod.rs index 4dd72f7..881ab5d 100644 --- a/kernel/src/loader/linux/mod.rs +++ b/kernel/src/loader/linux/mod.rs @@ -103,19 +103,10 @@ pub fn load_elf_with_platform

( where P: ArchLinuxElfPlatform, { - crate::println!("[loader] load_elf pid={} path={}", pid, raw_path); let abs = resolve_path(pid, raw_path)?; let elf_bytes = proc_fs::read_to_end_at(pid, &abs)?; let elf = elf::ElfFile::parse::

(&elf_bytes)?; let load_bias = choose_load_bias::

(&elf)?; - crate::println!( - "[loader] elf type={:?} entry={:#x} phnum={} load_bias={:#x}", - elf.elf_type, - elf.entry.as_raw(), - elf.ph_count, - load_bias.as_raw() - ); - patch::begin_rewrite_report(); let space: P::AddressSpace = PROCESS_TABLE .address_space(pid) @@ -123,56 +114,31 @@ where // NOTE: The loader does not validate user address ranges; it assumes a fresh // address space (or equivalent) before mapping the ELF image. - crate::println!("[loader] mapping segments count={}", elf.segments.len()); let mapped = map::map_segments::

(&space, &elf, &elf_bytes, load_bias)?; if let Some(dynamic) = elf.dynamic.as_ref() { - crate::println!("[loader] dynamic segment present"); space.with_page_table(|table, _| { let info = reloc::read_dynamic_info(table, load_bias, dynamic)?; - crate::println!( - "[loader] relocations rela={} rel={} relr={} jmprel={}", - info.rela_size, - info.rel_size, - info.relr_size, - info.jmprel_size - ); reloc::apply_relocations(table, load_bias, &info, &mapped)?; Ok::<(), LinuxLoadError>(()) })?; } space.with_page_table(|table, _| { map::apply_segment_permissions(table, &mapped)?; - if elf.interp.is_some() { - if let Some(relro) = elf.relro.as_ref() { - crate::println!("[loader] applying GNU_RELRO"); - reloc::apply_gnu_relro(table, load_bias, relro, &mapped)?; - } - } else if elf.relro.is_some() { - crate::println!("[loader] GNU_RELRO skipped (no PT_INTERP)"); + if elf.interp.is_some() + && let Some(relro) = elf.relro.as_ref() + { + reloc::apply_gnu_relro(table, load_bias, relro, &mapped)?; } Ok::<(), LinuxLoadError>(()) })?; let user_stack = P::allocate_user_stack(&space, 32 * 1024)?; let stack_top = P::user_stack_top(&user_stack); - crate::println!( - "[loader] allocated user stack top={:#x}", - stack_top.as_raw() - ); let stack_pointer = space .with_page_table(|table, _| stack::initialise_minimal_stack(table, stack_top)) .map_err(LinuxLoadError::from)?; let heap_base = compute_heap_base::

(load_bias, &elf)?; let phdr = compute_phdr_address(load_bias, &elf, &mapped)?; - crate::println!( - "[loader] entry={:#x} stack={:#x} heap_base={:#x} phdr={:#x}", - add_base(load_bias, elf.entry)?.as_raw(), - stack_pointer.as_raw(), - heap_base.as_raw(), - phdr.as_raw() - ); - patch::emit_rewrite_summary(); - Ok(LinuxProgram { entry: add_base(load_bias, elf.entry)?, user_stack, diff --git a/kernel/src/loader/linux/patch.rs b/kernel/src/loader/linux/patch.rs index ed04bfa..5950dfb 100644 --- a/kernel/src/loader/linux/patch.rs +++ b/kernel/src/loader/linux/patch.rs @@ -1,9 +1,6 @@ use crate::mem::addr::{VirtAddr, VirtIntoPtr}; use crate::mem::manager; use crate::mem::paging::{PageTableOps, PhysMapper, TranslationError}; -use crate::util::spinlock::SpinLock; -use core::fmt; -use core::mem::MaybeUninit; /// Translate Linux `syscall` instructions to `int 0x80` so we can reuse the existing software /// interrupt handler until `SYSCALL/SYSRET` is wired up. @@ -29,202 +26,20 @@ pub fn rewrite_syscalls_in_table( let opcode = unsafe { core::ptr::read(ptr) }; let next = unsafe { core::ptr::read(ptr.add(1)) }; if opcode == 0x0F && next == 0x05 { - record_syscall_found(); if !matches_syscall_pattern(table, addr) { - let context_start = addr.saturating_sub(CONTEXT_BYTES); - let mut context = [0u8; WINDOW_BYTES]; - let context_ok = - read_bytes(table, VirtAddr::new(context_start), &mut context).is_ok(); - record_syscall_skipped(addr, context, context_ok); offset = offset.saturating_add(1); continue; } - let context_start = addr.saturating_sub(CONTEXT_BYTES); - let mut before = [0u8; WINDOW_BYTES]; - let mut after = [0u8; WINDOW_BYTES]; - let before_ok = - read_bytes(table, VirtAddr::new(context_start), &mut before).is_ok(); - - let prev_byte = if addr > 0 { - read_byte(table, VirtAddr::new(addr - 1)).ok() - } else { - None - }; - let next_byte = read_byte(table, VirtAddr::new(addr + 2)).ok(); - let boundary_hint = boundary_hint(prev_byte); - unsafe { core::ptr::write(ptr, 0xCD); core::ptr::write(ptr.add(1), 0x80); } - - let after_ok = read_bytes(table, VirtAddr::new(context_start), &mut after).is_ok(); - record_rewrite(RewriteEntry { - vaddr: addr, - before, - after, - opcode_ok: opcode == 0x0F && next == 0x05, - before_ok, - after_ok, - prev: prev_byte.unwrap_or(0), - next: next_byte.unwrap_or(0), - hint: boundary_hint, - }); - } - offset = offset.saturating_add(1); - } - Ok(()) -} - -pub fn begin_rewrite_report() { - let mut tracker = REWRITE_TRACKER.lock(); - tracker.reset(); -} - -pub fn verify_rewrite_coverage( - base: VirtAddr, - size: usize, - table: &T, -) -> Result<(), TranslationError> { - const PROBE_START: usize = 0x4d7300; - const PROBE_END: usize = 0x4d7400; - - let tracker = REWRITE_TRACKER.lock(); - if tracker.overflowed() { - panic!("[loader] rewrite log overflow; cannot verify cd 80 coverage"); - } - let mut offset = 0usize; - while offset + 1 < size { - let addr = match base.as_raw().checked_add(offset) { - Some(addr) => addr, - None => return Err(TranslationError::NotMapped), - }; - if addr < PROBE_START || addr + 1 >= PROBE_END { - offset = offset.saturating_add(1); - continue; - } - let opcode = read_byte(table, VirtAddr::new(addr))?; - let next = read_byte(table, VirtAddr::new(addr + 1))?; - if opcode == 0xCD && next == 0x80 && !tracker.contains(addr) { - panic!( - "[loader] unexpected cd 80 at vaddr={:#x} (not in rewrite log)", - addr - ); } offset = offset.saturating_add(1); } Ok(()) } -pub fn emit_rewrite_summary() { - const PROBE_START: usize = 0x4d7300; - const PROBE_END: usize = 0x4d7400; - let tracker = REWRITE_TRACKER.lock(); - crate::println!( - "[loader] rewrite summary found={} replaced={} skipped={} total={} stored={} min={:#x} max={:#x}", - tracker.found_total, - tracker.replaced_total, - tracker.skipped_total, - tracker.total, - tracker.stored, - tracker.min_addr(), - tracker.max_addr() - ); - - crate::print!("[loader] rewrite vaddrs:"); - for idx in 0..tracker.stored { - let entry = tracker.entry(idx); - crate::print!(" {:#x}", entry.vaddr); - } - if tracker.overflowed() { - crate::print!(" ... (truncated)"); - } - crate::print!("\n"); - - if tracker.skipped_total > 0 { - crate::print!("[loader] rewrite skipped:"); - for idx in 0..tracker.skipped_stored { - let entry = tracker.skipped_entry(idx); - crate::print!(" {:#x}", entry.vaddr); - } - if tracker.skipped_overflowed() { - crate::print!(" ... (truncated)"); - } - crate::print!("\n"); - for idx in 0..tracker.skipped_stored { - let entry = tracker.skipped_entry(idx); - crate::println!( - "[loader] rewrite skipped context vaddr={:#x} ok={} bytes=[{}]", - entry.vaddr, - entry.context_ok, - HexBytes(&entry.context) - ); - } - } - - for idx in 0..tracker.stored { - let entry = tracker.entry(idx); - if entry.vaddr >= PROBE_START && entry.vaddr < PROBE_END { - crate::println!( - "[loader] rewrite probe vaddr={:#x} opcode_ok={} before_ok={} after_ok={} prev={:#04x} next={:#04x} hint={}", - entry.vaddr, - entry.opcode_ok, - entry.before_ok, - entry.after_ok, - entry.prev, - entry.next, - entry.hint - ); - crate::println!( - "[loader] rewrite probe before=[{}] after=[{}]", - HexBytes(&entry.before), - HexBytes(&entry.after) - ); - } - } -} - -pub fn dump_range( - start: VirtAddr, - len: usize, - table: &T, - label: &str, -) -> Result<(), TranslationError> { - const LINE_BYTES: usize = 16; - if len == 0 { - return Ok(()); - } - crate::println!( - "[loader] dump {label} start={:#x} len={:#x}", - start.as_raw(), - len - ); - let mut offset = 0usize; - while offset < len { - let line_len = core::cmp::min(LINE_BYTES, len - offset); - let mut buf = [0u8; LINE_BYTES]; - read_bytes( - table, - VirtAddr::new(start.as_raw().saturating_add(offset)), - &mut buf[..line_len], - )?; - crate::println!( - "[loader] dump {:#x}: {}", - start.as_raw().saturating_add(offset), - HexBytes(&buf[..line_len]) - ); - offset = offset.saturating_add(line_len); - } - Ok(()) -} - -const CONTEXT_BYTES: usize = 8; -const WINDOW_BYTES: usize = 16; -const REWRITE_LOG_CAPACITY: usize = 4096; -const SKIPPED_LOG_CAPACITY: usize = 64; - -static REWRITE_TRACKER: SpinLock = SpinLock::new(RewriteTracker::new()); - fn read_byte(table: &T, addr: VirtAddr) -> Result { let mapper = manager::phys_mapper(); let phys = table.translate(addr)?; @@ -232,204 +47,6 @@ fn read_byte(table: &T, addr: VirtAddr) -> Result( - table: &T, - start: VirtAddr, - out: &mut [u8], -) -> Result<(), TranslationError> { - for (idx, slot) in out.iter_mut().enumerate() { - let addr = start - .as_raw() - .checked_add(idx) - .ok_or(TranslationError::NotMapped)?; - *slot = read_byte(table, VirtAddr::new(addr))?; - } - Ok(()) -} - -fn boundary_hint(prev: Option) -> BoundaryHint { - match prev { - Some(0xC3) | Some(0xC2) | Some(0xCB) | Some(0xCA) | Some(0xCF) => BoundaryHint::Ret, - Some(0x90) => BoundaryHint::Nop, - Some(0xCC) => BoundaryHint::Int3, - Some(0x0F) => BoundaryHint::Overlap, - Some(0xE8) | Some(0xE9) | Some(0xEB) => BoundaryHint::Control, - _ => BoundaryHint::Unknown, - } -} - -#[derive(Copy, Clone)] -enum BoundaryHint { - Ret, - Nop, - Int3, - Overlap, - Control, - Unknown, -} - -impl fmt::Display for BoundaryHint { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let text = match self { - BoundaryHint::Ret => "prev=ret", - BoundaryHint::Nop => "prev=nop", - BoundaryHint::Int3 => "prev=int3", - BoundaryHint::Overlap => "prev=0f (overlap?)", - BoundaryHint::Control => "prev=ctrl", - BoundaryHint::Unknown => "prev=unknown", - }; - f.write_str(text) - } -} - -struct HexBytes<'a>(&'a [u8]); - -impl fmt::Display for HexBytes<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for (idx, byte) in self.0.iter().enumerate() { - if idx > 0 { - f.write_str(" ")?; - } - write!(f, "{:02x}", byte)?; - } - Ok(()) - } -} - -#[derive(Copy, Clone)] -struct RewriteEntry { - vaddr: usize, - before: [u8; WINDOW_BYTES], - after: [u8; WINDOW_BYTES], - opcode_ok: bool, - before_ok: bool, - after_ok: bool, - prev: u8, - next: u8, - hint: BoundaryHint, -} - -struct RewriteTracker { - entries: [MaybeUninit; REWRITE_LOG_CAPACITY], - stored: usize, - total: usize, - min: usize, - max: usize, - found_total: usize, - replaced_total: usize, - skipped_total: usize, - skipped_entries: [MaybeUninit; SKIPPED_LOG_CAPACITY], - skipped_stored: usize, -} - -impl RewriteTracker { - const fn new() -> Self { - Self { - entries: [MaybeUninit::uninit(); REWRITE_LOG_CAPACITY], - stored: 0, - total: 0, - min: usize::MAX, - max: 0, - found_total: 0, - replaced_total: 0, - skipped_total: 0, - skipped_entries: [MaybeUninit::uninit(); SKIPPED_LOG_CAPACITY], - skipped_stored: 0, - } - } - - fn reset(&mut self) { - self.stored = 0; - self.total = 0; - self.min = usize::MAX; - self.max = 0; - self.found_total = 0; - self.replaced_total = 0; - self.skipped_total = 0; - self.skipped_stored = 0; - } - - fn record(&mut self, entry: RewriteEntry) { - self.total = self.total.saturating_add(1); - if entry.vaddr < self.min { - self.min = entry.vaddr; - } - if entry.vaddr > self.max { - self.max = entry.vaddr; - } - if self.stored < REWRITE_LOG_CAPACITY { - self.entries[self.stored].write(entry); - self.stored += 1; - } - } - - fn entry(&self, idx: usize) -> &RewriteEntry { - unsafe { self.entries[idx].assume_init_ref() } - } - - fn skipped_entry(&self, idx: usize) -> &SkippedEntry { - unsafe { self.skipped_entries[idx].assume_init_ref() } - } - - fn contains(&self, vaddr: usize) -> bool { - for idx in 0..self.stored { - if self.entry(idx).vaddr == vaddr { - return true; - } - } - false - } - - fn overflowed(&self) -> bool { - self.total > self.stored - } - - fn skipped_overflowed(&self) -> bool { - self.skipped_total > self.skipped_stored - } - - fn min_addr(&self) -> usize { - if self.total == 0 { - 0 - } else { - self.min - } - } - - fn max_addr(&self) -> usize { - if self.total == 0 { - 0 - } else { - self.max - } - } -} - -fn record_rewrite(entry: RewriteEntry) { - let mut tracker = REWRITE_TRACKER.lock(); - tracker.replaced_total = tracker.replaced_total.saturating_add(1); - tracker.record(entry); -} - -fn record_syscall_found() { - let mut tracker = REWRITE_TRACKER.lock(); - tracker.found_total = tracker.found_total.saturating_add(1); -} - -fn record_syscall_skipped(vaddr: usize, context: [u8; WINDOW_BYTES], context_ok: bool) { - let mut tracker = REWRITE_TRACKER.lock(); - tracker.skipped_total = tracker.skipped_total.saturating_add(1); - if tracker.skipped_stored < SKIPPED_LOG_CAPACITY { - let idx = tracker.skipped_stored; - tracker.skipped_entries[idx].write(SkippedEntry { - vaddr, - context, - context_ok, - }); - tracker.skipped_stored = idx + 1; - } -} - fn matches_syscall_pattern(table: &T, addr: usize) -> bool { // Patterns anchored to the 0f 05 at `addr`. // - b8 imm32 0f 05 @@ -474,11 +91,7 @@ fn matches_b8_imm32_syscall(table: &T, addr: usize) -> bool { .unwrap_or(false) } -fn matches_rex_c7_c0_imm32_syscall( - table: &T, - addr: usize, - rex: u8, -) -> bool { +fn matches_rex_c7_c0_imm32_syscall(table: &T, addr: usize, rex: u8) -> bool { if addr < 7 { return false; } @@ -577,10 +190,3 @@ fn matches_nop_padded_syscall(table: &T, addr: usize) -> bool { } true } - -#[derive(Copy, Clone)] -struct SkippedEntry { - vaddr: usize, - context: [u8; WINDOW_BYTES], - context_ok: bool, -} diff --git a/kernel/src/loader/linux/reloc.rs b/kernel/src/loader/linux/reloc.rs index b3f4a2c..49e7cb1 100644 --- a/kernel/src/loader/linux/reloc.rs +++ b/kernel/src/loader/linux/reloc.rs @@ -23,7 +23,6 @@ const DT_RELRENT: i64 = 37; const R_X86_64_RELATIVE: u32 = 8; - #[derive(Debug, Default)] pub struct DynamicInfo { pub rela_addr: Option, @@ -141,44 +140,6 @@ pub fn read_dynamic_info( if info.relr_size > 0 && info.relr_addr.is_none() { return Err(LinuxLoadError::InvalidElf("DT_RELR missing")); } - if let (Some(rela_addr), true) = (info.rela_addr, info.rela_size > 0) { - crate::println!( - "[loader] rela addr={:#x} size={:#x} ent={:#x}", - rela_addr.as_raw(), - info.rela_size, - info.rela_ent - ); - let entry_size = core::mem::size_of::(); - if entry_size > 0 && info.rela_size.is_multiple_of(entry_size) { - let count = (info.rela_size / entry_size).min(3); - for idx in 0..count { - let entry_addr = match rela_addr.checked_add(idx * entry_size) { - Some(addr) => addr, - None => break, - }; - let r_offset = match user.read_u64(entry_addr) { - Ok(val) => val, - Err(_) => break, - }; - let r_info = match user.read_u64(entry_addr.checked_add(8).unwrap_or(entry_addr)) { - Ok(val) => val, - Err(_) => break, - }; - let r_addend = - match user.read_u64(entry_addr.checked_add(16).unwrap_or(entry_addr)) { - Ok(val) => val as i64, - Err(_) => break, - }; - crate::println!( - "[loader] rela[{}] r_offset={:#x} r_info={:#x} r_addend={:#x}", - idx, - r_offset, - r_info, - r_addend - ); - } - } - } Ok(info) } @@ -188,10 +149,6 @@ pub fn apply_relocations( info: &DynamicInfo, segments: &[MappedSegment], ) -> Result<(), LinuxLoadError> { - crate::println!( - "[loader] apply relocations base={:#x}", - base.as_raw() - ); apply_rel(table, base, info, segments)?; apply_rela(table, base, info, segments)?; apply_relr(table, base, info, segments)?; @@ -209,7 +166,15 @@ fn apply_rel( Some(addr) => addr, None => return Ok(()), }; - apply_rel_table(table, base, rel_addr, info.rel_size, info.rel_ent, segments, "REL") + apply_rel_table( + table, + base, + rel_addr, + info.rel_size, + info.rel_ent, + segments, + "REL", + ) } fn apply_rela( @@ -222,7 +187,15 @@ fn apply_rela( Some(addr) => addr, None => return Ok(()), }; - apply_rela_table(table, base, rela_addr, info.rela_size, info.rela_ent, segments, "RELA") + apply_rela_table( + table, + base, + rela_addr, + info.rela_size, + info.rela_ent, + segments, + "RELA", + ) } fn apply_jmprel( @@ -303,17 +276,6 @@ fn apply_rela_table( let raw_offset = usize::try_from(r_offset).map_err(|_| LinuxLoadError::SizeOverflow)?; let target = resolve_reloc_target(base, raw_offset, segments)?; let value_raw = resolve_symbol_reloc(base, reloc_type, sym, r_addend, segments)?; - let value_addr = VirtAddr::new(value_raw as usize); - if !is_mapped(value_addr, segments) { - crate::println!( - "[loader] rela[{}] unmapped value r_offset={:#x} target={:#x} addend={:#x} value={:#x}", - idx, - r_offset, - target.as_raw(), - r_addend, - value_raw - ); - } user.write_u64(target, value_raw)?; } @@ -432,10 +394,7 @@ fn apply_relative_at( Ok(()) } -fn user_read_u64( - table: &T, - addr: VirtAddr, -) -> Result { +fn user_read_u64(table: &T, addr: VirtAddr) -> Result { let user = UserMemoryAccess::new(table); user.read_u64(addr).map_err(LinuxLoadError::from) } @@ -445,9 +404,7 @@ fn resolve_reloc_target( raw: usize, segments: &[MappedSegment], ) -> Result { - let base_target = base - .checked_add(raw) - .ok_or(LinuxLoadError::SizeOverflow)?; + let base_target = base.checked_add(raw).ok_or(LinuxLoadError::SizeOverflow)?; if is_mapped(base_target, segments) { return Ok(base_target); } @@ -522,11 +479,6 @@ pub fn apply_gnu_relro( let relro_end = relro_start .checked_add(relro.mem_size) .ok_or(LinuxLoadError::SizeOverflow)?; - crate::println!( - "[loader] relro range={:#x}-{:#x}", - relro_start.as_raw(), - relro_end.as_raw() - ); let page_size = segments .first() .map(|seg| seg.page_size) diff --git a/kernel/src/loader/linux/stack.rs b/kernel/src/loader/linux/stack.rs index d254722..e848cbb 100644 --- a/kernel/src/loader/linux/stack.rs +++ b/kernel/src/loader/linux/stack.rs @@ -75,7 +75,9 @@ pub fn initialise_stack_with_args( // TODO: Replace zeroed bytes with a kernel RNG once available. let random_ptr = { - sp = sp.checked_sub(AT_RANDOM_LEN).ok_or(StackBuildError::Overflow)?; + sp = sp + .checked_sub(AT_RANDOM_LEN) + .ok_or(StackBuildError::Overflow)?; unsafe { core::ptr::write_bytes(sp as *mut u8, 0, AT_RANDOM_LEN); } @@ -142,7 +144,9 @@ pub fn initialise_stack_with_args_in_table( // TODO: Replace zeroed bytes with a kernel RNG once available. let random_ptr = { - sp = sp.checked_sub(AT_RANDOM_LEN).ok_or(StackBuildError::Overflow)?; + sp = sp + .checked_sub(AT_RANDOM_LEN) + .ok_or(StackBuildError::Overflow)?; let addr = VirtAddr::new(sp); let zeros = [0u8; AT_RANDOM_LEN]; user.write_bytes(addr, &zeros) diff --git a/kernel/src/process/fs.rs b/kernel/src/process/fs.rs index 4af2bcd..90152f9 100644 --- a/kernel/src/process/fs.rs +++ b/kernel/src/process/fs.rs @@ -122,22 +122,10 @@ pub fn control_fd( let entry = match process.fd_table().entry(fd) { Ok(entry) => entry, Err(_) => { - if request.command == 0x5410 { - crate::println!("[proc-fs] ioctl fd not found pid={} fd={}", pid, fd); - } return Err(ControlError::Invalid); } }; - let result = entry.file().ioctl(request); - if request.command == 0x5410 { - crate::println!( - "[proc-fs] ioctl fd={} cmd=0x{:x} -> {:?}", - fd, - request.command, - result - ); - } - result + entry.file().ioctl(request) } pub fn change_dir(pid: ProcessId, raw_path: &str) -> Result<(), VfsError> { diff --git a/kernel/src/process/mod.rs b/kernel/src/process/mod.rs index 79dc9b3..359862d 100644 --- a/kernel/src/process/mod.rs +++ b/kernel/src/process/mod.rs @@ -105,7 +105,6 @@ impl ProcessTable { let mut inner = self.inner.lock(); if inner.kernel_pid.is_some() { self.initialised.store(true, Ordering::Release); - crate::println!("[process] init_kernel already initialised"); return Ok(inner.kernel_pid.expect("kernel process must exist")); } @@ -114,7 +113,6 @@ impl ProcessTable { inner.next_pid = 1; inner.processes.push(process); self.initialised.store(true, Ordering::Release); - crate::println!("[process] init_kernel created pid=0"); Ok(0) } @@ -139,7 +137,6 @@ impl ProcessTable { let process = Arc::new(Process::kernel(pid, name, Abi::Host)); inner.next_pid = pid.checked_add(1).expect("process id overflow"); inner.processes.push(process); - crate::println!("[process] create_kernel_process pid={} name={}", pid, name); Ok(pid) } @@ -172,7 +169,6 @@ impl ProcessTable { let process = Arc::new(Process::user(pid, name, space, domain)); inner.next_pid = pid.checked_add(1).expect("process id overflow"); inner.processes.push(process); - crate::println!("[process] create_user_process pid={} name={}", pid, name); Ok(pid) } diff --git a/kernel/src/syscall/linux.rs b/kernel/src/syscall/linux.rs index 496a2fa..d47c45d 100644 --- a/kernel/src/syscall/linux.rs +++ b/kernel/src/syscall/linux.rs @@ -21,9 +21,6 @@ use crate::thread::SCHEDULER; use crate::trap::CurrentTrapFrame; use crate::util::stream::{ControlAccess, ControlError, ControlRequest}; -const DEBUG_LS_SYSCALL: bool = true; -const DEBUG_LINUX_ENOSYS: bool = true; - // NOTE: Error mapping is intentionally coarse right now (many failures collapse // to InvalidArgument/BadAddress). This keeps the syscall surface minimal but is // not Linux-accurate. @@ -143,23 +140,6 @@ pub fn dispatch( invocation: &SyscallInvocation, frame: Option<&mut CurrentTrapFrame>, ) -> DispatchResult { - if DEBUG_LS_SYSCALL { - match invocation.number { - 217 | 257 | 262 => { - crate::println!( - "[linux-ls] nr={} args=[{:x}, {:x}, {:x}, {:x}, {:x}, {:x}]", - invocation.number, - invocation.args[0], - invocation.args[1], - invocation.args[2], - invocation.args[3], - invocation.args[4], - invocation.args[5], - ); - } - _ => {} - } - } match LinuxSyscall::from_raw(invocation.number) { Some(LinuxSyscall::Read) => DispatchResult::Completed(handle_read(invocation)), Some(LinuxSyscall::Write) => DispatchResult::Completed(handle_write(invocation)), @@ -209,21 +189,7 @@ pub fn dispatch( } Some(LinuxSyscall::Exit) => handle_exit(invocation), Some(LinuxSyscall::ExitGroup) => handle_exit(invocation), - None => { - if DEBUG_LINUX_ENOSYS { - crate::println!( - "[linux-syscall] ENOSYS nr={} args=[{:x}, {:x}, {:x}, {:x}, {:x}, {:x}]", - invocation.number, - invocation.args[0], - invocation.args[1], - invocation.args[2], - invocation.args[3], - invocation.args[4], - invocation.args[5], - ); - } - DispatchResult::Completed(Err(SysError::NotImplemented)) - } + None => DispatchResult::Completed(Err(SysError::NotImplemented)), } } @@ -392,21 +358,11 @@ fn handle_ioctl(invocation: &SyscallInvocation) -> SysResult { .process_handle(pid) .map_err(|_| SysError::InvalidArgument)?; - let result = process.address_space().with_page_table(|table, _| { + process.address_space().with_page_table(|table, _| { let user = UserMemoryAccess::new(table); let request = crate::util::stream::ControlRequest::new(cmd, arg, &user); proc_fs::control_fd(pid, fd as u32, &request).map_err(map_control_error) - }); - if cmd == 0x540e || cmd == 0x540f || cmd == 0x5410 { - crate::println!( - "[linux-ioctl] pid={} fd={} cmd=0x{:x} -> {:?}", - pid, - fd, - cmd, - result - ); - } - result + }) } fn handle_getpid(_invocation: &SyscallInvocation) -> SysResult { @@ -622,8 +578,8 @@ fn handle_getdents64(invocation: &SyscallInvocation) -> SysResult { } let entries = proc_fs::read_dir_fd(pid, fd as u32).map_err(|_| SysError::InvalidArgument)?; - let mut index = proc_fs::dir_offset(pid, fd as u32) - .map_err(|_| SysError::InvalidArgument)? as usize; + let mut index = + proc_fs::dir_offset(pid, fd as u32).map_err(|_| SysError::InvalidArgument)? as usize; if index >= entries.len() { return Ok(0); } @@ -632,32 +588,24 @@ fn handle_getdents64(invocation: &SyscallInvocation) -> SysResult { let mut written = 0usize; while index < entries.len() { let entry = &entries[index]; - let reclen = dirent64_reclen(entry.name.as_bytes().len(), HEADER_LEN, ALIGN)?; + let reclen = dirent64_reclen(entry.name.len(), HEADER_LEN, ALIGN)?; if written + reclen > count { break; } - write_dirent64( - base, - written, - entry, - (index + 1) as u64, - reclen, - HEADER_LEN, - )?; + write_dirent64(base, written, entry, (index + 1) as u64, reclen, HEADER_LEN)?; written += reclen; index += 1; } if written == 0 && index < entries.len() { let entry = &entries[index]; - let min = dirent64_reclen(entry.name.as_bytes().len(), HEADER_LEN, ALIGN)?; + let min = dirent64_reclen(entry.name.len(), HEADER_LEN, ALIGN)?; if min > count { return Err(SysError::InvalidArgument); } } - proc_fs::set_dir_offset(pid, fd as u32, index as u64) - .map_err(|_| SysError::InvalidArgument)?; + proc_fs::set_dir_offset(pid, fd as u32, index as u64).map_err(|_| SysError::InvalidArgument)?; Ok(written as u64) } @@ -704,9 +652,7 @@ fn write_dirent64( copy_to_user(dst, &header).map_err(|_| SysError::BadAddress)?; let name = entry.name.as_bytes(); - let name_dst = dst - .checked_add(header_len) - .ok_or(SysError::BadAddress)?; + let name_dst = dst.checked_add(header_len).ok_or(SysError::BadAddress)?; copy_to_user(name_dst, name).map_err(|_| SysError::BadAddress)?; let nul_dst = name_dst .checked_add(name.len()) @@ -842,7 +788,9 @@ fn handle_brk(invocation: &SyscallInvocation) -> SysResult { return Err(SysError::InvalidArgument); } } - let phys = table.translate(page.start).map_err(|_| SysError::InvalidArgument)?; + let phys = table + .translate(page.start) + .map_err(|_| SysError::InvalidArgument)?; let mapper = manager::phys_mapper(); unsafe { core::ptr::write_bytes(mapper.phys_to_virt(phys).into_mut_ptr(), 0, page_size); @@ -889,7 +837,6 @@ fn handle_fork( Ok(pid) => pid, Err(err) => return DispatchResult::Completed(Err(err)), }; - crate::println!("[fork] parent_pid={}", pid); let parent_stack = match SCHEDULER.current_user_stack_info() { Some(info) => info, @@ -916,7 +863,6 @@ fn handle_fork( Ok(pid) => pid, Err(_) => return DispatchResult::Completed(Err(SysError::InvalidArgument)), }; - crate::println!("[fork] created child_pid={}", child_pid); if let Ok(child_proc) = PROCESS_TABLE.process_handle(child_pid) { child_proc.set_brk_state(parent_proc.brk_state()); @@ -958,7 +904,6 @@ fn handle_fork( return DispatchResult::Completed(Err(spawn_error_to_sys(err))); } - crate::println!("[fork] spawned child thread for pid={}", child_pid); DispatchResult::Completed(Ok(child_pid)) } @@ -989,7 +934,6 @@ fn handle_execve( Ok(path) => path, Err(err) => return DispatchResult::Completed(Err(err)), }; - crate::println!("[execve] pid={} path={}", pid, path); let argv_ptr = invocation.arg(1).unwrap_or(0); let envp_ptr = invocation.arg(2).unwrap_or(0); @@ -1008,11 +952,6 @@ fn handle_execve( Ok(list) => list, Err(err) => return DispatchResult::Completed(Err(err)), }; - crate::println!( - "[execve] argv_count={} envp_count={}", - argv.len(), - envp.len() - ); // Drop the previous user stack before clearing mappings so we do not unmap the // freshly allocated stack on replacement. @@ -1032,11 +971,6 @@ fn handle_execve( Ok(program) => program, Err(_err) => return DispatchResult::Completed(Err(SysError::InvalidArgument)), }; - crate::println!( - "[execve] entry={:#x} stack_top={:#x}", - program.entry.as_raw(), - ::user_stack_top(&program.user_stack).as_raw() - ); let auxv = crate::loader::linux::build_auxv(&program, PageSize::SIZE_4K.bytes()); let argv_refs: alloc::vec::Vec<&str> = argv.iter().map(|s| s.as_str()).collect(); @@ -1065,7 +999,6 @@ fn handle_execve( frame.rip = program.entry.as_raw() as u64; frame.rsp = stack_pointer.as_raw() as u64; frame.regs.rax = 0; - crate::println!("[execve] new rsp={:#x}", frame.rsp); DispatchResult::Completed(Ok(0)) } @@ -1147,10 +1080,7 @@ fn handle_arch_prctl(invocation: &SyscallInvocation) -> SysResult { } // NOTE: This assumes FS base is part of the thread context and restored on context switches. - crate::println!("[arch_prctl] ARCH_SET_FS requested={:#x}", value); crate::arch::x86_64::set_fs_base(value); - let fs_base = x86_64::registers::model_specific::FsBase::read().as_u64(); - crate::println!("[arch_prctl] FS base now={:#x}", fs_base); Ok(0) } diff --git a/kernel/src/syscall/mod.rs b/kernel/src/syscall/mod.rs index 6b0ebea..a57ec41 100644 --- a/kernel/src/syscall/mod.rs +++ b/kernel/src/syscall/mod.rs @@ -63,29 +63,10 @@ pub fn dispatch_with_frame( invocation: &SyscallInvocation, frame: Option<&mut crate::trap::CurrentTrapFrame>, ) -> DispatchResult { - crate::println!( - "[syscall] abi={:?} nr={} args=[{:x}, {:x}, {:x}, {:x}, {:x}, {:x}]", - abi, - invocation.number, - invocation.args[0], - invocation.args[1], - invocation.args[2], - invocation.args[3], - invocation.args[4], - invocation.args[5], - ); - let result = match abi { + match abi { Abi::Host => DispatchResult::Completed(host::dispatch(invocation)), Abi::Linux => linux::dispatch(invocation, frame), - }; - if matches!(result, DispatchResult::Completed(Err(SysError::NotImplemented))) { - crate::println!( - "[syscall] unimplemented abi={:?} nr={}", - abi, - invocation.number - ); } - result } /// Encode a syscall result into an ABI-specific return value. diff --git a/kernel/src/thread/mod.rs b/kernel/src/thread/mod.rs index 91e0ec2..e740dc4 100644 --- a/kernel/src/thread/mod.rs +++ b/kernel/src/thread/mod.rs @@ -94,7 +94,6 @@ impl Scheduler { .init_kernel() .map_err(SchedulerError::Process)?; inner.kernel_process = Some(kernel_pid); - crate::println!("[thread] scheduler init kernel_pid={}", kernel_pid); let kernel_process = PROCESS_TABLE .process_handle(kernel_pid) @@ -109,7 +108,6 @@ impl Scheduler { inner.current = Some(bootstrap.id); inner.threads.push(bootstrap); kernel_process.mark_running(); - crate::println!("[thread] bootstrap thread id=0"); let idle_id = inner.next_tid; let idle = ThreadControl::idle(idle_id, kernel_process.clone(), kernel_space.clone()) @@ -119,7 +117,6 @@ impl Scheduler { .map_err(SchedulerError::Process)?; inner.next_tid = idle_id.checked_add(1).expect("thread id overflow"); inner.idle = Some(idle.id); - crate::println!("[thread] idle thread id={}", idle.id); inner.threads.push(idle); inner.initialised = true; syscall::set_current_abi(kernel_process.abi()); @@ -170,11 +167,6 @@ impl Scheduler { return Err(SpawnError::SchedulerNotReady); } - crate::println!( - "[thread] spawn kernel thread name={} pid={}", - name, - process - ); self.spawn_thread_locked(&mut inner, process, name, entry) } @@ -190,12 +182,6 @@ impl Scheduler { return Err(SpawnError::SchedulerNotReady); } - crate::println!( - "[thread] spawn user thread name={} pid={} entry={:#x}", - name, - process, - entry.as_raw() - ); inner.spawn_user_thread(process, name, entry, stack_size) } @@ -212,13 +198,6 @@ impl Scheduler { return Err(SpawnError::SchedulerNotReady); } - crate::println!( - "[thread] spawn user thread (stack) name={} pid={} entry={:#x} sp={:#x}", - name, - process, - entry.as_raw(), - stack_pointer.as_raw() - ); inner.spawn_user_thread_with_stack(process, name, entry, user_stack, stack_pointer) } @@ -234,11 +213,6 @@ impl Scheduler { return Err(SpawnError::SchedulerNotReady); } - crate::println!( - "[thread] spawn user thread (ctx) name={} pid={}", - name, - process - ); inner.spawn_user_thread_with_context(process, name, context, user_stack) } From 1b409fc58908d5fcb8c4972e7713e42c5ef0d8a4 Mon Sep 17 00:00:00 2001 From: n4mlz Date: Thu, 29 Jan 2026 19:52:14 +0900 Subject: [PATCH 24/25] feat: enhance ContainerContext with UTS support and update related syscall handling --- kernel/src/container/DESIGN.md | 2 + kernel/src/container/context.rs | 91 ++++++++++++++++++++++++++++++++- kernel/src/container/mod.rs | 2 +- kernel/src/container/table.rs | 2 +- kernel/src/process/mod.rs | 7 +-- kernel/src/syscall/DESIGN.md | 2 +- kernel/src/syscall/linux.rs | 52 ++----------------- kernel/src/syscall/mod.rs | 19 ++++++- 8 files changed, 121 insertions(+), 56 deletions(-) diff --git a/kernel/src/container/DESIGN.md b/kernel/src/container/DESIGN.md index a36d913..9bc697c 100644 --- a/kernel/src/container/DESIGN.md +++ b/kernel/src/container/DESIGN.md @@ -15,6 +15,8 @@ `bundlePath`, `annotations`) and is paired with `ContainerContext` inside a single lock. - `ContainerMutable` also keeps a process list for the container; the init process PID is mirrored in `ContainerState::pid` so OCI `state` reports it consistently. +- `ContainerContext` stores the container VFS plus per-container UTS data (hostname/domainname + from the OCI spec) so `uname` can reflect the bundle configuration. - The parsed OCI `Spec` (`config.json`) is stored immutably as `Arc` so it can be shared without additional locking. diff --git a/kernel/src/container/context.rs b/kernel/src/container/context.rs index 2c09cd9..b03a894 100644 --- a/kernel/src/container/context.rs +++ b/kernel/src/container/context.rs @@ -1,15 +1,18 @@ use alloc::sync::Arc; +use oci_spec::runtime::Spec; + use crate::fs::{Node, Vfs}; #[derive(Clone)] pub struct ContainerContext { vfs: Arc, + uts: Uts, } impl ContainerContext { - pub fn new(vfs: Arc) -> Self { - Self { vfs } + pub fn new(vfs: Arc, uts: Uts) -> Self { + Self { vfs, uts } } pub fn rootfs(&self) -> Arc { @@ -19,4 +22,88 @@ impl ContainerContext { pub fn vfs(&self) -> Arc { self.vfs.clone() } + + pub fn uts(&self) -> Uts { + self.uts + } +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct Uts { + sysname: [u8; 65], + nodename: [u8; 65], + release: [u8; 65], + version: [u8; 65], + machine: [u8; 65], + domainname: [u8; 65], +} + +impl Uts { + pub fn default_host() -> Self { + Self::build(UtsFields { + sysname: "Linux", + nodename: "cyrius", + release: "0.0.1-alpha", + version: "cyrius", + machine: "x86_64", + domainname: "", + }) + } + + pub fn from_spec(spec: &Spec) -> Self { + let nodename = spec.hostname().as_deref().unwrap_or("cyrius"); + let domainname = spec.domainname().as_deref().unwrap_or(""); + Self::build(UtsFields { + sysname: "Linux", + nodename, + release: "0.0.1-alpha", + version: "cyrius", + machine: "x86_64", + domainname, + }) + } + + pub fn as_bytes(&self) -> &[u8] { + unsafe { + core::slice::from_raw_parts( + (self as *const Uts) as *const u8, + core::mem::size_of::(), + ) + } + } + + fn build(fields: UtsFields<'_>) -> Self { + fn write_field(dst: &mut [u8; 65], src: &str) { + let bytes = src.as_bytes(); + let len = dst.len().saturating_sub(1).min(bytes.len()); + dst[..len].copy_from_slice(&bytes[..len]); + dst[len] = 0; + } + + let mut uts = Self { + sysname: [0; 65], + nodename: [0; 65], + release: [0; 65], + version: [0; 65], + machine: [0; 65], + domainname: [0; 65], + }; + write_field(&mut uts.sysname, fields.sysname); + write_field(&mut uts.nodename, fields.nodename); + write_field(&mut uts.release, fields.release); + write_field(&mut uts.version, fields.version); + write_field(&mut uts.machine, fields.machine); + write_field(&mut uts.domainname, fields.domainname); + uts + } +} + +struct UtsFields<'a> { + sysname: &'a str, + nodename: &'a str, + release: &'a str, + version: &'a str, + machine: &'a str, + domainname: &'a str, } diff --git a/kernel/src/container/mod.rs b/kernel/src/container/mod.rs index 702b32e..66824a0 100644 --- a/kernel/src/container/mod.rs +++ b/kernel/src/container/mod.rs @@ -14,7 +14,7 @@ mod spec; pub mod state; mod table; -pub use context::ContainerContext; +pub use context::{ContainerContext, Uts}; pub use error::ContainerError; pub use repository::ContainerRepository; pub use spec::{SpecLoader, SpecMetadata}; diff --git a/kernel/src/container/table.rs b/kernel/src/container/table.rs index cc1bb3f..67d52ec 100644 --- a/kernel/src/container/table.rs +++ b/kernel/src/container/table.rs @@ -45,7 +45,7 @@ impl ContainerTable { annotations: meta.annotations, }; let vfs = SpecLoader::build_container_vfs(&bundle, &spec)?; - let context = ContainerContext::new(vfs); + let context = ContainerContext::new(vfs, crate::container::Uts::from_spec(&spec)); let container = Arc::new(Container::new(state, spec, context)); self.repo.insert(id, container) diff --git a/kernel/src/process/mod.rs b/kernel/src/process/mod.rs index 359862d..8a19867 100644 --- a/kernel/src/process/mod.rs +++ b/kernel/src/process/mod.rs @@ -669,9 +669,10 @@ mod tests { annotations: Default::default(), }, oci_spec::runtime::Spec::default(), - crate::container::ContainerContext::new(Arc::new(crate::fs::Vfs::new( - crate::fs::memfs::MemDirectory::new(), - ))), + crate::container::ContainerContext::new( + Arc::new(crate::fs::Vfs::new(crate::fs::memfs::MemDirectory::new())), + crate::container::Uts::default_host(), + ), )); let pid = PROCESS_TABLE .create_user_process( diff --git a/kernel/src/syscall/DESIGN.md b/kernel/src/syscall/DESIGN.md index a09605d..fc6f4d8 100644 --- a/kernel/src/syscall/DESIGN.md +++ b/kernel/src/syscall/DESIGN.md @@ -24,7 +24,7 @@ `execve`, `wait4`, `arch_prctl`, `ioctl` (routed through `ControlOps`), `fcntl` (dup + FD_CLOEXEC), and basic process/session metadata (`getppid`, `getpgrp`, `getpgid`, `setpgid`, `getsid`, `setsid`), plus - `uname`, `geteuid`, and stubbed signal + `uname` (UTS fields sourced from the container context), `geteuid`, and stubbed signal calls. Unsupported numbers map to `ENOSYS`, while unsupported ioctls map to `ENOTTY`. - `/dev/tty` open assigns the global controlling TTY when the caller is a session leader and no controlling TTY is present yet; this is a minimal bridge until full tty/session semantics land. diff --git a/kernel/src/syscall/linux.rs b/kernel/src/syscall/linux.rs index d47c45d..b1f5622 100644 --- a/kernel/src/syscall/linux.rs +++ b/kernel/src/syscall/linux.rs @@ -5,6 +5,7 @@ use alloc::vec::Vec; use crate::arch::Arch; use crate::arch::api::{ArchPageTableAccess, ArchThread}; +use crate::container::Uts; use crate::fs::{DirEntry, NodeKind}; use crate::interrupt::INTERRUPTS; use crate::mem::addr::{ @@ -686,7 +687,10 @@ fn handle_uname(invocation: &SyscallInvocation) -> SysResult { let process = PROCESS_TABLE .process_handle(pid) .map_err(|_| SysError::InvalidArgument)?; - let uts = LinuxUtsName::new(); + let uts = match process.domain() { + crate::process::ProcessDomain::Container(container) => container.context().uts(), + _ => Uts::default_host(), + }; let dst = VirtAddr::new(addr as usize); process.address_space().with_page_table(|table, _| { let user = UserMemoryAccess::new(table); @@ -1600,52 +1604,6 @@ struct LinuxWinsize { ws_ypixel: u16, } -#[repr(C)] -#[derive(Clone, Copy)] -struct LinuxUtsName { - sysname: [u8; 65], - nodename: [u8; 65], - release: [u8; 65], - version: [u8; 65], - machine: [u8; 65], - domainname: [u8; 65], -} - -impl LinuxUtsName { - fn new() -> Self { - fn write_field(dst: &mut [u8; 65], src: &[u8]) { - let len = dst.len().saturating_sub(1).min(src.len()); - dst[..len].copy_from_slice(&src[..len]); - dst[len] = 0; - } - - let mut uts = Self { - sysname: [0; 65], - nodename: [0; 65], - release: [0; 65], - version: [0; 65], - machine: [0; 65], - domainname: [0; 65], - }; - write_field(&mut uts.sysname, b"Linux"); - write_field(&mut uts.nodename, b"cyrius"); - write_field(&mut uts.release, b"5.10.0"); - write_field(&mut uts.version, b"cyrius"); - write_field(&mut uts.machine, b"x86_64"); - write_field(&mut uts.domainname, b""); - uts - } - - fn as_bytes(&self) -> &[u8] { - unsafe { - core::slice::from_raw_parts( - (self as *const LinuxUtsName) as *const u8, - core::mem::size_of::(), - ) - } - } -} - struct KernelControlAccess; impl ControlAccess for KernelControlAccess { diff --git a/kernel/src/syscall/mod.rs b/kernel/src/syscall/mod.rs index a57ec41..98bd4f0 100644 --- a/kernel/src/syscall/mod.rs +++ b/kernel/src/syscall/mod.rs @@ -63,10 +63,27 @@ pub fn dispatch_with_frame( invocation: &SyscallInvocation, frame: Option<&mut crate::trap::CurrentTrapFrame>, ) -> DispatchResult { - match abi { + let result = match abi { Abi::Host => DispatchResult::Completed(host::dispatch(invocation)), Abi::Linux => linux::dispatch(invocation, frame), + }; + if matches!( + result, + DispatchResult::Completed(Err(SysError::NotImplemented)) + ) { + crate::println!( + "[syscall] unimplemented abi={:?} nr={} args=[{:x}, {:x}, {:x}, {:x}, {:x}, {:x}]", + abi, + invocation.number, + invocation.args[0], + invocation.args[1], + invocation.args[2], + invocation.args[3], + invocation.args[4], + invocation.args[5], + ); } + result } /// Encode a syscall result into an ABI-specific return value. From 259a5537b87f025136ae72ef749a9249d01bcee4 Mon Sep 17 00:00:00 2001 From: n4mlz Date: Fri, 30 Jan 2026 16:09:59 +0900 Subject: [PATCH 25/25] fix: update test bootloader configuration to increase stack size and ensure proper memory mapping --- kernel/src/kernel.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/kernel/src/kernel.rs b/kernel/src/kernel.rs index c4acb91..832ee1d 100644 --- a/kernel/src/kernel.rs +++ b/kernel/src/kernel.rs @@ -39,11 +39,20 @@ const BOOTLOADER_CONFIG: BootloaderConfig = { config }; +#[cfg(test)] +const TEST_BOOTLOADER_CONFIG: BootloaderConfig = { + let mut config = BootloaderConfig::new_default(); + config.mappings.physical_memory = Some(Mapping::Dynamic); + // Increase stack size for tests to avoid stack overflow + config.kernel_stack_size = 256 * 1024; // 256KB + config +}; + #[cfg(not(test))] entry_point!(kernel_main, config = &BOOTLOADER_CONFIG); #[cfg(test)] -entry_point!(test_kernel_main, config = &BOOTLOADER_CONFIG); +entry_point!(test_kernel_main, config = &TEST_BOOTLOADER_CONFIG); fn kernel_main(boot_info: &'static mut BootInfo) -> ! { init::init_runtime(boot_info);