diff --git a/kernel/src/interrupts/context_switch.rs b/kernel/src/interrupts/context_switch.rs index 94161e3..2cb842f 100644 --- a/kernel/src/interrupts/context_switch.rs +++ b/kernel/src/interrupts/context_switch.rs @@ -303,7 +303,7 @@ fn switch_to_thread( // Update TSS.RSP0 with new thread's kernel stack top // This is critical for interrupt/exception handling if let Some(kernel_stack_top) = thread.kernel_stack_top { - crate::per_cpu::update_tss_rsp0(kernel_stack_top); + crate::per_cpu::update_tss_rsp0(kernel_stack_top.as_u64()); log::trace!("sched: switch to thread {} rsp0={:#x}", thread_id, kernel_stack_top); } }); diff --git a/kernel/src/ipc/fd.rs b/kernel/src/ipc/fd.rs new file mode 100644 index 0000000..ef3d3f7 --- /dev/null +++ b/kernel/src/ipc/fd.rs @@ -0,0 +1,208 @@ +//! File descriptor types and table +//! +//! This module provides the unified file descriptor abstraction for POSIX-like I/O. +//! Each process has its own file descriptor table that maps small integers +//! to underlying file objects (pipes, stdio, sockets, etc.). + +use alloc::sync::Arc; +use spin::Mutex; + +/// Maximum number of file descriptors per process +pub const MAX_FDS: usize = 256; + +/// Standard file descriptor numbers +pub const STDIN: i32 = 0; +pub const STDOUT: i32 = 1; +pub const STDERR: i32 = 2; + +/// File descriptor flags +pub mod flags { + /// Close-on-exec flag (used by fcntl F_SETFD) + #[allow(dead_code)] + pub const FD_CLOEXEC: u32 = 1; +} + +/// Types of file descriptors +/// +/// This unified enum supports all fd types in Breenix: +/// - Standard I/O (stdin/stdout/stderr) +/// - Pipes (read and write ends) +/// - UDP sockets (with future support for TCP, files, etc.) +/// +/// Note: Sockets use Arc> like pipes because they need to be shared +/// and cannot be cloned (they contain unique socket handles and rx queues). +#[derive(Clone)] +pub enum FdKind { + /// Standard I/O (stdin, stdout, stderr) + StdIo(i32), + /// Read end of a pipe + PipeRead(Arc>), + /// Write end of a pipe + PipeWrite(Arc>), + /// UDP socket (wrapped in Arc> for sharing and dup/fork) + UdpSocket(Arc>), +} + +impl core::fmt::Debug for FdKind { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + FdKind::StdIo(n) => write!(f, "StdIo({})", n), + FdKind::PipeRead(_) => write!(f, "PipeRead"), + FdKind::PipeWrite(_) => write!(f, "PipeWrite"), + FdKind::UdpSocket(_) => write!(f, "UdpSocket"), + } + } +} + +/// A file descriptor entry in the per-process table +#[derive(Clone)] +pub struct FileDescriptor { + /// What kind of file this descriptor refers to + pub kind: FdKind, + /// Flags (FD_CLOEXEC, etc.) + pub flags: u32, +} + +impl core::fmt::Debug for FileDescriptor { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("FileDescriptor") + .field("kind", &self.kind) + .field("flags", &self.flags) + .finish() + } +} + +impl FileDescriptor { + /// Create a new file descriptor + pub fn new(kind: FdKind) -> Self { + FileDescriptor { kind, flags: 0 } + } + + /// Create with specific flags (used by pipe2, etc.) + #[allow(dead_code)] + pub fn with_flags(kind: FdKind, flags: u32) -> Self { + FileDescriptor { kind, flags } + } +} + +/// Per-process file descriptor table +/// +/// Note: Uses Box to heap-allocate the fd array to avoid stack overflow +/// during process creation (the array is ~6KB which is too large for stack). +pub struct FdTable { + /// The file descriptors (None = unused slot) + fds: alloc::boxed::Box<[Option; MAX_FDS]>, +} + +impl Default for FdTable { + fn default() -> Self { + Self::new() + } +} + +impl Clone for FdTable { + fn clone(&self) -> Self { + FdTable { + fds: alloc::boxed::Box::new((*self.fds).clone()), + } + } +} + +impl FdTable { + /// Create a new file descriptor table with standard I/O pre-allocated + pub fn new() -> Self { + // Use Box::new to allocate directly on heap, avoiding stack overflow + let mut fds = alloc::boxed::Box::new(core::array::from_fn(|_| None)); + + // Pre-allocate stdin, stdout, stderr + fds[STDIN as usize] = Some(FileDescriptor::new(FdKind::StdIo(STDIN))); + fds[STDOUT as usize] = Some(FileDescriptor::new(FdKind::StdIo(STDOUT))); + fds[STDERR as usize] = Some(FileDescriptor::new(FdKind::StdIo(STDERR))); + + FdTable { fds } + } + + /// Allocate a new file descriptor with the given kind + /// Returns the fd number on success, or an error code + pub fn alloc(&mut self, kind: FdKind) -> Result { + self.alloc_at_least(0, kind) + } + + /// Allocate a new file descriptor >= min_fd + pub fn alloc_at_least(&mut self, min_fd: i32, kind: FdKind) -> Result { + let start = min_fd.max(0) as usize; + for i in start..MAX_FDS { + if self.fds[i].is_none() { + self.fds[i] = Some(FileDescriptor::new(kind)); + return Ok(i as i32); + } + } + Err(24) // EMFILE - too many open files + } + + /// Get a reference to a file descriptor + pub fn get(&self, fd: i32) -> Option<&FileDescriptor> { + if fd < 0 || fd as usize >= MAX_FDS { + return None; + } + self.fds[fd as usize].as_ref() + } + + /// Get a mutable reference to a file descriptor (used by fcntl) + #[allow(dead_code)] + pub fn get_mut(&mut self, fd: i32) -> Option<&mut FileDescriptor> { + if fd < 0 || fd as usize >= MAX_FDS { + return None; + } + self.fds[fd as usize].as_mut() + } + + /// Close a file descriptor + /// Returns the closed FileDescriptor on success, or an error code + pub fn close(&mut self, fd: i32) -> Result { + if fd < 0 || fd as usize >= MAX_FDS { + return Err(9); // EBADF - bad file descriptor + } + self.fds[fd as usize].take().ok_or(9) // EBADF + } + + /// Duplicate a file descriptor to a specific slot + /// Used for dup2() syscall + #[allow(dead_code)] + pub fn dup2(&mut self, old_fd: i32, new_fd: i32) -> Result { + if old_fd < 0 || old_fd as usize >= MAX_FDS { + return Err(9); // EBADF + } + if new_fd < 0 || new_fd as usize >= MAX_FDS { + return Err(9); // EBADF + } + + let fd_entry = self.fds[old_fd as usize].clone().ok_or(9)?; + + // Close new_fd if it's open (silently ignore errors) + let _ = self.close(new_fd); + + self.fds[new_fd as usize] = Some(fd_entry); + Ok(new_fd) + } + + /// Duplicate a file descriptor to the lowest available slot + /// Used for dup() syscall + #[allow(dead_code)] + pub fn dup(&mut self, old_fd: i32) -> Result { + if old_fd < 0 || old_fd as usize >= MAX_FDS { + return Err(9); // EBADF + } + + let fd_entry = self.fds[old_fd as usize].clone().ok_or(9)?; + + // Find lowest available slot + for i in 0..MAX_FDS { + if self.fds[i].is_none() { + self.fds[i] = Some(fd_entry); + return Ok(i as i32); + } + } + Err(24) // EMFILE + } +} diff --git a/kernel/src/ipc/mod.rs b/kernel/src/ipc/mod.rs new file mode 100644 index 0000000..af2a78f --- /dev/null +++ b/kernel/src/ipc/mod.rs @@ -0,0 +1,12 @@ +//! Inter-Process Communication (IPC) module +//! +//! This module provides IPC primitives for Breenix: +//! - File descriptors (fd.rs) - Per-process file descriptor tables +//! - Pipes (pipe.rs) - Unidirectional byte streams + +pub mod fd; +pub mod pipe; + +// Re-export public API - some of these are not used yet but are part of the public API +pub use fd::{FdKind, FdTable}; +pub use pipe::create_pipe; diff --git a/kernel/src/ipc/pipe.rs b/kernel/src/ipc/pipe.rs new file mode 100644 index 0000000..98feb19 --- /dev/null +++ b/kernel/src/ipc/pipe.rs @@ -0,0 +1,174 @@ +//! Pipe buffer implementation +//! +//! Pipes provide unidirectional byte streams for inter-process communication. +//! This module implements the kernel-side pipe buffer that connects the +//! read and write ends of a pipe. + +use alloc::vec::Vec; + +/// Default pipe buffer size (matches Linux) +pub const PIPE_BUF_SIZE: usize = 65536; + +/// Pipe buffer - a circular buffer with reader/writer tracking +pub struct PipeBuffer { + /// The buffer storage + buffer: Vec, + /// Read position in the circular buffer + read_pos: usize, + /// Write position in the circular buffer + write_pos: usize, + /// Number of bytes currently in the buffer + len: usize, + /// Number of active readers (0 = broken pipe on write) + readers: usize, + /// Number of active writers (0 = EOF on read) + writers: usize, +} + +impl PipeBuffer { + /// Create a new pipe buffer + pub fn new() -> Self { + let mut buffer = Vec::with_capacity(PIPE_BUF_SIZE); + buffer.resize(PIPE_BUF_SIZE, 0); + PipeBuffer { + buffer, + read_pos: 0, + write_pos: 0, + len: 0, + readers: 1, + writers: 1, + } + } + + /// Read from the pipe buffer + /// + /// Returns: + /// - Ok(n) where n > 0: n bytes were read + /// - Ok(0): EOF (no writers remaining) + /// - Err(11): EAGAIN - would block (buffer empty but writers exist) + pub fn read(&mut self, buf: &mut [u8]) -> Result { + if self.len == 0 { + // Buffer is empty + if self.writers == 0 { + // No writers - EOF + return Ok(0); + } else { + // Writers exist but no data - would block + return Err(11); // EAGAIN + } + } + + // Read up to buf.len() bytes + let to_read = buf.len().min(self.len); + let mut read = 0; + + while read < to_read { + buf[read] = self.buffer[self.read_pos]; + self.read_pos = (self.read_pos + 1) % PIPE_BUF_SIZE; + read += 1; + } + + self.len -= read; + Ok(read) + } + + /// Write to the pipe buffer + /// + /// Returns: + /// - Ok(n) where n > 0: n bytes were written + /// - Err(32): EPIPE - broken pipe (no readers) + /// - Err(11): EAGAIN - would block (buffer full) + pub fn write(&mut self, buf: &[u8]) -> Result { + if self.readers == 0 { + // No readers - broken pipe + return Err(32); // EPIPE + } + + let available = PIPE_BUF_SIZE - self.len; + if available == 0 { + // Buffer is full - would block + return Err(11); // EAGAIN + } + + // Write up to available space + let to_write = buf.len().min(available); + let mut written = 0; + + while written < to_write { + self.buffer[self.write_pos] = buf[written]; + self.write_pos = (self.write_pos + 1) % PIPE_BUF_SIZE; + written += 1; + } + + self.len += written; + Ok(written) + } + + /// Check if pipe is readable (has data or EOF) + #[allow(dead_code)] + pub fn is_readable(&self) -> bool { + self.len > 0 || self.writers == 0 + } + + /// Check if pipe is writable (has space and readers exist) + #[allow(dead_code)] + pub fn is_writable(&self) -> bool { + self.len < PIPE_BUF_SIZE && self.readers > 0 + } + + /// Close the read end of the pipe + pub fn close_read(&mut self) { + if self.readers > 0 { + self.readers -= 1; + } + } + + /// Close the write end of the pipe + pub fn close_write(&mut self) { + if self.writers > 0 { + self.writers -= 1; + } + } + + /// Get the number of bytes available to read + #[allow(dead_code)] + pub fn available(&self) -> usize { + self.len + } + + /// Get the space available for writing + #[allow(dead_code)] + pub fn space(&self) -> usize { + PIPE_BUF_SIZE - self.len + } + + /// Check if pipe has active readers (used by write to detect broken pipe) + #[allow(dead_code)] + pub fn has_readers(&self) -> bool { + self.readers > 0 + } + + /// Check if pipe has active writers (used by read to detect EOF) + #[allow(dead_code)] + pub fn has_writers(&self) -> bool { + self.writers > 0 + } +} + +impl Default for PipeBuffer { + fn default() -> Self { + Self::new() + } +} + +/// Create a new pipe +/// +/// Returns (read_buffer, write_buffer) where both point to the same underlying buffer +/// wrapped in Arc for shared access. +pub fn create_pipe() -> ( + alloc::sync::Arc>, + alloc::sync::Arc>, +) { + let buffer = alloc::sync::Arc::new(spin::Mutex::new(PipeBuffer::new())); + (buffer.clone(), buffer) +} diff --git a/kernel/src/main.rs b/kernel/src/main.rs index f2ded65..112c6d9 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -43,6 +43,7 @@ mod per_cpu; mod process; mod rtc_test; mod signal; +mod ipc; mod serial; mod socket; mod spinlock; @@ -189,7 +190,7 @@ fn kernel_main(boot_info: &'static mut bootloader_api::BootInfo) -> ! { let initial_kernel_stack = memory::kernel_stack::allocate_kernel_stack() .expect("Failed to allocate initial kernel stack"); let stack_top = initial_kernel_stack.top(); - per_cpu::set_kernel_stack_top(stack_top); + per_cpu::set_kernel_stack_top(stack_top.as_u64()); // Use gdt::set_tss_rsp0 which works with TSS_PTR directly gdt::set_tss_rsp0(stack_top); log::info!("Initial TSS.RSP0 set to {:#x}", stack_top); @@ -327,8 +328,8 @@ fn kernel_main(boot_info: &'static mut bootloader_api::BootInfo) -> ! { x86_64::instructions::interrupts::without_interrupts(|| { // Set TSS.RSP0 to the kernel stack BEFORE switching // This ensures interrupts from userspace will use the correct stack - per_cpu::set_kernel_stack_top(idle_kernel_stack_top); - per_cpu::update_tss_rsp0(idle_kernel_stack_top); + per_cpu::set_kernel_stack_top(idle_kernel_stack_top.as_u64()); + per_cpu::update_tss_rsp0(idle_kernel_stack_top.as_u64()); log::info!("TSS.RSP0 set to kernel stack at {:#x}", idle_kernel_stack_top); // Keep the kernel stack alive (it will be used forever) @@ -402,9 +403,9 @@ extern "C" fn kernel_main_on_kernel_stack(_arg: *mut core::ffi::c_void) -> ! { // CRITICAL: Ensure TSS.RSP0 is set to the kernel stack // This was already done before the stack switch, but verify it - per_cpu::set_kernel_stack_top(idle_kernel_stack_top); - per_cpu::update_tss_rsp0(idle_kernel_stack_top); - + per_cpu::set_kernel_stack_top(idle_kernel_stack_top.as_u64()); + per_cpu::update_tss_rsp0(idle_kernel_stack_top.as_u64()); + log::info!("TSS.RSP0 verified at {:#x}", idle_kernel_stack_top); // Initialize scheduler with init_task as the current thread @@ -617,6 +618,10 @@ fn kernel_main_continue() -> ! { // log::info!("=== SIGNAL TEST: Register preservation across signals ==="); // test_exec::test_signal_regs(); + // Test pipe IPC syscalls + log::info!("=== IPC TEST: Pipe syscall functionality ==="); + test_exec::test_pipe(); + // Run fault tests to validate privilege isolation log::info!("=== FAULT TEST: Running privilege violation tests ==="); userspace_fault_tests::run_fault_tests(); diff --git a/kernel/src/net/udp.rs b/kernel/src/net/udp.rs index 69b11f6..f163034 100644 --- a/kernel/src/net/udp.rs +++ b/kernel/src/net/udp.rs @@ -136,7 +136,8 @@ fn deliver_to_socket( src_port: u16, payload: &[u8], ) { - use crate::socket::{FdKind, udp::UdpPacket}; + use crate::ipc::fd::FdKind; + use crate::socket::udp::UdpPacket; // Access process manager with interrupts disabled to prevent deadlock let result = crate::process::with_process_manager(|manager| { @@ -151,9 +152,10 @@ fn deliver_to_socket( // Find the socket in the process's fd_table // We need to iterate through all FDs to find the one with matching port - for fd_num in 3..crate::socket::MAX_FDS { - if let Some(fd_entry) = process.fd_table.get(fd_num as u32) { - if let FdKind::UdpSocket(socket) = &fd_entry.kind { + for fd_num in 3..crate::ipc::fd::MAX_FDS { + if let Some(fd_entry) = process.fd_table.get(fd_num as i32) { + if let FdKind::UdpSocket(socket_ref) = &fd_entry.kind { + let socket = socket_ref.lock(); if socket.local_port == Some(dst_port) { // Found the socket! Enqueue the packet let packet = UdpPacket { diff --git a/kernel/src/per_cpu.rs b/kernel/src/per_cpu.rs index 9dd9659..5275637 100644 --- a/kernel/src/per_cpu.rs +++ b/kernel/src/per_cpu.rs @@ -19,17 +19,17 @@ static MAX_PREEMPT_IMBALANCE: AtomicU64 = AtomicU64::new(0); #[repr(C, align(64))] pub struct PerCpuData { /// CPU ID (offset 0) - for multi-processor support - pub cpu_id: usize, - + pub cpu_id: u64, + /// Current thread pointer (offset 8) pub current_thread: *mut crate::task::thread::Thread, - + /// Kernel stack pointer for syscalls/interrupts (offset 16) - TSS.RSP0 - pub kernel_stack_top: VirtAddr, - + pub kernel_stack_top: u64, + /// Idle thread pointer (offset 24) pub idle_thread: *mut crate::task::thread::Thread, - + /// Preempt count for kernel preemption control (offset 32) - properly aligned u32 /// Linux-style bit layout: /// Bits 0-7: PREEMPT count (nested preempt_disable calls) @@ -39,19 +39,19 @@ pub struct PerCpuData { /// Bit 28: PREEMPT_ACTIVE flag /// Bits 29-31: Reserved pub preempt_count: u32, - + /// Reschedule needed flag (offset 36) - u8 for compact layout pub need_resched: u8, - + /// Explicit padding to maintain alignment (offset 37-39) _pad: [u8; 3], - + /// User RSP scratch space for syscall entry (offset 40) pub user_rsp_scratch: u64, - + /// TSS pointer for this CPU (offset 48) pub tss: *mut x86_64::structures::tss::TaskStateSegment, - + /// Softirq pending bitmap (offset 56) - 32 bits for different softirq types pub softirq_pending: u32, @@ -73,7 +73,34 @@ pub struct PerCpuData { /// Exception cleanup context flag (offset 88) - allows scheduling from kernel mode /// Set by exception handlers (GPF, page fault) when they terminate a process /// and need to allow scheduling from kernel mode - pub exception_cleanup_context: bool, + pub exception_cleanup_context: u8, + + /// Padding to align diagnostic fields (offset 89-95) + _pad3: [u8; 7], + + // === Context Switch Diagnostics (Ultra-low overhead) === + // These fields detect state corruption during context switches without + // adding logging overhead to the hot path. Based on seL4/Linux patterns. + + /// Pre-switch canary (offset 96): RSP ^ CR3 | MAGIC_PRE + /// Set before context switch, verified after to detect corruption + pub switch_pre_canary: u64, + + /// Post-switch canary (offset 104): RSP ^ CR3 | MAGIC_POST + /// Set after context switch for comparison with pre-canary + pub switch_post_canary: u64, + + /// TSC timestamp (offset 112): rdtsc value when context switch started + /// Used to detect stuck transitions (timeout detection) + pub switch_tsc: u64, + + /// Switch violation count (offset 120): Number of detected violations + /// Incremented atomically on canary mismatch + pub switch_violations: u64, + + /// Padding to reach 192 bytes (align(64) boundary) + /// (offset 128-191): 64 bytes of padding + _pad_final: [u8; 64], } // Linux-style preempt_count bit layout constants @@ -147,6 +174,19 @@ const KERNEL_CR3_OFFSET: usize = 72; // offset 72: u64 (8 bytes) - ALIGNED const SAVED_PROCESS_CR3_OFFSET: usize = 80; // offset 80: u64 (8 bytes) - ALIGNED #[allow(dead_code)] const EXCEPTION_CLEANUP_CONTEXT_OFFSET: usize = 88; // offset 88: bool (1 byte) +// _pad3 at offset 89-95 (7 bytes) +#[allow(dead_code)] +const SWITCH_PRE_CANARY_OFFSET: usize = 96; // offset 96: u64 (8 bytes) +#[allow(dead_code)] +const SWITCH_POST_CANARY_OFFSET: usize = 104; // offset 104: u64 (8 bytes) +#[allow(dead_code)] +const SWITCH_TSC_OFFSET: usize = 112; // offset 112: u64 (8 bytes) +#[allow(dead_code)] +const SWITCH_VIOLATIONS_OFFSET: usize = 120; // offset 120: u64 (8 bytes) + +// Magic values for canary computation (non-zero to detect corruption) +#[allow(dead_code)] +const CANARY_MAGIC: u64 = 0xDEADCAFE_00000000; // Compile-time assertions to ensure offsets are correct // These will fail to compile if the offsets don't match expected values @@ -155,9 +195,9 @@ const _: () = assert!(PREEMPT_COUNT_OFFSET == 32, "preempt_count offset mismatch const _: () = assert!(USER_RSP_SCRATCH_OFFSET % 8 == 0, "user_rsp_scratch must be 8-byte aligned"); const _: () = assert!(core::mem::size_of::() == 8, "This code assumes 64-bit pointers"); -// Verify struct size is 128 bytes due to align(64) attribute -// The actual data is 88 bytes (saved_process_cr3 at offset 80), but align(64) rounds up to 128 -const _: () = assert!(core::mem::size_of::() == 128, "PerCpuData must be 128 bytes (aligned to 64)"); +// Verify struct size is 192 bytes due to align(64) attribute +// The actual data is 128 bytes (switch_violations ends at offset 128), but align(64) rounds up to 192 +const _: () = assert!(core::mem::size_of::() == 192, "PerCpuData must be 192 bytes (aligned to 64)"); // Verify bit layout matches Linux kernel const _: () = assert!(PREEMPT_MASK == 0x000000FF, "PREEMPT_MASK incorrect"); @@ -170,9 +210,9 @@ impl PerCpuData { /// Create a new per-CPU data structure pub const fn new(cpu_id: usize) -> Self { Self { - cpu_id, + cpu_id: cpu_id as u64, current_thread: ptr::null_mut(), - kernel_stack_top: VirtAddr::new(0), + kernel_stack_top: 0, idle_thread: ptr::null_mut(), preempt_count: 0, need_resched: 0, @@ -184,7 +224,13 @@ impl PerCpuData { next_cr3: 0, kernel_cr3: 0, saved_process_cr3: 0, - exception_cleanup_context: false, + exception_cleanup_context: 0, + _pad3: [0; 7], + switch_pre_canary: 0, + switch_post_canary: 0, + switch_tsc: 0, + switch_violations: 0, + _pad_final: [0; 64], } } } @@ -277,7 +323,7 @@ pub fn set_current_thread(thread: *mut crate::task::thread::Thread) { } /// Get the kernel stack top from per-CPU data -pub fn kernel_stack_top() -> VirtAddr { +pub fn kernel_stack_top() -> u64 { unsafe { // Access kernel_stack_top field via GS segment // Offset 16 = cpu_id (8) + current_thread (8) @@ -287,18 +333,18 @@ pub fn kernel_stack_top() -> VirtAddr { out(reg) stack_top, options(nostack, preserves_flags) ); - VirtAddr::new(stack_top) + stack_top } } /// Set the kernel stack top in per-CPU data -pub fn set_kernel_stack_top(stack_top: VirtAddr) { +pub fn set_kernel_stack_top(stack_top: u64) { unsafe { // Write to kernel_stack_top field via GS segment // Offset 16 = cpu_id (8) + current_thread (8) core::arch::asm!( "mov gs:[16], {}", - in(reg) stack_top.as_u64(), + in(reg) stack_top, options(nostack, preserves_flags) ); } @@ -631,7 +677,7 @@ pub fn set_idle_thread(thread: *mut crate::task::thread::Thread) { /// Update TSS RSP0 with the current thread's kernel stack /// This must be called on every context switch to a thread -pub fn update_tss_rsp0(kernel_stack_top: VirtAddr) { +pub fn update_tss_rsp0(kernel_stack_top: u64) { unsafe { // BUG FIX: Previously this code read gs:0 expecting a pointer to PerCpuData, // but gs:0 contains cpu_id (value 0), not a pointer. When cpu_id is 0, @@ -654,13 +700,13 @@ pub fn update_tss_rsp0(kernel_stack_top: VirtAddr) { // Update per-CPU kernel_stack_top at offset 16 (KERNEL_STACK_TOP_OFFSET) core::arch::asm!( "mov gs:[{offset}], {}", - in(reg) kernel_stack_top.as_u64(), + in(reg) kernel_stack_top, offset = const KERNEL_STACK_TOP_OFFSET, options(nostack, preserves_flags) ); // Update TSS.RSP0 - (*tss_ptr).privilege_stack_table[0] = kernel_stack_top; + (*tss_ptr).privilege_stack_table[0] = VirtAddr::new(kernel_stack_top); // log::trace!("Updated TSS.RSP0 to {:#x}", kernel_stack_top); // Disabled to avoid deadlock } @@ -1058,11 +1104,9 @@ pub fn set_exception_cleanup_context() { } unsafe { - // Set exception_cleanup_context to true (1) at offset 88 - let value: u8 = 1; + // Set exception_cleanup_context to 1 at offset 88 core::arch::asm!( - "mov byte ptr gs:[{offset}], {val}", - val = in(reg_byte) value, + "mov byte ptr gs:[{offset}], 1", offset = const EXCEPTION_CLEANUP_CONTEXT_OFFSET, options(nostack, preserves_flags) ); @@ -1077,11 +1121,9 @@ pub fn clear_exception_cleanup_context() { } unsafe { - // Set exception_cleanup_context to false (0) at offset 88 - let value: u8 = 0; + // Set exception_cleanup_context to 0 at offset 88 core::arch::asm!( - "mov byte ptr gs:[{offset}], {val}", - val = in(reg_byte) value, + "mov byte ptr gs:[{offset}], 0", offset = const EXCEPTION_CLEANUP_CONTEXT_OFFSET, options(nostack, preserves_flags) ); diff --git a/kernel/src/process/creation.rs b/kernel/src/process/creation.rs index df69486..f459658 100644 --- a/kernel/src/process/creation.rs +++ b/kernel/src/process/creation.rs @@ -26,11 +26,13 @@ pub fn create_user_process(name: String, elf_data: &[u8]) -> Result Result SocketHandle { handle } -/// Types of file descriptors -#[derive(Debug)] -pub enum FdKind { - /// Standard input - Stdin, - /// Standard output - Stdout, - /// Standard error - Stderr, - /// UDP socket - UdpSocket(Box), -} - -/// File descriptor entry -#[derive(Debug)] -pub struct FileDescriptor { - /// Type of this file descriptor - pub kind: FdKind, - /// Flags (O_NONBLOCK, etc.) - part of API, not yet used - pub _flags: u32, -} - -impl FileDescriptor { - /// Create a new file descriptor - pub fn new(kind: FdKind, flags: u32) -> Self { - FileDescriptor { kind, _flags: flags } - } -} - -/// File descriptor table for a process -pub struct FdTable { - /// Fixed-size table of file descriptors - table: [Option; MAX_FDS], -} - -impl Default for FdTable { - fn default() -> Self { - Self::new() - } -} - -impl FdTable { - /// Create a new FD table with stdin/stdout/stderr pre-allocated - pub fn new() -> Self { - // Initialize with all None - const NONE: Option = None; - let mut table = [NONE; MAX_FDS]; - - // Pre-allocate standard file descriptors - table[0] = Some(FileDescriptor::new(FdKind::Stdin, 0)); - table[1] = Some(FileDescriptor::new(FdKind::Stdout, 0)); - table[2] = Some(FileDescriptor::new(FdKind::Stderr, 0)); - - FdTable { table } - } - - /// Allocate a new file descriptor, returning its number - pub fn alloc(&mut self, fd: FileDescriptor) -> Option { - // Find first free slot (starting from 3 to skip stdin/stdout/stderr) - for i in 3..MAX_FDS { - if self.table[i].is_none() { - self.table[i] = Some(fd); - return Some(i as u32); - } - } - None // No free slots - } - - /// Get a reference to a file descriptor - pub fn get(&self, fd: u32) -> Option<&FileDescriptor> { - if (fd as usize) < MAX_FDS { - self.table[fd as usize].as_ref() - } else { - None - } - } - - /// Get a mutable reference to a file descriptor - pub fn get_mut(&mut self, fd: u32) -> Option<&mut FileDescriptor> { - if (fd as usize) < MAX_FDS { - self.table[fd as usize].as_mut() - } else { - None - } - } - - /// Close a file descriptor (part of API, not yet used) - #[allow(dead_code)] - pub fn close(&mut self, fd: u32) -> Result<(), i32> { - if (fd as usize) < MAX_FDS { - if self.table[fd as usize].is_some() { - self.table[fd as usize] = None; - Ok(()) - } else { - Err(crate::syscall::errno::EBADF) // Bad file descriptor - } - } else { - Err(crate::syscall::errno::EBADF) - } - } -} - -impl core::fmt::Debug for FdTable { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - let open_count = self.table.iter().filter(|fd| fd.is_some()).count(); - f.debug_struct("FdTable") - .field("open_fds", &open_count) - .finish() - } -} - /// Global socket registry - maps ports to sockets for incoming packet dispatch pub struct SocketRegistry { /// UDP port bindings: port -> (pid, socket_handle) diff --git a/kernel/src/syscall/dispatcher.rs b/kernel/src/syscall/dispatcher.rs index 98796f1..fd99a14 100644 --- a/kernel/src/syscall/dispatcher.rs +++ b/kernel/src/syscall/dispatcher.rs @@ -53,5 +53,7 @@ pub fn dispatch_syscall( SyscallNumber::Bind => super::socket::sys_bind(arg1, arg2, arg3), SyscallNumber::SendTo => super::socket::sys_sendto(arg1, arg2, arg3, arg4, arg5, arg6), SyscallNumber::RecvFrom => super::socket::sys_recvfrom(arg1, arg2, arg3, arg4, arg5, arg6), + SyscallNumber::Pipe => super::pipe::sys_pipe(arg1), + SyscallNumber::Close => super::pipe::sys_close(arg1 as i32), } } diff --git a/kernel/src/syscall/errno.rs b/kernel/src/syscall/errno.rs index 90f9094..ff89612 100644 --- a/kernel/src/syscall/errno.rs +++ b/kernel/src/syscall/errno.rs @@ -8,7 +8,8 @@ pub const EBADF: i32 = 9; /// Resource temporarily unavailable (would block) pub const EAGAIN: i32 = 11; -/// Cannot allocate memory +/// Cannot allocate memory (part of memory API) +#[allow(dead_code)] pub const ENOMEM: i32 = 12; /// Bad address diff --git a/kernel/src/syscall/handler.rs b/kernel/src/syscall/handler.rs index 2a7288b..2791e75 100644 --- a/kernel/src/syscall/handler.rs +++ b/kernel/src/syscall/handler.rs @@ -27,6 +27,7 @@ use super::{SyscallNumber, SyscallResult}; use core::sync::atomic::{AtomicBool, Ordering}; +use x86_64::VirtAddr; #[repr(C)] #[derive(Debug)] @@ -172,6 +173,8 @@ pub extern "C" fn rust_syscall_handler(frame: &mut SyscallFrame) { Some(SyscallNumber::RecvFrom) => { super::socket::sys_recvfrom(args.0, args.1, args.2, args.3, args.4, args.5) } + Some(SyscallNumber::Pipe) => super::pipe::sys_pipe(args.0), + Some(SyscallNumber::Close) => super::pipe::sys_close(args.0 as i32), None => { log::warn!("Unknown syscall number: {} - returning ENOSYS", syscall_num); SyscallResult::Err(super::ErrorCode::NoSys as u64) @@ -191,8 +194,8 @@ pub extern "C" fn rust_syscall_handler(frame: &mut SyscallFrame) { // When userspace triggers an interrupt (like int3), the CPU switches to kernel // mode and uses TSS.RSP0 as the kernel stack. This must be set correctly! let kernel_stack_top = crate::per_cpu::kernel_stack_top(); - if kernel_stack_top.as_u64() != 0 { - crate::gdt::set_tss_rsp0(kernel_stack_top); + if kernel_stack_top != 0 { + crate::gdt::set_tss_rsp0(VirtAddr::new(kernel_stack_top)); } else { log::error!("CRITICAL: Cannot set TSS.RSP0 - kernel_stack_top is 0!"); } diff --git a/kernel/src/syscall/handlers.rs b/kernel/src/syscall/handlers.rs index bb2d59c..8754398 100644 --- a/kernel/src/syscall/handlers.rs +++ b/kernel/src/syscall/handlers.rs @@ -10,10 +10,12 @@ use core::sync::atomic::{AtomicBool, Ordering}; /// Global flag to signal that userspace testing is complete and kernel should exit pub static USERSPACE_TEST_COMPLETE: AtomicBool = AtomicBool::new(false); -/// File descriptors +/// File descriptors (legacy constants, now using FdKind-based routing) #[allow(dead_code)] const FD_STDIN: u64 = 0; +#[allow(dead_code)] const FD_STDOUT: u64 = 1; +#[allow(dead_code)] const FD_STDERR: u64 = 2; /// Copy data from userspace memory @@ -53,6 +55,11 @@ fn copy_from_user(user_ptr: u64, len: usize) -> Result, &'static str> { /// /// CRITICAL: Like copy_from_user, this now works WITHOUT switching CR3. /// We rely on kernel mappings being present in all process page tables. +/// +/// NOTE: This function does NOT acquire the PROCESS_MANAGER lock. +/// It only validates the address range. The caller is responsible for +/// ensuring we're in a valid syscall context. This avoids deadlock when +/// called from syscall handlers that already hold the PROCESS_MANAGER lock. pub fn copy_to_user(user_ptr: u64, kernel_ptr: u64, len: usize) -> Result<(), &'static str> { if user_ptr == 0 { return Err("null pointer"); @@ -64,32 +71,6 @@ pub fn copy_to_user(user_ptr: u64, kernel_ptr: u64, len: usize) -> Result<(), &' return Err("invalid userspace address"); } - // Get current thread to find process - just for validation - let current_thread_id = match crate::task::scheduler::current_thread_id() { - Some(id) => id, - None => { - log::error!("copy_to_user: No current thread"); - return Err("no current thread"); - } - }; - - // Verify that we have a valid process (but don't switch page tables) - { - let manager_guard = crate::process::manager(); - if let Some(ref manager) = *manager_guard { - if manager.find_process_by_thread(current_thread_id).is_none() { - log::error!( - "copy_to_user: No process found for thread {}", - current_thread_id - ); - return Err("no process for thread"); - } - } else { - log::error!("copy_to_user: No process manager"); - return Err("no process manager"); - } - } - // CRITICAL: Access user memory WITHOUT switching CR3 // This works because when we're in a syscall from userspace, we're already // using the process's page table, which has both kernel and user mappings @@ -168,8 +149,10 @@ pub fn sys_exit(exit_code: i32) -> SyscallResult { /// sys_write - Write to a file descriptor /// -/// Currently only supports stdout/stderr writing to serial port. +/// Supports stdout/stderr (serial port) and pipe write ends. pub fn sys_write(fd: u64, buf_ptr: u64, count: u64) -> SyscallResult { + use crate::ipc::FdKind; + log::info!( "USERSPACE: sys_write called: fd={}, buf_ptr={:#x}, count={}", fd, @@ -177,11 +160,6 @@ pub fn sys_write(fd: u64, buf_ptr: u64, count: u64) -> SyscallResult { count ); - // Validate file descriptor - if fd != FD_STDOUT && fd != FD_STDERR { - return SyscallResult::Err(22); // EINVAL - } - // Validate buffer pointer and count if buf_ptr == 0 || count == 0 { return SyscallResult::Ok(0); @@ -200,12 +178,78 @@ pub fn sys_write(fd: u64, buf_ptr: u64, count: u64) -> SyscallResult { } }; + // Get current process to look up fd + let thread_id = match crate::task::scheduler::current_thread_id() { + Some(id) => id, + None => { + // Fall back to stdio behavior for kernel threads + return write_to_stdio(fd, &buffer); + } + }; + let manager_guard = crate::process::manager(); + let process = match &*manager_guard { + Some(manager) => match manager.find_process_by_thread(thread_id) { + Some((_pid, p)) => p, + None => { + // Fall back to stdio behavior for kernel threads + return write_to_stdio(fd, &buffer); + } + }, + None => { + // Fall back to stdio behavior for kernel threads + return write_to_stdio(fd, &buffer); + } + }; + + // Look up the file descriptor + let fd_entry = match process.fd_table.get(fd as i32) { + Some(entry) => entry, + None => { + log::error!("sys_write: Bad fd {}", fd); + return SyscallResult::Err(9); // EBADF + } + }; + + match &fd_entry.kind { + FdKind::StdIo(n) if *n == 1 || *n == 2 => { + // stdout or stderr - write to serial + write_to_stdio(fd, &buffer) + } + FdKind::StdIo(_) => { + // stdin - can't write + SyscallResult::Err(9) // EBADF + } + FdKind::PipeWrite(pipe_buffer) => { + // Write to pipe + let mut pipe = pipe_buffer.lock(); + match pipe.write(&buffer) { + Ok(n) => { + log::debug!("sys_write: Wrote {} bytes to pipe", n); + SyscallResult::Ok(n as u64) + } + Err(e) => { + log::debug!("sys_write: Pipe write error: {}", e); + SyscallResult::Err(e as u64) + } + } + } + FdKind::PipeRead(_) => { + // Can't write to read end of pipe + SyscallResult::Err(9) // EBADF + } + FdKind::UdpSocket(_) => { + // Can't write to UDP socket - must use sendto + log::error!("sys_write: Cannot write to UDP socket, use sendto instead"); + SyscallResult::Err(95) // EOPNOTSUPP + } + } +} + +/// Helper function to write to stdio (serial port) +fn write_to_stdio(fd: u64, buffer: &[u8]) -> SyscallResult { // Log the actual data being written (for verification) if buffer.len() <= 30 { - // For small writes, show the actual content - let s = core::str::from_utf8(&buffer).unwrap_or(""); - - // Also log the raw bytes in hex for verification + let s = core::str::from_utf8(buffer).unwrap_or(""); let mut hex_str = alloc::string::String::new(); for (i, &byte) in buffer.iter().enumerate() { if i > 0 { @@ -213,22 +257,21 @@ pub fn sys_write(fd: u64, buf_ptr: u64, count: u64) -> SyscallResult { } hex_str.push_str(&alloc::format!("{:02x}", byte)); } - log::info!("sys_write: Writing '{}' ({} bytes) to fd {}", s, buffer.len(), fd); log::info!(" Raw bytes: [{}]", hex_str); } else { log::info!("sys_write: Writing {} bytes to fd {}", buffer.len(), fd); } - + // Write to serial port let mut bytes_written = 0; - for &byte in &buffer { + for &byte in buffer { crate::serial::write_byte(byte); bytes_written += 1; } // Log the output for userspace writes - if let Ok(s) = core::str::from_utf8(&buffer) { + if let Ok(s) = core::str::from_utf8(buffer) { log::info!("USERSPACE OUTPUT: {}", s.trim_end()); } @@ -237,17 +280,89 @@ pub fn sys_write(fd: u64, buf_ptr: u64, count: u64) -> SyscallResult { /// sys_read - Read from a file descriptor /// -/// Currently returns 0 (no data available) as keyboard is async-only. -#[allow(dead_code)] -pub fn sys_read(fd: u64, _buf_ptr: u64, _count: u64) -> SyscallResult { - // Validate file descriptor - if fd != FD_STDIN { - return SyscallResult::Err(22); // EINVAL +/// Supports stdin (returns 0) and pipe read ends. +pub fn sys_read(fd: u64, buf_ptr: u64, count: u64) -> SyscallResult { + use crate::ipc::FdKind; + + log::debug!("sys_read: fd={}, buf_ptr={:#x}, count={}", fd, buf_ptr, count); + + // Validate buffer pointer and count + if buf_ptr == 0 || count == 0 { + return SyscallResult::Ok(0); } - // TODO: Implement synchronous keyboard reading - // For now, always return 0 (no data available) - SyscallResult::Ok(0) + // Get current process to look up fd + let thread_id = match crate::task::scheduler::current_thread_id() { + Some(id) => id, + None => { + // Fall back to stdin behavior for kernel threads + return SyscallResult::Ok(0); + } + }; + let manager_guard = crate::process::manager(); + let process = match &*manager_guard { + Some(manager) => match manager.find_process_by_thread(thread_id) { + Some((_pid, p)) => p, + None => { + // Fall back to stdin behavior for kernel threads + return SyscallResult::Ok(0); + } + }, + None => { + // Fall back to stdin behavior for kernel threads + return SyscallResult::Ok(0); + } + }; + + // Look up the file descriptor + let fd_entry = match process.fd_table.get(fd as i32) { + Some(entry) => entry, + None => { + log::error!("sys_read: Bad fd {}", fd); + return SyscallResult::Err(9); // EBADF + } + }; + + match &fd_entry.kind { + FdKind::StdIo(0) => { + // stdin - no data available (keyboard is async-only) + SyscallResult::Ok(0) + } + FdKind::StdIo(_) => { + // stdout/stderr - can't read + SyscallResult::Err(9) // EBADF + } + FdKind::PipeRead(pipe_buffer) => { + // Read from pipe + let mut user_buf = alloc::vec![0u8; count as usize]; + let mut pipe = pipe_buffer.lock(); + match pipe.read(&mut user_buf) { + Ok(n) => { + if n > 0 { + // Copy to userspace + if copy_to_user(buf_ptr, user_buf.as_ptr() as u64, n).is_err() { + return SyscallResult::Err(14); // EFAULT + } + } + log::debug!("sys_read: Read {} bytes from pipe", n); + SyscallResult::Ok(n as u64) + } + Err(e) => { + log::debug!("sys_read: Pipe read error: {}", e); + SyscallResult::Err(e as u64) + } + } + } + FdKind::PipeWrite(_) => { + // Can't read from write end of pipe + SyscallResult::Err(9) // EBADF + } + FdKind::UdpSocket(_) => { + // Can't read from UDP socket - must use recvfrom + log::error!("sys_read: Cannot read from UDP socket, use recvfrom instead"); + SyscallResult::Err(95) // EOPNOTSUPP + } + } } /// sys_yield - Yield CPU to another task diff --git a/kernel/src/syscall/mod.rs b/kernel/src/syscall/mod.rs index d5ca27d..c5ba2cf 100644 --- a/kernel/src/syscall/mod.rs +++ b/kernel/src/syscall/mod.rs @@ -11,6 +11,7 @@ pub mod handler; pub mod handlers; pub mod memory; pub mod mmap; +pub mod pipe; pub mod signal; pub mod socket; pub mod time; @@ -24,9 +25,10 @@ pub enum SyscallNumber { Exit = 0, Write = 1, Read = 2, - Yield = 3, + Yield = 3, // Note: Linux uses sched_yield = 24, but we use 3 GetTime = 4, Fork = 5, + Close = 6, // Custom number (Linux close = 3, conflicts with our Yield) Mmap = 9, // Linux syscall number for mmap Mprotect = 10, // Linux syscall number for mprotect Munmap = 11, // Linux syscall number for munmap @@ -34,6 +36,7 @@ pub enum SyscallNumber { Sigaction = 13, // Linux syscall number for rt_sigaction Sigprocmask = 14, // Linux syscall number for rt_sigprocmask Sigreturn = 15, // Linux syscall number for rt_sigreturn + Pipe = 22, // Linux syscall number for pipe GetPid = 39, // Linux syscall number for getpid Socket = 41, // Linux syscall number for socket SendTo = 44, // Linux syscall number for sendto @@ -56,6 +59,7 @@ impl SyscallNumber { 3 => Some(Self::Yield), 4 => Some(Self::GetTime), 5 => Some(Self::Fork), + 6 => Some(Self::Close), 9 => Some(Self::Mmap), 10 => Some(Self::Mprotect), 11 => Some(Self::Munmap), @@ -63,6 +67,7 @@ impl SyscallNumber { 13 => Some(Self::Sigaction), 14 => Some(Self::Sigprocmask), 15 => Some(Self::Sigreturn), + 22 => Some(Self::Pipe), 39 => Some(Self::GetPid), 41 => Some(Self::Socket), 44 => Some(Self::SendTo), diff --git a/kernel/src/syscall/pipe.rs b/kernel/src/syscall/pipe.rs new file mode 100644 index 0000000..de4a3c5 --- /dev/null +++ b/kernel/src/syscall/pipe.rs @@ -0,0 +1,169 @@ +//! Pipe syscall implementation +//! +//! Implements the pipe() syscall for creating unidirectional communication channels. + +use super::userptr::copy_to_user; +use super::SyscallResult; +use crate::ipc::{create_pipe, FdKind}; +use crate::process::manager; + +/// sys_pipe - Create a pipe +/// +/// Creates a unidirectional data channel that can be used for inter-process communication. +/// Two file descriptors are returned: pipefd[0] is the read end, pipefd[1] is the write end. +/// +/// # Arguments +/// * `pipefd_ptr` - Pointer to an array of two i32s to receive the file descriptors +/// +/// # Returns +/// * `Ok(0)` on success +/// * `Err(errno)` on failure: +/// - EFAULT (14): Invalid pointer +/// - EMFILE (24): Too many open files +/// - ESRCH (3): Process not found +pub fn sys_pipe(pipefd_ptr: u64) -> SyscallResult { + log::debug!("sys_pipe: Creating pipe, pipefd_ptr={:#x}", pipefd_ptr); + + // Validate output pointer + if pipefd_ptr == 0 { + log::error!("sys_pipe: null pipefd pointer"); + return SyscallResult::Err(14); // EFAULT + } + + // Get current process + let thread_id = match crate::task::scheduler::current_thread_id() { + Some(id) => id, + None => { + log::error!("sys_pipe: No current thread"); + return SyscallResult::Err(3); // ESRCH + } + }; + let mut manager_guard = manager(); + let process = match &mut *manager_guard { + Some(manager) => match manager.find_process_by_thread_mut(thread_id) { + Some((_pid, p)) => p, + None => { + log::error!("sys_pipe: Process not found for thread {}", thread_id); + return SyscallResult::Err(3); // ESRCH + } + }, + None => { + log::error!("sys_pipe: Process manager not initialized"); + return SyscallResult::Err(3); // ESRCH + } + }; + + // Create the pipe buffer (shared between read and write ends) + let (read_buffer, write_buffer) = create_pipe(); + + // Allocate file descriptors for both ends + let read_fd = match process.fd_table.alloc(FdKind::PipeRead(read_buffer)) { + Ok(fd) => fd, + Err(e) => { + log::error!("sys_pipe: Failed to allocate read fd: {}", e); + return SyscallResult::Err(e as u64); + } + }; + + let write_fd = match process.fd_table.alloc(FdKind::PipeWrite(write_buffer)) { + Ok(fd) => fd, + Err(e) => { + // Clean up read fd on failure + let _ = process.fd_table.close(read_fd); + log::error!("sys_pipe: Failed to allocate write fd: {}", e); + return SyscallResult::Err(e as u64); + } + }; + + // Write the file descriptors to user space + let pipefd: [i32; 2] = [read_fd, write_fd]; + if let Err(e) = copy_to_user(pipefd_ptr as *mut [i32; 2], &pipefd) { + // Clean up on failure + let _ = process.fd_table.close(read_fd); + let _ = process.fd_table.close(write_fd); + log::error!("sys_pipe: Failed to copy fds to user: {}", e); + return SyscallResult::Err(14); // EFAULT + } + + log::info!( + "sys_pipe: Created pipe with read_fd={}, write_fd={}", + read_fd, + write_fd + ); + + SyscallResult::Ok(0) +} + +/// sys_close - Close a file descriptor +/// +/// # Arguments +/// * `fd` - The file descriptor to close +/// +/// # Returns +/// * `Ok(0)` on success +/// * `Err(errno)` on failure: +/// - EBADF (9): Bad file descriptor +/// - ESRCH (3): Process not found +pub fn sys_close(fd: i32) -> SyscallResult { + log::debug!("sys_close: Closing fd={}", fd); + + // Get current process + let thread_id = match crate::task::scheduler::current_thread_id() { + Some(id) => id, + None => { + log::error!("sys_close: No current thread"); + return SyscallResult::Err(3); // ESRCH + } + }; + let mut manager_guard = manager(); + let process = match &mut *manager_guard { + Some(manager) => match manager.find_process_by_thread_mut(thread_id) { + Some((_pid, p)) => p, + None => { + log::error!("sys_close: Process not found for thread {}", thread_id); + return SyscallResult::Err(3); // ESRCH + } + }, + None => { + log::error!("sys_close: Process manager not initialized"); + return SyscallResult::Err(3); // ESRCH + } + }; + + // Close the file descriptor + match process.fd_table.close(fd) { + Ok(fd_entry) => { + // Handle cleanup for specific fd types + match fd_entry.kind { + FdKind::PipeRead(buffer) => { + // Mark reader as closed + buffer.lock().close_read(); + log::debug!("sys_close: Closed pipe read end fd={}", fd); + } + FdKind::PipeWrite(buffer) => { + // Mark writer as closed + buffer.lock().close_write(); + log::debug!("sys_close: Closed pipe write end fd={}", fd); + } + FdKind::StdIo(_) => { + log::debug!("sys_close: Closed stdio fd={}", fd); + } + FdKind::UdpSocket(socket_ref) => { + // Unbind the socket if it was bound + let socket = socket_ref.lock(); + if let Some(port) = socket.local_port { + crate::socket::SOCKET_REGISTRY.unbind_udp(port); + log::debug!("sys_close: Closed UDP socket fd={}, unbound port {}", fd, port); + } else { + log::debug!("sys_close: Closed unbound UDP socket fd={}", fd); + } + } + } + SyscallResult::Ok(0) + } + Err(e) => { + log::error!("sys_close: Failed to close fd={}: error {}", fd, e); + SyscallResult::Err(e as u64) + } + } +} diff --git a/kernel/src/syscall/socket.rs b/kernel/src/syscall/socket.rs index 12ccf5e..aa4edab 100644 --- a/kernel/src/syscall/socket.rs +++ b/kernel/src/syscall/socket.rs @@ -2,13 +2,11 @@ //! //! Implements socket, bind, sendto, recvfrom syscalls for UDP. -use alloc::boxed::Box; - -use super::errno::{EAFNOSUPPORT, EAGAIN, EBADF, EFAULT, EINVAL, ENETUNREACH, ENOTSOCK, ENOMEM}; +use super::errno::{EAFNOSUPPORT, EAGAIN, EBADF, EFAULT, EINVAL, ENETUNREACH, ENOTSOCK}; use super::{ErrorCode, SyscallResult}; use crate::socket::types::{AF_INET, SOCK_DGRAM, SockAddrIn}; use crate::socket::udp::UdpSocket; -use crate::socket::{FdKind, FileDescriptor}; +use crate::ipc::fd::FdKind; /// sys_socket - Create a new socket /// @@ -59,19 +57,18 @@ pub fn sys_socket(domain: u64, sock_type: u64, _protocol: u64) -> SyscallResult } }; - // Create UDP socket - let socket = UdpSocket::new(); - let fd = FileDescriptor::new(FdKind::UdpSocket(Box::new(socket)), 0); + // Create UDP socket wrapped in Arc> for sharing + let socket = alloc::sync::Arc::new(spin::Mutex::new(UdpSocket::new())); // Allocate file descriptor in process - match process.fd_table.alloc(fd) { - Some(num) => { + match process.fd_table.alloc(FdKind::UdpSocket(socket)) { + Ok(num) => { log::info!("UDP: Socket created fd={}", num); SyscallResult::Ok(num as u64) } - None => { + Err(e) => { log::warn!("sys_socket: fd_table full (no free slots)"); - SyscallResult::Err(ENOMEM as u64) + SyscallResult::Err(e as u64) } } } @@ -133,18 +130,19 @@ pub fn sys_bind(fd: u64, addr_ptr: u64, addrlen: u64) -> SyscallResult { }; // Get the socket from fd table - let fd_entry = match process.fd_table.get_mut(fd as u32) { + let fd_entry = match process.fd_table.get(fd as i32) { Some(e) => e, None => return SyscallResult::Err(EBADF as u64), }; - // Verify it's a UDP socket - let socket = match &mut fd_entry.kind { - FdKind::UdpSocket(s) => s, + // Verify it's a UDP socket and get Arc> reference + let socket_ref = match &fd_entry.kind { + FdKind::UdpSocket(s) => s.clone(), _ => return SyscallResult::Err(ENOTSOCK as u64), }; - // Bind the socket + // Bind the socket (lock the mutex and hold the guard) + let mut socket = socket_ref.lock(); match socket.bind(pid, addr.addr, addr.port_host()) { Ok(()) => { log::info!("UDP: Socket bound to port {}", addr.port_host()); @@ -225,14 +223,14 @@ pub fn sys_sendto( }; // Get the socket from fd table - let fd_entry = match process.fd_table.get(fd as u32) { + let fd_entry = match process.fd_table.get(fd as i32) { Some(e) => e, None => return SyscallResult::Err(EBADF as u64), }; // Verify it's a UDP socket and extract source port match &fd_entry.kind { - FdKind::UdpSocket(s) => s.local_port().unwrap_or(0), + FdKind::UdpSocket(s) => s.lock().local_port().unwrap_or(0), _ => return SyscallResult::Err(ENOTSOCK as u64), } // manager_guard dropped here, releasing the lock @@ -308,19 +306,19 @@ pub fn sys_recvfrom( }; // Get the socket from fd table - let fd_entry = match process.fd_table.get_mut(fd as u32) { + let fd_entry = match process.fd_table.get(fd as i32) { Some(e) => e, None => return SyscallResult::Err(EBADF as u64), }; - // Verify it's a UDP socket - let socket = match &mut fd_entry.kind { - FdKind::UdpSocket(s) => s, + // Verify it's a UDP socket and get Arc> reference + let socket_ref = match &fd_entry.kind { + FdKind::UdpSocket(s) => s.clone(), _ => return SyscallResult::Err(ENOTSOCK as u64), }; - // Try to receive a packet - let packet = match socket.recv_from() { + // Try to receive a packet (lock the mutex) + let packet = match socket_ref.lock().recv_from() { Some(p) => p, None => return SyscallResult::Err(EAGAIN as u64), // Would block }; diff --git a/kernel/src/test_exec.rs b/kernel/src/test_exec.rs index 1edacff..1aaf28f 100644 --- a/kernel/src/test_exec.rs +++ b/kernel/src/test_exec.rs @@ -1023,3 +1023,38 @@ pub fn test_signal_regs() { } } } + +/// Test pipe syscall functionality +/// +/// TWO-STAGE VALIDATION PATTERN: +/// - Stage 1 (Checkpoint): Process creation +/// - Marker: "Pipe test: process scheduled for execution" +/// - This is a CHECKPOINT confirming process creation succeeded +/// - Stage 2 (Boot stage): Validates pipe operations +/// - Marker: "PIPE_TEST_PASSED" +/// - This PROVES pipe creation, read/write, and close all work +pub fn test_pipe() { + log::info!("Testing pipe syscall functionality"); + + #[cfg(feature = "testing")] + let pipe_test_elf_buf = crate::userspace_test::get_test_binary("pipe_test"); + #[cfg(feature = "testing")] + let pipe_test_elf: &[u8] = &pipe_test_elf_buf; + #[cfg(not(feature = "testing"))] + let pipe_test_elf = &create_hello_world_elf(); + + match crate::process::creation::create_user_process( + String::from("pipe_test"), + pipe_test_elf, + ) { + Ok(pid) => { + log::info!("Created pipe_test process with PID {:?}", pid); + log::info!("Pipe test: process scheduled for execution."); + log::info!(" -> Should print 'PIPE_TEST_PASSED' if pipe operations succeed"); + } + Err(e) => { + log::error!("Failed to create pipe_test process: {}", e); + log::error!("Pipe test cannot run without valid userspace process"); + } + } +} diff --git a/kernel/src/test_userspace.rs b/kernel/src/test_userspace.rs index fe99db0..a508ab8 100644 --- a/kernel/src/test_userspace.rs +++ b/kernel/src/test_userspace.rs @@ -226,10 +226,10 @@ pub fn test_minimal_userspace() { let kernel_stack_top = kernel_stack.top(); crate::serial_println!("Setting TSS RSP0 to {:#x}", kernel_stack_top.as_u64()); - + // Try the per_cpu method first - crate::per_cpu::update_tss_rsp0(kernel_stack_top); - + crate::per_cpu::update_tss_rsp0(kernel_stack_top.as_u64()); + // Also set it directly via GDT module's public function crate::gdt::set_tss_rsp0(kernel_stack_top); crate::serial_println!("Set TSS.RSP0 directly via GDT module"); diff --git a/libs/libbreenix/src/io.rs b/libs/libbreenix/src/io.rs index 267aa9e..b8c94a2 100644 --- a/libs/libbreenix/src/io.rs +++ b/libs/libbreenix/src/io.rs @@ -89,3 +89,30 @@ pub fn println(s: &str) { stdout().write_str(s); stdout().write(b"\n"); } + +/// Close a file descriptor. +/// +/// # Arguments +/// * `fd` - File descriptor to close +/// +/// # Returns +/// 0 on success, negative errno on error. +#[inline] +pub fn close(file: Fd) -> i64 { + unsafe { raw::syscall1(nr::CLOSE, file) as i64 } +} + +/// Create a pipe. +/// +/// Creates a unidirectional data channel. pipefd[0] is the read end, +/// pipefd[1] is the write end. +/// +/// # Arguments +/// * `pipefd` - Array to receive the two file descriptors +/// +/// # Returns +/// 0 on success, negative errno on error. +#[inline] +pub fn pipe(pipefd: &mut [i32; 2]) -> i64 { + unsafe { raw::syscall1(nr::PIPE, pipefd.as_mut_ptr() as u64) as i64 } +} diff --git a/libs/libbreenix/src/syscall.rs b/libs/libbreenix/src/syscall.rs index 62cfff7..a4e460e 100644 --- a/libs/libbreenix/src/syscall.rs +++ b/libs/libbreenix/src/syscall.rs @@ -16,6 +16,7 @@ pub mod nr { pub const YIELD: u64 = 3; pub const GET_TIME: u64 = 4; pub const FORK: u64 = 5; + pub const CLOSE: u64 = 6; // Custom number (not Linux standard) pub const MMAP: u64 = 9; // Linux x86_64 mmap pub const MPROTECT: u64 = 10; // Linux x86_64 mprotect pub const MUNMAP: u64 = 11; // Linux x86_64 munmap @@ -23,6 +24,7 @@ pub mod nr { pub const SIGACTION: u64 = 13; // Linux x86_64 rt_sigaction pub const SIGPROCMASK: u64 = 14; // Linux x86_64 rt_sigprocmask pub const SIGRETURN: u64 = 15; // Linux x86_64 rt_sigreturn + pub const PIPE: u64 = 22; // Linux x86_64 pipe pub const GETPID: u64 = 39; pub const SOCKET: u64 = 41; pub const SENDTO: u64 = 44; diff --git a/userspace/tests/Cargo.toml b/userspace/tests/Cargo.toml index 5cf74ae..5549a02 100644 --- a/userspace/tests/Cargo.toml +++ b/userspace/tests/Cargo.toml @@ -74,6 +74,10 @@ path = "signal_regs_test.rs" name = "udp_socket_test" path = "udp_socket_test.rs" +[[bin]] +name = "pipe_test" +path = "pipe_test.rs" + [profile.release] panic = "abort" lto = true diff --git a/userspace/tests/build.sh b/userspace/tests/build.sh index 9869e8e..4997c3a 100755 --- a/userspace/tests/build.sh +++ b/userspace/tests/build.sh @@ -47,6 +47,7 @@ BINARIES=( "signal_return_test" "signal_regs_test" "udp_socket_test" + "pipe_test" ) echo "Building ${#BINARIES[@]} userspace binaries with libbreenix..." @@ -95,7 +96,7 @@ done echo "" echo "These binaries use libbreenix for syscalls:" echo " - libbreenix::process (exit, fork, exec, getpid, gettid, yield)" -echo " - libbreenix::io (read, write, print, println)" +echo " - libbreenix::io (read, write, print, println, close, pipe)" echo " - libbreenix::time (clock_gettime)" echo " - libbreenix::memory (brk, sbrk)" echo " - libbreenix::signal (kill, sigaction, sigprocmask)" diff --git a/userspace/tests/pipe_test.rs b/userspace/tests/pipe_test.rs new file mode 100644 index 0000000..fe433d9 --- /dev/null +++ b/userspace/tests/pipe_test.rs @@ -0,0 +1,242 @@ +//! Pipe syscall test program +//! +//! Tests the pipe() and close() syscalls for IPC. + +#![no_std] +#![no_main] + +use core::panic::PanicInfo; + +// System call numbers +const SYS_EXIT: u64 = 0; +const SYS_WRITE: u64 = 1; +const SYS_READ: u64 = 2; +const SYS_CLOSE: u64 = 6; +const SYS_PIPE: u64 = 22; + +// Syscall wrappers +#[inline(always)] +unsafe fn syscall1(n: u64, arg1: u64) -> u64 { + let ret: u64; + core::arch::asm!( + "int 0x80", + inlateout("rax") n => ret, + inlateout("rdi") arg1 => _, + out("rcx") _, + out("rdx") _, + out("rsi") _, + out("r8") _, + out("r9") _, + out("r10") _, + out("r11") _, + ); + ret +} + +#[inline(always)] +unsafe fn syscall3(n: u64, arg1: u64, arg2: u64, arg3: u64) -> u64 { + let ret: u64; + core::arch::asm!( + "int 0x80", + inlateout("rax") n => ret, + inlateout("rdi") arg1 => _, + inlateout("rsi") arg2 => _, + inlateout("rdx") arg3 => _, + out("rcx") _, + out("r8") _, + out("r9") _, + out("r10") _, + out("r11") _, + ); + ret +} + +// Helper to write a string +#[inline(always)] +fn write_str(s: &str) { + unsafe { + syscall3(SYS_WRITE, 1, s.as_ptr() as u64, s.len() as u64); + } +} + +// Helper to write a decimal number +#[inline(always)] +fn write_num(n: i64) { + if n < 0 { + write_str("-"); + write_num_inner(-n as u64); + } else { + write_num_inner(n as u64); + } +} + +#[inline(always)] +fn write_num_inner(mut n: u64) { + let mut buf = [0u8; 20]; + let mut i = 19; + + if n == 0 { + write_str("0"); + return; + } + + while n > 0 { + buf[i] = b'0' + (n % 10) as u8; + n /= 10; + i -= 1; + } + + let s = unsafe { core::str::from_utf8_unchecked(&buf[i + 1..]) }; + write_str(s); +} + +// Helper to exit with error message +#[inline(always)] +fn fail(msg: &str) -> ! { + write_str("USERSPACE PIPE: FAIL - "); + write_str(msg); + write_str("\n"); + unsafe { + syscall1(SYS_EXIT, 1); + } + loop {} +} + +#[no_mangle] +pub extern "C" fn _start() -> ! { + write_str("=== Pipe Test Program ===\n"); + + // Phase 1: Create a pipe + write_str("Phase 1: Creating pipe with pipe()...\n"); + let mut pipefd: [i32; 2] = [0, 0]; + let ret = unsafe { syscall1(SYS_PIPE, pipefd.as_mut_ptr() as u64) } as i64; + + if ret < 0 { + write_str(" pipe() returned error: "); + write_num(ret); + write_str("\n"); + fail("pipe() failed"); + } + + write_str(" Pipe created successfully\n"); + write_str(" Read fd: "); + write_num(pipefd[0] as i64); + write_str("\n Write fd: "); + write_num(pipefd[1] as i64); + write_str("\n"); + + // Validate fd numbers are reasonable (should be >= 3 after stdin/stdout/stderr) + if pipefd[0] < 3 || pipefd[1] < 3 { + fail("Pipe fds should be >= 3 (after stdin/stdout/stderr)"); + } + if pipefd[0] == pipefd[1] { + fail("Read and write fds should be different"); + } + write_str(" FD numbers are valid\n"); + + // Phase 2: Write data to pipe + write_str("Phase 2: Writing data to pipe...\n"); + let test_data = b"Hello, Pipe!"; + let write_ret = unsafe { + syscall3(SYS_WRITE, pipefd[1] as u64, test_data.as_ptr() as u64, test_data.len() as u64) + } as i64; + + if write_ret < 0 { + write_str(" write() returned error: "); + write_num(write_ret); + write_str("\n"); + fail("write to pipe failed"); + } + + write_str(" Wrote "); + write_num(write_ret); + write_str(" bytes to pipe\n"); + + if write_ret != test_data.len() as i64 { + fail("Did not write expected number of bytes"); + } + + // Phase 3: Read data from pipe + write_str("Phase 3: Reading data from pipe...\n"); + let mut read_buf = [0u8; 32]; + let read_ret = unsafe { + syscall3(SYS_READ, pipefd[0] as u64, read_buf.as_mut_ptr() as u64, read_buf.len() as u64) + } as i64; + + if read_ret < 0 { + write_str(" read() returned error: "); + write_num(read_ret); + write_str("\n"); + fail("read from pipe failed"); + } + + write_str(" Read "); + write_num(read_ret); + write_str(" bytes from pipe\n"); + + if read_ret != test_data.len() as i64 { + fail("Did not read expected number of bytes"); + } + + // Phase 4: Verify data matches + write_str("Phase 4: Verifying data...\n"); + let read_slice = &read_buf[..read_ret as usize]; + + if read_slice != test_data { + write_str(" Data mismatch!\n"); + write_str(" Expected: "); + if let Ok(s) = core::str::from_utf8(test_data) { + write_str(s); + } + write_str("\n Got: "); + if let Ok(s) = core::str::from_utf8(read_slice) { + write_str(s); + } + write_str("\n"); + fail("Data verification failed"); + } + + write_str(" Data verified: '"); + if let Ok(s) = core::str::from_utf8(read_slice) { + write_str(s); + } + write_str("'\n"); + + // Phase 5: Close the pipe ends + write_str("Phase 5: Closing pipe file descriptors...\n"); + + let close_read = unsafe { syscall1(SYS_CLOSE, pipefd[0] as u64) } as i64; + if close_read < 0 { + write_str(" close(read_fd) returned error: "); + write_num(close_read); + write_str("\n"); + fail("close(read_fd) failed"); + } + write_str(" Closed read fd\n"); + + let close_write = unsafe { syscall1(SYS_CLOSE, pipefd[1] as u64) } as i64; + if close_write < 0 { + write_str(" close(write_fd) returned error: "); + write_num(close_write); + write_str("\n"); + fail("close(write_fd) failed"); + } + write_str(" Closed write fd\n"); + + // All tests passed - emit boot stage markers + write_str("USERSPACE PIPE: ALL TESTS PASSED\n"); + write_str("PIPE_TEST_PASSED\n"); + unsafe { + syscall1(SYS_EXIT, 0); + } + loop {} +} + +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + write_str("PANIC in pipe test!\n"); + unsafe { + syscall1(SYS_EXIT, 1); + } + loop {} +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 539de21..fcc8450 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -516,6 +516,13 @@ fn get_boot_stages() -> Vec { failure_meaning: "UDP socket test did not complete successfully", check_hint: "Check userspace/tests/udp_socket_test.rs for which step failed", }, + // IPC (pipe) tests + BootStage { + name: "Pipe IPC test passed", + marker: "PIPE_TEST_PASSED", + failure_meaning: "pipe() syscall test failed - pipe creation, read/write, or close broken", + check_hint: "Check kernel/src/syscall/pipe.rs and kernel/src/ipc/pipe.rs - verify pipe creation, fd allocation, and read/write operations", + }, // NOTE: ENOSYS syscall verification requires external_test_bins feature // which is not enabled by default. Add back when external binaries are integrated. ]