diff --git a/kernel/src/device/virtio/net.rs b/kernel/src/device/virtio/net.rs index e6c0bc4..b3fa3d8 100644 --- a/kernel/src/device/virtio/net.rs +++ b/kernel/src/device/virtio/net.rs @@ -15,6 +15,7 @@ use crate::mem::dma::{DmaError, DmaRegion, DmaRegionProvider}; use crate::trap::{CurrentTrapFrame, TrapInfo}; use crate::util::lazylock::LazyLock; use crate::util::spinlock::SpinLock; + use core::sync::atomic::fence; use core::sync::atomic::{AtomicBool, Ordering}; diff --git a/kernel/src/fs/DESIGN.md b/kernel/src/fs/DESIGN.md index 8e151e0..d6f3bd2 100644 --- a/kernel/src/fs/DESIGN.md +++ b/kernel/src/fs/DESIGN.md @@ -11,6 +11,9 @@ - Each process owns its own `FdTable` and current working directory; `open` resolves a `Node`, calls `Node::open`, and binds the resulting `File` to an FD in the process table. Container processes route path resolution through the container VFS instead of the global VFS. +- Directory FDs expose their backing `Node` so `openat`/`newfstatat` can resolve relative paths + against the directory handle itself. Directory nodes track parent/name metadata so the kernel + can reconstruct a mount-aware absolute path when needed (e.g. for mount-point resolution). - Common filesystem helpers that operate directly on `Node` live in `fs::ops`; any process-aware path handling stays in `process::fs`. - The VFS differentiates node kinds via `NodeKind` (regular/dir/symlink/device/etc.); device nodes diff --git a/kernel/src/fs/fd.rs b/kernel/src/fs/fd.rs index 224e3e5..d825a46 100644 --- a/kernel/src/fs/fd.rs +++ b/kernel/src/fs/fd.rs @@ -64,6 +64,19 @@ impl FdTable { Ok(fd) } + pub fn open_file_with_flags( + &self, + file: Arc, + close_on_exec: bool, + ) -> Result { + let mut guard = self.inner.lock(); + let fd = guard.allocate_fd(self.next_fd.fetch_add(1, Ordering::AcqRel)); + let mut entry = FdEntry::new(file); + entry.set_close_on_exec(close_on_exec); + guard.set(fd, entry)?; + Ok(fd) + } + pub fn open_fixed(&self, fd: Fd, file: Arc) -> Result<(), VfsError> { let mut guard = self.inner.lock(); if guard.exists(fd) { diff --git a/kernel/src/fs/file.rs b/kernel/src/fs/file.rs index c50ee9e..6154cf0 100644 --- a/kernel/src/fs/file.rs +++ b/kernel/src/fs/file.rs @@ -1,10 +1,11 @@ -use alloc::vec::Vec; +use alloc::{sync::Arc, vec::Vec}; +use core::any::Any; -use super::{DirEntry, VfsError}; +use super::{DirEntry, Node, VfsError}; use crate::util::stream::{ControlError, ControlRequest}; /// Per-open handle that performs I/O and control operations. -pub trait File: Send + Sync { +pub trait File: Send + Sync + Any { fn read(&self, buf: &mut [u8]) -> Result; fn write(&self, _data: &[u8]) -> Result { @@ -22,4 +23,10 @@ pub trait File: Send + Sync { fn ioctl(&self, _request: &ControlRequest<'_>) -> Result { Err(ControlError::Unsupported) } + + fn dir_node(&self) -> Option> { + None + } + + fn as_any(&self) -> &dyn Any; } diff --git a/kernel/src/fs/mod.rs b/kernel/src/fs/mod.rs index 121c72e..0ba70b7 100644 --- a/kernel/src/fs/mod.rs +++ b/kernel/src/fs/mod.rs @@ -30,6 +30,7 @@ pub enum VfsError { AlreadyMounted, AlreadyExists, InvalidPath, + BadFd, NotFound, NotDirectory, NotFile, @@ -116,6 +117,13 @@ impl Vfs { self.resolve_absolute(path, 0) } + pub fn mount_path_for_node(&self, node: &Arc) -> Option { + self.mounts + .iter() + .find(|mount| Arc::ptr_eq(&mount.root, node)) + .map(|mount| mount.path.clone()) + } + fn inject_mount_points(&self, path: &Path, entries: &mut Vec) { let base_components = path.components(); for mount in &self.mounts { diff --git a/kernel/src/fs/node/char_device.rs b/kernel/src/fs/node/char_device.rs index 8f2c492..2686fc0 100644 --- a/kernel/src/fs/node/char_device.rs +++ b/kernel/src/fs/node/char_device.rs @@ -67,4 +67,8 @@ where fn ioctl(&self, request: &ControlRequest<'_>) -> Result { self.device.control(request) } + + fn as_any(&self) -> &dyn core::any::Any { + self + } } diff --git a/kernel/src/fs/node/directory.rs b/kernel/src/fs/node/directory.rs index bc0b498..b02c1af 100644 --- a/kernel/src/fs/node/directory.rs +++ b/kernel/src/fs/node/directory.rs @@ -9,6 +9,14 @@ pub trait DirNode: Send + Sync { fn lookup(&self, name: &PathComponent) -> Result, VfsError>; fn read_dir(&self) -> Result, VfsError>; + fn parent(&self) -> Option> { + None + } + + fn name(&self) -> Option<&str> { + None + } + fn create_file(&self, _name: &str) -> Result, VfsError> { Err(VfsError::ReadOnly) } diff --git a/kernel/src/fs/vfs/fat32.rs b/kernel/src/fs/vfs/fat32.rs index 9eb2d49..a0fe67b 100644 --- a/kernel/src/fs/vfs/fat32.rs +++ b/kernel/src/fs/vfs/fat32.rs @@ -10,7 +10,7 @@ use alloc::{ boxed::Box, format, string::{String, ToString}, - sync::Arc, + sync::{Arc, Weak}, vec, vec::Vec, }; @@ -79,9 +79,12 @@ impl FatFileSystem { } let volume = Arc::new(FatVolume::new(device, bpb)?); let chain = volume.cluster_chain(volume.bpb.root_cluster)?; - let root = Arc::new(FatDirectory { + let root = Arc::new_cyclic(|self_ref| FatDirectory { volume: volume.clone(), chain, + parent: None, + name: None, + self_ref: self_ref.clone(), }); Ok(Self { root }) } @@ -376,6 +379,9 @@ impl BiosParameterBlock { pub struct FatDirectory { volume: Arc>, chain: Vec, + parent: Option>>, + name: Option, + self_ref: Weak>, } struct FatDirFile { @@ -396,6 +402,14 @@ impl File for FatDirFile { fn readdir(&self) -> Result, VfsError> { self.node.read_dir() } + + fn dir_node(&self) -> Option> { + Some(self.node.clone()) + } + + fn as_any(&self) -> &dyn core::any::Any { + self + } } impl Node for FatDirectory { @@ -461,13 +475,20 @@ impl DirNode for FatDirectory { } if entry.cmp_name == target { return match entry.kind { - NodeKind::Directory => Ok(Arc::new(FatDirectory { - volume: self.volume.clone(), - chain: self + NodeKind::Directory => { + let chain = self .volume .cluster_chain(entry.first_cluster) - .map_err(|_| VfsError::Corrupted)?, - })), + .map_err(|_| VfsError::Corrupted)?; + let parent = self.self_ref.upgrade().ok_or(VfsError::Corrupted)?; + Ok(Arc::new_cyclic(|self_ref| FatDirectory { + volume: self.volume.clone(), + chain, + parent: Some(Arc::downgrade(&parent)), + name: Some(entry.name.clone()), + self_ref: self_ref.clone(), + })) + } NodeKind::Regular => Ok(Arc::new(FatFileNode { volume: self.volume.clone(), clusters: self @@ -483,6 +504,17 @@ impl DirNode for FatDirectory { } Err(VfsError::NotFound) } + + fn parent(&self) -> Option> { + self.parent + .as_ref() + .and_then(|parent| parent.upgrade()) + .map(|parent| parent as Arc) + } + + fn name(&self) -> Option<&str> { + self.name.as_deref() + } } pub struct FatFileNode { @@ -529,6 +561,10 @@ impl File for FatFileHandle { *guard = next; Ok(next as u64) } + + fn as_any(&self) -> &dyn core::any::Any { + self + } } impl Node for FatFileNode { diff --git a/kernel/src/fs/vfs/memfs.rs b/kernel/src/fs/vfs/memfs.rs index f723a76..ff3e9b8 100644 --- a/kernel/src/fs/vfs/memfs.rs +++ b/kernel/src/fs/vfs/memfs.rs @@ -1,7 +1,7 @@ use alloc::{ collections::BTreeMap, string::{String, ToString}, - sync::Arc, + sync::{Arc, Weak}, vec::Vec, }; @@ -15,6 +15,9 @@ use crate::fs::{ /// Simple in-memory writable filesystem backed by a tree of nodes. pub struct MemDirectory { inner: SpinLock, + parent: Option>, + name: Option, + self_ref: Weak, } struct DirInner { @@ -23,10 +26,17 @@ struct DirInner { impl MemDirectory { pub fn new() -> Arc { - Arc::new(Self { + Self::new_with_parent(None, None) + } + + fn new_with_parent(parent: Option>, name: Option) -> Arc { + Arc::new_cyclic(|self_ref| Self { inner: SpinLock::new(DirInner { entries: BTreeMap::new(), }), + parent, + name, + self_ref: self_ref.clone(), }) } } @@ -130,6 +140,10 @@ impl File for MemFileHandle { *guard = next; Ok(next as u64) } + + fn as_any(&self) -> &dyn core::any::Any { + self + } } struct MemDirFile { @@ -154,6 +168,14 @@ impl File for MemDirFile { fn readdir(&self) -> Result, VfsError> { self.node.read_dir() } + + fn dir_node(&self) -> Option> { + Some(self.node.clone()) + } + + fn as_any(&self) -> &dyn core::any::Any { + self + } } impl Node for MemFileNode { @@ -216,6 +238,17 @@ impl DirNode for MemDirectory { .ok_or(VfsError::NotFound) } + fn parent(&self) -> Option> { + self.parent + .as_ref() + .and_then(|parent| parent.upgrade()) + .map(|parent| parent as Arc) + } + + fn name(&self) -> Option<&str> { + self.name.as_deref() + } + fn create_file(&self, name: &str) -> Result, VfsError> { let mut inner = self.inner.lock(); if inner.entries.contains_key(name) { @@ -231,7 +264,9 @@ impl DirNode for MemDirectory { if inner.entries.contains_key(name) { return Err(VfsError::AlreadyExists); } - let dir = MemDirectory::new(); + let parent = self.self_ref.upgrade().ok_or(VfsError::Corrupted)?; + let dir = + MemDirectory::new_with_parent(Some(Arc::downgrade(&parent)), Some(name.to_string())); inner.entries.insert(name.to_string(), dir.clone()); Ok(dir) } diff --git a/kernel/src/kernel_proc/DESIGN.md b/kernel/src/kernel_proc/DESIGN.md index aa5631c..1d8a0d4 100644 --- a/kernel/src/kernel_proc/DESIGN.md +++ b/kernel/src/kernel_proc/DESIGN.md @@ -26,6 +26,8 @@ (`cc`/`gcc`/`clang`) into `target/xtask-assets/linux-syscall.elf`. - Additional syscall coverage comes from `linux-syscall-adv.elf` plus its exec target `linux-syscall-child.elf`, built via `xtask-assets` from `xtask-assets/fixtures`. +- TCP socket syscall coverage uses `linux-syscall-net.elf`, built via `xtask-assets` from + `xtask-assets/fixtures`, with a kernel-side smoltcp client driving the accept path. - `xtask` also runs the linux-syscall binary on the host to confirm Linux and Cyrius produce the same stdout for the stdin/file I/O scenario. diff --git a/kernel/src/kernel_proc/linux_box.rs b/kernel/src/kernel_proc/linux_box.rs index 923c5b4..534a65c 100644 --- a/kernel/src/kernel_proc/linux_box.rs +++ b/kernel/src/kernel_proc/linux_box.rs @@ -123,10 +123,17 @@ mod tests { use crate::fs::memfs::MemDirectory; use crate::interrupt::{INTERRUPTS, SYSTEM_TIMER, TimerTicks}; use crate::loader::linux; + use crate::net::runtime::{self, TestNetworkDevice}; + use crate::net::smoltcp::{SmoltcpStack, to_smoltcp_ipv4}; + use crate::net::{IpAddr, Ipv4Addr, NetCidr}; use crate::println; use crate::process::PROCESS_TABLE; use crate::test::kernel_test_case; use crate::thread::{SCHEDULER, SchedulerError}; + use crate::util::spinlock::SpinLock; + use core::sync::atomic::{AtomicBool, Ordering}; + use smoltcp::socket::tcp::{Socket as RawTcpSocket, SocketBuffer as RawTcpSocketBuffer}; + use smoltcp::wire::{IpAddress, IpEndpoint}; /// ELF fixture generated by `xtask` (via `xtask-assets`) under `target/xtask-assets`. const LINUX_SYSCALL_ELF: &[u8] = include_bytes!(concat!( env!("CARGO_MANIFEST_DIR"), @@ -140,11 +147,86 @@ mod tests { env!("CARGO_MANIFEST_DIR"), "/../target/xtask-assets/linux-syscall-child.elf" )); + const LINUX_SYSCALL_NET_ELF: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../target/xtask-assets/linux-syscall-net.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; + const NET_SERVER_IP: Ipv4Addr = Ipv4Addr::new(10, 0, 2, 15); + const NET_CLIENT_IP: Ipv4Addr = Ipv4Addr::new(10, 0, 2, 16); + const NET_PORT: u16 = 12_346; + static NET_CLIENT_DEVICE: SpinLock> = SpinLock::new(None); + static NET_CLIENT_DONE: AtomicBool = AtomicBool::new(false); + static NET_CLIENT_OK: AtomicBool = AtomicBool::new(false); + + fn net_client_entry() -> ! { + let device = NET_CLIENT_DEVICE + .lock() + .take() + .expect("net client device missing"); + let client_addr = NetCidr::new(IpAddr::V4(NET_CLIENT_IP), 24); + let mut client_stack = SmoltcpStack::new(device, &[client_addr]); + + let local_endpoint = + IpEndpoint::new(IpAddress::Ipv4(to_smoltcp_ipv4(NET_CLIENT_IP)), 40_001); + let remote_endpoint = + IpEndpoint::new(IpAddress::Ipv4(to_smoltcp_ipv4(NET_SERVER_IP)), NET_PORT); + + let client_socket = RawTcpSocket::new( + RawTcpSocketBuffer::new(alloc::vec![0; 4096]), + RawTcpSocketBuffer::new(alloc::vec![0; 4096]), + ); + let client_handle = client_stack.sockets_mut().add(client_socket); + + client_stack + .with_context_and_sockets(|cx, sockets| { + sockets.get_mut::(client_handle).connect( + cx, + remote_endpoint, + local_endpoint, + ) + }) + .expect("client connect"); + + let payload = b"PING"; + let mut sent = false; + let mut received = false; + let mut recv_buf = [0u8; 16]; + + for _ in 0..200_000 { + let _ = client_stack.poll(); + let _ = runtime::poll(); + client_stack.with_context_and_sockets(|_, sockets| { + let socket = sockets.get_mut::(client_handle); + if !sent && socket.can_send() { + let _ = socket.send_slice(payload); + sent = true; + } + if socket.can_recv() { + if let Ok(size) = socket.recv_slice(&mut recv_buf) { + if size >= 4 && &recv_buf[..4] == b"PONG" { + received = true; + } + } + } + }); + if received { + break; + } + core::hint::spin_loop(); + } + + NET_CLIENT_OK.store(received, Ordering::Release); + NET_CLIENT_DONE.store(true, Ordering::Release); + + loop { + core::hint::spin_loop(); + } + } #[kernel_test_case] fn linux_binary_reads_stdin_and_file() { @@ -157,7 +239,6 @@ mod tests { Err(SchedulerError::AlreadyStarted) => false, Err(err) => panic!("scheduler start failed: {:?}", err), }; - let root = MemDirectory::new(); force_replace_root(root.clone()); @@ -231,7 +312,7 @@ mod tests { let output = tty.drain_output(); assert_eq!( output, - b"WRITEV\nSTAT:OK\nLSTAT:OK\nDENTS:OK\nIOCTL:OK\nMMAP:OK\nBRK:OK\nARCH:OK\nFORK:CHILD\nEXEC:CHILD\nWAIT:42\n" + b"WRITEV\nSTAT:OK\nLSTAT:OK\nOPENAT:OK\nFSTATAT:OK\nDENTS:OK\nIOCTL:OK\nMMAP:OK\nBRK:OK\nARCH:OK\nFORK:CHILD\nEXEC:CHILD\nWAIT:42\n" ); if started { @@ -239,6 +320,61 @@ mod tests { } } + #[kernel_test_case] + fn linux_binary_accepts_tcp_connection() { + println!("[test] linux_binary_accepts_tcp_connection"); + + 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), + }; + + runtime::reset_for_tests(); + let (server_dev, client_dev) = TestNetworkDevice::pair(); + runtime::init_for_tests(server_dev); + *NET_CLIENT_DEVICE.lock() = Some(client_dev); + NET_CLIENT_DONE.store(false, Ordering::Release); + NET_CLIENT_OK.store(false, Ordering::Release); + + let root = MemDirectory::new(); + force_replace_root(root.clone()); + + let bin = root.create_file("net").expect("create net fixture"); + let handle = bin.open(crate::fs::OpenOptions::new(0)).expect("open net"); + let _ = handle.write(LINUX_SYSCALL_NET_ELF).expect("write net"); + + let tty = global_tty(); + tty.clear_output(); + + let pid = launch_process("/net").expect("launch net fixture"); + SCHEDULER + .spawn_kernel_thread("net-client", net_client_entry) + .expect("spawn net client"); + + super::wait_for_exit(pid); + while !NET_CLIENT_DONE.load(Ordering::Acquire) { + core::hint::spin_loop(); + } + assert!( + NET_CLIENT_OK.load(Ordering::Acquire), + "client did not receive PONG" + ); + + let output = tty.drain_output(); + assert_eq!(output, b"NET:OK\n"); + + if started { + SCHEDULER.shutdown(); + SYSTEM_TIMER + .start_periodic(TimerTicks::new(10_000_000)) + .expect("failed to restart system timer after linux-box test"); + INTERRUPTS.enable(); + } + } + #[kernel_test_case] fn linux_page_fault_trap_frame_matches_cpu_frame() { println!("[test] linux_page_fault_trap_frame_matches_cpu_frame"); diff --git a/kernel/src/net/DESIGN.md b/kernel/src/net/DESIGN.md index 7354b83..9e375a7 100644 --- a/kernel/src/net/DESIGN.md +++ b/kernel/src/net/DESIGN.md @@ -17,6 +17,7 @@ - The runtime is intentionally monomorphic by wrapping devices in a `NetDevice` enum, keeping the smoltcp type stable while still allowing test-only injection. - Default IPv4 configuration targets QEMU user networking (`10.0.2.15/24` with gateway `10.0.2.2`). - `net::spawn_background_tasks` creates a dedicated kernel process/thread that continuously polls the runtime once the scheduler is ready. +- TCP socket handles are not removed immediately on close. `runtime` keeps a small “closing” list and reaps handles only after the smoltcp socket reaches `Closed` or `TimeWait` (and has no pending send queue), providing a best-effort drain before teardown. ## TCP Wrapper - `tcp.rs` maps smoltcp TCP sockets to a std-like blocking interface: @@ -25,6 +26,12 @@ - `TcpStream::{read, write_all}` spin using corresponding `try_*` methods to avoid monopolising the runtime lock. - Accepting a connection replaces the listener’s socket handle with a fresh listening socket, mirroring std’s “accept returns a stream while the listener keeps listening” contract. +## Socket Files +- `socket.rs` bridges Linux TCP socket syscalls into the VFS by exposing `TcpSocketFile`, a `File` + implementation with a small state machine (init → bound → listening → stream). +- `socket/bind/listen/accept` operate on the per-FD `TcpSocketFile` instance; accepted streams are + wrapped in a fresh `TcpSocketFile` so read/write can reuse the existing FD table paths. + ## Test Strategy - TCP integration tests use a test-only virtual wire device pair (`runtime::TestNetworkDevice`) to drive both client and server stacks deterministically inside QEMU. - The server side uses the public TCP wrapper while the client side drives a raw smoltcp socket, with explicit interleaved polling on both stacks. diff --git a/kernel/src/net/mod.rs b/kernel/src/net/mod.rs index de4bbca..4160f9d 100644 --- a/kernel/src/net/mod.rs +++ b/kernel/src/net/mod.rs @@ -1,12 +1,14 @@ pub mod consts; pub mod runtime; pub mod smoltcp; +pub mod socket; pub mod tcp; #[cfg(not(test))] use core::sync::atomic::{AtomicU64, Ordering}; pub use no_std_net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}; +pub use socket::{SocketError, TcpSocketFile}; pub use tcp::{TcpError, TcpListener, TcpStream}; /// IP address with CIDR prefix length. diff --git a/kernel/src/net/runtime.rs b/kernel/src/net/runtime.rs index 06ec61f..145bd97 100644 --- a/kernel/src/net/runtime.rs +++ b/kernel/src/net/runtime.rs @@ -1,5 +1,8 @@ #[cfg(test)] use alloc::sync::Arc; +use alloc::vec::Vec; + +use smoltcp::iface::SocketHandle; use crate::device::net::{LinkState, NetworkDevice, SharedNetworkDevice}; use crate::device::virtio::net::{VirtioNetError, VirtioPciNetDevice}; @@ -22,6 +25,7 @@ pub enum NetError { pub struct NetRuntime { stack: SmoltcpStack, + closing: Vec, } impl NetRuntime { @@ -33,16 +37,52 @@ impl NetRuntime { .routes_mut() .add_default_ipv4_route(gateway) .map_err(NetError::Route)?; - Ok(Self { stack }) + Ok(Self { + stack, + closing: Vec::new(), + }) } pub fn poll(&mut self) -> smoltcp::iface::PollResult { - self.stack.poll() + let result = self.stack.poll(); + self.reap_closed_sockets(); + result } pub fn stack_mut(&mut self) -> &mut SmoltcpStack { &mut self.stack } + + fn defer_close(&mut self, handle: SocketHandle) { + if self.closing.contains(&handle) { + return; + } + self.closing.push(handle); + } + + fn reap_closed_sockets(&mut self) { + let mut idx = 0; + while idx < self.closing.len() { + let handle = self.closing[idx]; + let socket = self + .stack + .sockets_mut() + .get_mut::(handle); + let pending = socket.send_queue(); + let state = socket.state(); + let remove = pending == 0 + && matches!( + state, + smoltcp::socket::tcp::State::Closed | smoltcp::socket::tcp::State::TimeWait + ); + if remove { + let _ = self.stack.sockets_mut().remove(handle); + self.closing.swap_remove(idx); + } else { + idx += 1; + } + } + } } pub fn init() -> Result<(), NetError> { @@ -84,6 +124,10 @@ pub fn is_initialised() -> bool { guard.is_some() } +pub fn defer_socket_close(handle: SocketHandle) -> Result<(), NetError> { + with_runtime(|rt| rt.defer_close(handle)) +} + #[derive(Debug)] pub enum NetDeviceError { Virtio(VirtioNetError), diff --git a/kernel/src/net/socket.rs b/kernel/src/net/socket.rs new file mode 100644 index 0000000..acb797a --- /dev/null +++ b/kernel/src/net/socket.rs @@ -0,0 +1,142 @@ +use alloc::sync::Arc; + +use crate::fs::{File, VfsError}; +use crate::interrupt::INTERRUPTS; +use crate::net::{SocketAddr, TcpError, TcpListener, TcpStream}; +use crate::util::spinlock::SpinLock; + +#[derive(Debug)] +pub enum SocketError { + InvalidState, + Tcp(TcpError), +} + +impl From for SocketError { + fn from(err: TcpError) -> Self { + Self::Tcp(err) + } +} + +enum TcpSocketState { + Init, + Bound(SocketAddr), + Listening(TcpListener), + Stream(TcpStream), +} + +pub struct TcpSocketFile { + state: SpinLock, +} + +impl TcpSocketFile { + pub fn new() -> Arc { + Arc::new(Self { + state: SpinLock::new(TcpSocketState::Init), + }) + } + + pub fn bind(&self, addr: SocketAddr) -> Result<(), SocketError> { + let mut guard = self.state.lock(); + match &*guard { + TcpSocketState::Init => { + *guard = TcpSocketState::Bound(addr); + Ok(()) + } + _ => Err(SocketError::InvalidState), + } + } + + pub fn listen(&self) -> Result<(), SocketError> { + let mut guard = self.state.lock(); + let addr = match &*guard { + TcpSocketState::Bound(addr) => *addr, + _ => return Err(SocketError::InvalidState), + }; + let listener = TcpListener::bind(addr)?; + *guard = TcpSocketState::Listening(listener); + Ok(()) + } + + pub fn accept(&self) -> Result<(TcpStream, SocketAddr), SocketError> { + loop { + let mut guard = self.state.lock(); + match &mut *guard { + TcpSocketState::Listening(listener) => { + if let Some((stream, remote)) = listener.try_accept()? { + return Ok((stream, remote)); + } + } + _ => return Err(SocketError::InvalidState), + } + drop(guard); + // Syscalls may run with interrupts disabled; enable them while waiting. + INTERRUPTS.enable(); + core::hint::spin_loop(); + } + } + + pub fn set_stream(&self, stream: TcpStream) -> Result<(), SocketError> { + let mut guard = self.state.lock(); + match &*guard { + TcpSocketState::Init => { + *guard = TcpSocketState::Stream(stream); + Ok(()) + } + _ => Err(SocketError::InvalidState), + } + } +} + +impl File for TcpSocketFile { + fn read(&self, buf: &mut [u8]) -> Result { + loop { + let mut guard = self.state.lock(); + match &mut *guard { + TcpSocketState::Stream(stream) => match stream.try_read(buf) { + Ok(Some(n)) => return Ok(n), + Ok(None) => {} + Err(_) => return Err(VfsError::UnderlyingDevice), + }, + _ => return Err(VfsError::NotFile), + } + drop(guard); + INTERRUPTS.enable(); + core::hint::spin_loop(); + } + } + + fn write(&self, data: &[u8]) -> Result { + let mut offset = 0usize; + while offset < data.len() { + let mut guard = self.state.lock(); + match &mut *guard { + TcpSocketState::Stream(stream) => match stream.try_write(&data[offset..]) { + Ok(Some(0)) => return Err(VfsError::UnderlyingDevice), + Ok(Some(n)) => offset += n, + Ok(None) => {} + Err(_) => return Err(VfsError::UnderlyingDevice), + }, + _ => return Err(VfsError::NotFile), + } + drop(guard); + if offset < data.len() { + INTERRUPTS.enable(); + core::hint::spin_loop(); + } + } + Ok(data.len()) + } + + fn as_any(&self) -> &dyn core::any::Any { + self + } +} + +impl Drop for TcpSocketFile { + fn drop(&mut self) { + let mut guard = self.state.lock(); + if let TcpSocketState::Stream(stream) = &mut *guard { + let _ = stream.close(); + } + } +} diff --git a/kernel/src/net/tcp.rs b/kernel/src/net/tcp.rs index 07916e6..d93f765 100644 --- a/kernel/src/net/tcp.rs +++ b/kernel/src/net/tcp.rs @@ -15,7 +15,6 @@ const EPHEMERAL_START: u16 = 49_152; const EPHEMERAL_END: u16 = 65_535; static NEXT_EPHEMERAL_PORT: AtomicU16 = AtomicU16::new(EPHEMERAL_START); - #[derive(Debug)] pub enum TcpError { Net(NetError), @@ -251,9 +250,8 @@ impl TcpStream { impl Drop for TcpStream { fn drop(&mut self) { - let _ = runtime::with_runtime(|rt| { - rt.stack_mut().sockets_mut().remove(self.handle); - }); + let _ = self.close(); + let _ = runtime::defer_socket_close(self.handle); } } @@ -307,9 +305,12 @@ fn next_port() -> u16 { #[cfg(test)] mod tests { use alloc::vec; + use alloc::vec::Vec; use smoltcp::iface::SocketHandle; - use smoltcp::socket::tcp::{Socket as RawTcpSocket, SocketBuffer as RawTcpSocketBuffer}; + use smoltcp::socket::tcp::{ + RecvError, Socket as RawTcpSocket, SocketBuffer as RawTcpSocketBuffer, + }; use smoltcp::wire::{IpAddress, IpEndpoint}; use crate::net::runtime::{self, TestNetworkDevice}; @@ -393,6 +394,97 @@ mod tests { assert_eq!(&echoed[..echoed_len], payload); } + #[kernel_test_case] + fn tcp_close_waits_for_tx_drain() { + println!("[test] tcp_close_waits_for_tx_drain"); + + runtime::reset_for_tests(); + let (server_dev, client_dev) = TestNetworkDevice::pair(); + runtime::init_for_tests(server_dev); + + let client_addr = NetCidr::new(IpAddr::V4(CLIENT_IP), 24); + let mut client_stack = SmoltcpStack::new(client_dev, &[client_addr]); + + let server_addr = SocketAddr::new(IpAddr::V4(SERVER_IP), PORT); + let mut listener = TcpListener::bind(server_addr).expect("bind listener"); + + let mut client_socket = RawTcpSocket::new( + RawTcpSocketBuffer::new(vec![0; 128 * 1024]), + RawTcpSocketBuffer::new(vec![0; 128 * 1024]), + ); + let client_handle = client_stack.sockets_mut().add(client_socket); + + let local_endpoint = IpEndpoint::new(IpAddress::Ipv4(to_smoltcp_ipv4(CLIENT_IP)), 40_001); + let remote_endpoint = IpEndpoint::new(IpAddress::Ipv4(to_smoltcp_ipv4(SERVER_IP)), PORT); + + client_stack + .with_context_and_sockets(|cx, sockets| { + sockets.get_mut::(client_handle).connect( + cx, + remote_endpoint, + local_endpoint, + ) + }) + .expect("client connect"); + + let mut accepted = None; + for _ in 0..200_000 { + poll_both(&mut client_stack); + if let Some((stream, _addr)) = listener.try_accept().expect("try_accept") { + accepted = Some(stream); + break; + } + } + let mut stream = accepted.expect("server accepted connection"); + + let payload = vec![0xAC; 64 * 1024]; + let mut sent = 0usize; + for _ in 0..500_000 { + poll_both(&mut client_stack); + if let Some(n) = stream + .try_write(&payload[sent..]) + .expect("server try_write") + { + sent += n; + if sent == payload.len() { + break; + } + } + } + assert_eq!(sent, payload.len()); + stream.close().expect("server close"); + + let mut received = Vec::with_capacity(payload.len()); + let mut finished = false; + for _ in 0..500_000 { + poll_both(&mut client_stack); + let socket = client_stack + .sockets_mut() + .get_mut::(client_handle); + if socket.can_recv() { + let mut buf = [0u8; 4096]; + match socket.recv_slice(&mut buf) { + Ok(n) => { + received.extend_from_slice(&buf[..n]); + if received.len() == payload.len() { + continue; + } + } + Err(RecvError::Finished) => { + finished = true; + break; + } + Err(err) => panic!("client recv error: {:?}", err), + } + } else if !socket.may_recv() { + finished = true; + break; + } + } + assert_eq!(received.len(), payload.len()); + assert!(finished, "client did not observe FIN"); + } + fn poll_both(client_stack: &mut SmoltcpStack) { let _ = client_stack.poll(); let _ = runtime::poll(); diff --git a/kernel/src/process/fs.rs b/kernel/src/process/fs.rs index 90152f9..26465fd 100644 --- a/kernel/src/process/fs.rs +++ b/kernel/src/process/fs.rs @@ -1,13 +1,17 @@ use alloc::string::ToString; +use alloc::sync::Arc; use alloc::vec::Vec; use crate::fs::{ - DirEntry, Fd, NodeKind, OpenOptions, Path, Vfs, VfsError, read_to_end_with_vfs, with_vfs, + DirEntry, Fd, File, Node, NodeKind, OpenOptions, Path, PathComponent, Vfs, VfsError, + read_to_end_with_vfs, with_vfs, }; use crate::util::stream::{ControlError, ControlRequest}; use super::{PROCESS_TABLE, ProcessHandle, ProcessId, ProcessVfs}; +pub const AT_FDCWD: i32 = -100; + fn process_handle(pid: ProcessId) -> Result { PROCESS_TABLE .process_handle(pid) @@ -28,21 +32,53 @@ fn with_process_vfs( } pub fn open_path(pid: ProcessId, raw_path: &str, flags: u64) -> Result { + open_path_with_options(pid, raw_path, flags, false) +} + +pub fn open_path_with_options( + pid: ProcessId, + raw_path: &str, + flags: u64, + close_on_exec: bool, +) -> Result { let process = process_handle(pid)?; let abs = Path::resolve(raw_path, &process.cwd())?; let file = with_process_vfs(&process, |vfs| { vfs.open_absolute(&abs, OpenOptions::new(flags)) })?; + process.fd_table().open_file_with_flags(file, close_on_exec) +} + +pub fn open_file(pid: ProcessId, file: Arc) -> Result { + let process = process_handle(pid)?; process.fd_table().open_file(file) } +pub fn open_file_with_flags( + pid: ProcessId, + file: Arc, + close_on_exec: bool, +) -> Result { + let process = process_handle(pid)?; + process.fd_table().open_file_with_flags(file, close_on_exec) +} + pub fn open_path_with_create(pid: ProcessId, raw_path: &str, flags: u64) -> Result { + open_path_with_create_and_options(pid, raw_path, flags, false) +} + +pub fn open_path_with_create_and_options( + pid: ProcessId, + raw_path: &str, + flags: u64, + close_on_exec: bool, +) -> Result { let process = process_handle(pid)?; let abs = Path::resolve(raw_path, &process.cwd())?; match with_process_vfs(&process, |vfs| { vfs.open_absolute(&abs, OpenOptions::new(flags)) }) { - Ok(file) => process.fd_table().open_file(file), + Ok(file) => process.fd_table().open_file_with_flags(file, close_on_exec), Err(VfsError::NotFound) => { let parent = abs.parent().ok_or(VfsError::InvalidPath)?; let name = abs @@ -55,7 +91,71 @@ pub fn open_path_with_create(pid: ProcessId, raw_path: &str, flags: u64) -> Resu let dir_view = dir.as_dir().ok_or(VfsError::NotDirectory)?; let file_node = dir_view.create_file(&name)?; let file = file_node.clone().open(OpenOptions::new(flags))?; - process.fd_table().open_file(file) + process.fd_table().open_file_with_flags(file, close_on_exec) + } + Err(err) => Err(err), + } +} + +pub fn open_path_at( + pid: ProcessId, + dirfd: i32, + raw_path: &str, + flags: u64, +) -> Result { + open_path_at_with_options(pid, dirfd, raw_path, flags, false) +} + +pub fn open_path_at_with_options( + pid: ProcessId, + dirfd: i32, + raw_path: &str, + flags: u64, + close_on_exec: bool, +) -> Result { + let abs = resolve_path_at(pid, dirfd, raw_path)?; + let process = process_handle(pid)?; + let file = with_process_vfs(&process, |vfs| { + vfs.open_absolute(&abs, OpenOptions::new(flags)) + })?; + process.fd_table().open_file_with_flags(file, close_on_exec) +} + +pub fn open_path_at_with_create( + pid: ProcessId, + dirfd: i32, + raw_path: &str, + flags: u64, +) -> Result { + open_path_at_with_create_and_options(pid, dirfd, raw_path, flags, false) +} + +pub fn open_path_at_with_create_and_options( + pid: ProcessId, + dirfd: i32, + raw_path: &str, + flags: u64, + close_on_exec: bool, +) -> Result { + let abs = resolve_path_at(pid, dirfd, raw_path)?; + let process = process_handle(pid)?; + match with_process_vfs(&process, |vfs| { + vfs.open_absolute(&abs, OpenOptions::new(flags)) + }) { + Ok(file) => process.fd_table().open_file_with_flags(file, close_on_exec), + Err(VfsError::NotFound) => { + let parent = abs.parent().ok_or(VfsError::InvalidPath)?; + let name = abs + .components() + .last() + .ok_or(VfsError::InvalidPath)? + .as_str() + .to_string(); + let dir = with_process_vfs(&process, |vfs| vfs.resolve_node(&parent))?; + let dir_view = dir.as_dir().ok_or(VfsError::NotDirectory)?; + let file_node = dir_view.create_file(&name)?; + let file = file_node.clone().open(OpenOptions::new(flags))?; + process.fd_table().open_file_with_flags(file, close_on_exec) } Err(err) => Err(err), } @@ -128,6 +228,12 @@ pub fn control_fd( entry.file().ioctl(request) } +pub fn fd_file(pid: ProcessId, fd: Fd) -> Result, VfsError> { + let process = process_handle(pid)?; + let entry = process.fd_table().entry(fd)?; + Ok(entry.file().clone()) +} + pub fn change_dir(pid: ProcessId, raw_path: &str) -> Result<(), VfsError> { let process = process_handle(pid)?; let abs = Path::resolve(raw_path, &process.cwd())?; @@ -156,8 +262,35 @@ pub fn stat_path_no_follow( ) -> Result { let process = process_handle(pid)?; let abs = Path::resolve(raw_path, &process.cwd())?; + stat_path_no_follow_abs(&process, &abs) +} + +pub fn stat_path_at( + pid: ProcessId, + dirfd: i32, + raw_path: &str, +) -> Result { + let abs = resolve_path_at(pid, dirfd, raw_path)?; + let process = process_handle(pid)?; + with_process_vfs(&process, |vfs| vfs.stat_absolute(&abs)) +} + +pub fn stat_path_no_follow_at( + pid: ProcessId, + dirfd: i32, + raw_path: &str, +) -> Result { + let abs = resolve_path_at(pid, dirfd, raw_path)?; + let process = process_handle(pid)?; + stat_path_no_follow_abs(&process, &abs) +} + +fn stat_path_no_follow_abs( + process: &ProcessHandle, + abs: &Path, +) -> Result { if abs.components().is_empty() { - return with_process_vfs(&process, |vfs| vfs.stat_absolute(&abs)); + return with_process_vfs(process, |vfs| vfs.stat_absolute(abs)); } let parent = abs.parent().ok_or(VfsError::InvalidPath)?; let name = abs @@ -165,7 +298,7 @@ pub fn stat_path_no_follow( .last() .ok_or(VfsError::InvalidPath)? .clone(); - with_process_vfs(&process, |vfs| { + 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)?; @@ -173,6 +306,60 @@ pub fn stat_path_no_follow( }) } +fn resolve_path_at(pid: ProcessId, dirfd: i32, raw_path: &str) -> Result { + let process = process_handle(pid)?; + if raw_path.is_empty() { + return Err(VfsError::InvalidPath); + } + if raw_path.starts_with('/') { + return Path::resolve(raw_path, &process.cwd()); + } + if dirfd == AT_FDCWD { + return Path::resolve(raw_path, &process.cwd()); + } + if dirfd < 0 { + return Err(VfsError::InvalidPath); + } + + let base = dirfd_base_path(pid, dirfd as u32)?; + Path::resolve(raw_path, &base) +} + +fn dirfd_base_path(pid: ProcessId, fd: Fd) -> Result { + let process = process_handle(pid)?; + let entry = process.fd_table().entry(fd).map_err(|_| VfsError::BadFd)?; + let file = entry.file().clone(); + let node = file.dir_node().ok_or(VfsError::NotDirectory)?; + with_process_vfs(&process, |vfs| dir_node_base_path(vfs, node)) +} + +fn dir_node_base_path(vfs: &Vfs, node: Arc) -> Result { + let mut components = Vec::new(); + let mut current = node; + loop { + if let Some(mount_path) = vfs.mount_path_for_node(¤t) { + let mut base_components = mount_path.components().to_vec(); + components.reverse(); + base_components.extend(components); + return Ok(Path::from_components(true, base_components)); + } + let current_dir = current.as_dir().ok_or(VfsError::NotDirectory)?; + if let Some(name) = current_dir.name() { + components.push(PathComponent::new(name)); + } else if current_dir.parent().is_some() { + return Err(VfsError::InvalidPath); + } + match current_dir.parent() { + Some(parent) => { + current = parent; + } + None => break, + } + } + components.reverse(); + Ok(Path::from_components(true, components)) +} + 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 fc6f4d8..927851e 100644 --- a/kernel/src/syscall/DESIGN.md +++ b/kernel/src/syscall/DESIGN.md @@ -19,13 +19,15 @@ 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`, `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 + `read`, `write`, `open`, `openat`, `close`, `writev`, `stat`, `lstat`, `newfstatat`, `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` (UTS fields sourced from the container context), `geteuid`, and stubbed signal - calls. Unsupported numbers map to `ENOSYS`, while unsupported ioctls map to `ENOTTY`. + calls, plus basic socket setup (`socket`, `bind`, `listen`, `accept`, `accept4`, `sendto`, + `setsockopt`) for TCP/IPv4 streams. + 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/host.rs b/kernel/src/syscall/host.rs index af42fbc..fb3ad6f 100644 --- a/kernel/src/syscall/host.rs +++ b/kernel/src/syscall/host.rs @@ -53,8 +53,10 @@ fn encode_error(err: SysError) -> u64 { SysError::NotImplemented => HostErrno::NotImplemented as u64, SysError::InvalidArgument => HostErrno::InvalidArgument as u64, SysError::NotFound => HostErrno::NotFound as u64, + SysError::BadFileDescriptor => HostErrno::InvalidArgument as u64, SysError::BadAddress => HostErrno::BadAddress as u64, SysError::NotTty => HostErrno::InvalidArgument as u64, + SysError::NotDirectory => 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 b1f5622..ad385b8 100644 --- a/kernel/src/syscall/linux.rs +++ b/kernel/src/syscall/linux.rs @@ -16,6 +16,7 @@ use crate::mem::paging::{FrameAllocator, MapError, PageTableOps, PhysMapper}; use crate::mem::user::{ UserMemoryAccess, copy_from_user, copy_to_user, with_user_slice, with_user_slice_mut, }; +use crate::net::{IpAddr, Ipv4Addr, SocketAddr, TcpSocketFile}; use crate::process::fs as proc_fs; use crate::process::{ControllingTty, PROCESS_TABLE, ProcessHandle, ProcessId}; use crate::thread::SCHEDULER; @@ -30,6 +31,8 @@ use crate::util::stream::{ControlAccess, ControlError, ControlRequest}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum LinuxErrno { NoEntry = 2, + BadFileDescriptor = 9, + NotDirectory = 20, NoSys = 38, InvalidArgument = 22, BadAddress = 14, @@ -46,6 +49,7 @@ pub enum LinuxSyscall { Close = 3, Stat = 4, Lstat = 6, + SendTo = 44, Poll = 7, Lseek = 8, Mmap = 9, @@ -81,7 +85,29 @@ pub enum LinuxSyscall { GetDents64 = 217, SetTidAddress = 218, ClockGetTime = 228, -} + Socket = 41, + Bind = 49, + Listen = 50, + Accept = 43, + Accept4 = 288, + SetSockOpt = 54, + OpenAt = 257, + NewFstatAt = 262, +} + +const AF_INET: u16 = 2; +const SOCK_STREAM: u32 = 1; +const SOCKADDR_IN_LEN: usize = 16; +const AT_SYMLINK_NOFOLLOW: u32 = 0x100; +const LINUX_O_CLOEXEC: u64 = 0x80000; +const MSG_NOSIGNAL: u32 = 0x4000; +const SOL_SOCKET: u32 = 1; +const SO_REUSEADDR: u32 = 2; +const SO_REUSEPORT: u32 = 15; +const IPPROTO_TCP: u32 = 6; +const TCP_NODELAY: u32 = 1; +const SOCK_NONBLOCK: u32 = 0x800; +const SOCK_CLOEXEC: u32 = 0x80000; impl LinuxSyscall { pub fn from_raw(value: u64) -> Option { @@ -103,6 +129,7 @@ impl LinuxSyscall { 16 => Some(Self::Ioctl), 20 => Some(Self::Writev), 39 => Some(Self::GetPid), + 44 => Some(Self::SendTo), 63 => Some(Self::Uname), 96 => Some(Self::GetTimeOfDay), 72 => Some(Self::Fcntl), @@ -128,15 +155,20 @@ impl LinuxSyscall { 217 => Some(Self::GetDents64), 218 => Some(Self::SetTidAddress), 228 => Some(Self::ClockGetTime), + 41 => Some(Self::Socket), + 43 => Some(Self::Accept), + 54 => Some(Self::SetSockOpt), + 49 => Some(Self::Bind), + 50 => Some(Self::Listen), + 257 => Some(Self::OpenAt), + 262 => Some(Self::NewFstatAt), + 288 => Some(Self::Accept4), _ => None, } } } /// 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>, @@ -145,10 +177,12 @@ pub fn dispatch( Some(LinuxSyscall::Read) => DispatchResult::Completed(handle_read(invocation)), Some(LinuxSyscall::Write) => DispatchResult::Completed(handle_write(invocation)), Some(LinuxSyscall::Open) => DispatchResult::Completed(handle_open(invocation)), + Some(LinuxSyscall::OpenAt) => DispatchResult::Completed(handle_openat(invocation)), 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::NewFstatAt) => DispatchResult::Completed(handle_newfstatat(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)), @@ -188,6 +222,13 @@ pub fn dispatch( Some(LinuxSyscall::ClockGetTime) => { DispatchResult::Completed(handle_clock_gettime(invocation)) } + Some(LinuxSyscall::SendTo) => DispatchResult::Completed(handle_sendto(invocation)), + Some(LinuxSyscall::Socket) => DispatchResult::Completed(handle_socket(invocation)), + Some(LinuxSyscall::Bind) => DispatchResult::Completed(handle_bind(invocation)), + Some(LinuxSyscall::Listen) => DispatchResult::Completed(handle_listen(invocation)), + Some(LinuxSyscall::Accept) => DispatchResult::Completed(handle_accept(invocation)), + Some(LinuxSyscall::Accept4) => DispatchResult::Completed(handle_accept4(invocation)), + Some(LinuxSyscall::SetSockOpt) => DispatchResult::Completed(handle_setsockopt(invocation)), Some(LinuxSyscall::Exit) => handle_exit(invocation), Some(LinuxSyscall::ExitGroup) => handle_exit(invocation), None => DispatchResult::Completed(Err(SysError::NotImplemented)), @@ -263,13 +304,45 @@ fn handle_open(invocation: &SyscallInvocation) -> SysResult { })?; let create = (flags & LinuxOpenFlags::Creat as u64) != 0; + let cloexec = (flags & LINUX_O_CLOEXEC) != 0; + let result = if create { + proc_fs::open_path_with_create_and_options(pid, &path, flags, cloexec) + } else { + proc_fs::open_path_with_options(pid, &path, flags, cloexec) + }; + + let fd = result.map_err(map_vfs_error)?; + if path == "/dev/tty" && process.session_id() == pid && !process.has_controlling_tty() { + process.set_controlling_tty(ControllingTty::Global); + } + Ok(fd as u64) +} + +fn handle_openat(invocation: &SyscallInvocation) -> SysResult { + let pid = SCHEDULER + .current_process_id() + .ok_or(SysError::InvalidArgument)?; + let dirfd = invocation.arg(0).ok_or(SysError::InvalidArgument)? as i32; + let ptr = invocation.arg(1).ok_or(SysError::InvalidArgument)?; + let flags = invocation.arg(2).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) + })?; + + let create = (flags & LinuxOpenFlags::Creat as u64) != 0; + let cloexec = (flags & LINUX_O_CLOEXEC) != 0; let result = if create { - proc_fs::open_path_with_create(pid, &path, flags) + proc_fs::open_path_at_with_create_and_options(pid, dirfd, &path, flags, cloexec) } else { - proc_fs::open_path(pid, &path, flags) + proc_fs::open_path_at_with_options(pid, dirfd, &path, flags, cloexec) }; - let fd = result.map_err(|_| SysError::InvalidArgument)?; + let fd = result.map_err(map_vfs_error)?; if path == "/dev/tty" && process.session_id() == pid && !process.has_controlling_tty() { process.set_controlling_tty(ControllingTty::Global); } @@ -366,6 +439,125 @@ fn handle_ioctl(invocation: &SyscallInvocation) -> SysResult { }) } +fn handle_socket(invocation: &SyscallInvocation) -> SysResult { + let pid = current_pid()?; + let domain = invocation.arg(0).ok_or(SysError::InvalidArgument)? as u32; + let socket_type = invocation.arg(1).ok_or(SysError::InvalidArgument)? as u32; + let protocol = invocation.arg(2).ok_or(SysError::InvalidArgument)? as u32; + + if domain != AF_INET as u32 { + return Err(SysError::InvalidArgument); + } + let base_type = socket_type & 0xff; + let type_flags = socket_type & !0xff; + let allowed = SOCK_NONBLOCK | SOCK_CLOEXEC; + if type_flags & !allowed != 0 { + return Err(SysError::InvalidArgument); + } + if base_type != SOCK_STREAM { + return Err(SysError::InvalidArgument); + } + if protocol != 0 { + return Err(SysError::InvalidArgument); + } + + let file = TcpSocketFile::new(); + let fd = proc_fs::open_file_with_flags(pid, file, socket_type & SOCK_CLOEXEC != 0) + .map_err(|_| SysError::InvalidArgument)?; + Ok(fd as u64) +} + +fn handle_bind(invocation: &SyscallInvocation) -> SysResult { + let pid = current_pid()?; + let fd = invocation.arg(0).ok_or(SysError::InvalidArgument)? as u32; + let addr_ptr = invocation.arg(1).ok_or(SysError::InvalidArgument)?; + let addr_len = invocation.arg(2).ok_or(SysError::InvalidArgument)?; + let addr_len = usize::try_from(addr_len).map_err(|_| SysError::InvalidArgument)?; + + let addr = read_sockaddr_in(addr_ptr, addr_len)?; + let file = proc_fs::fd_file(pid, fd).map_err(|_| SysError::InvalidArgument)?; + let socket = file + .as_ref() + .as_any() + .downcast_ref::() + .ok_or(SysError::InvalidArgument)?; + socket.bind(addr).map_err(|_| SysError::InvalidArgument)?; + Ok(0) +} + +fn handle_listen(invocation: &SyscallInvocation) -> SysResult { + let pid = current_pid()?; + let fd = invocation.arg(0).ok_or(SysError::InvalidArgument)? as u32; + let _backlog = invocation.arg(1).ok_or(SysError::InvalidArgument)?; + + let file = proc_fs::fd_file(pid, fd).map_err(|_| SysError::InvalidArgument)?; + let socket = file + .as_ref() + .as_any() + .downcast_ref::() + .ok_or(SysError::InvalidArgument)?; + socket.listen().map_err(|_| SysError::InvalidArgument)?; + Ok(0) +} + +fn handle_accept(invocation: &SyscallInvocation) -> SysResult { + let pid = current_pid()?; + let fd = invocation.arg(0).ok_or(SysError::InvalidArgument)? as u32; + let addr_ptr = invocation.arg(1).ok_or(SysError::InvalidArgument)?; + let addrlen_ptr = invocation.arg(2).ok_or(SysError::InvalidArgument)?; + handle_accept_raw(pid, fd, addr_ptr, addrlen_ptr) +} + +fn handle_accept4(invocation: &SyscallInvocation) -> SysResult { + let pid = current_pid()?; + let fd = invocation.arg(0).ok_or(SysError::InvalidArgument)? as u32; + let addr_ptr = invocation.arg(1).ok_or(SysError::InvalidArgument)?; + let addrlen_ptr = invocation.arg(2).ok_or(SysError::InvalidArgument)?; + let flags = invocation.arg(3).ok_or(SysError::InvalidArgument)? as u32; + + let allowed = SOCK_NONBLOCK | SOCK_CLOEXEC; + if flags & !allowed != 0 { + return Err(SysError::InvalidArgument); + } + + let new_fd = handle_accept_raw(pid, fd, addr_ptr, addrlen_ptr)?; + if (flags & SOCK_CLOEXEC) != 0 { + proc_fs::set_fd_flags(pid, new_fd as u32, 1).map_err(|_| SysError::InvalidArgument)?; + } + Ok(new_fd as u64) +} + +fn handle_accept_raw( + pid: ProcessId, + fd: u32, + addr_ptr: u64, + addrlen_ptr: u64, +) -> Result { + let file = proc_fs::fd_file(pid, fd).map_err(|_| SysError::InvalidArgument)?; + let socket = file + .as_ref() + .as_any() + .downcast_ref::() + .ok_or(SysError::InvalidArgument)?; + let (stream, remote) = socket.accept().map_err(|_| SysError::InvalidArgument)?; + let new_socket = TcpSocketFile::new(); + new_socket + .set_stream(stream) + .map_err(|_| SysError::InvalidArgument)?; + let new_fd = proc_fs::open_file(pid, new_socket).map_err(|_| SysError::InvalidArgument)?; + + if addr_ptr != 0 && addrlen_ptr != 0 { + let len = read_u32(addrlen_ptr)? as usize; + if len < SOCKADDR_IN_LEN { + return Err(SysError::InvalidArgument); + } + write_sockaddr_in(remote, addr_ptr)?; + write_u32(addrlen_ptr, SOCKADDR_IN_LEN as u32)?; + } + + Ok(new_fd as u64) +} + fn handle_getpid(_invocation: &SyscallInvocation) -> SysResult { let pid = SCHEDULER .current_process_id() @@ -519,6 +711,44 @@ fn handle_lstat(invocation: &SyscallInvocation) -> SysResult { Ok(0) } +fn handle_newfstatat(invocation: &SyscallInvocation) -> SysResult { + let dirfd = invocation.arg(0).ok_or(SysError::InvalidArgument)? as i32; + let path_ptr = invocation.arg(1).ok_or(SysError::InvalidArgument)?; + let stat_ptr = invocation.arg(2).ok_or(SysError::InvalidArgument)?; + let flags = invocation.arg(3).unwrap_or(0) as u32; + + let allowed = AT_SYMLINK_NOFOLLOW; + if flags & !allowed != 0 { + return Err(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 = if (flags & AT_SYMLINK_NOFOLLOW) != 0 { + proc_fs::stat_path_no_follow_at(pid, dirfd, &path) + } else { + proc_fs::stat_path_at(pid, dirfd, &path) + } + .map_err(map_vfs_error)?; + + 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)?; @@ -558,6 +788,62 @@ fn handle_getcwd(invocation: &SyscallInvocation) -> SysResult { Ok((bytes.len() + 1) as u64) } +fn handle_sendto(invocation: &SyscallInvocation) -> SysResult { + const MAX_WRITE: usize = 4096; + + let pid = current_pid()?; + let fd = invocation.arg(0).ok_or(SysError::InvalidArgument)?; + let ptr = invocation.arg(1).ok_or(SysError::InvalidArgument)?; + let len = invocation.arg(2).ok_or(SysError::InvalidArgument)?; + let flags = invocation.arg(3).unwrap_or(0) as u32; + let dest = invocation.arg(4).unwrap_or(0); + let addrlen = invocation.arg(5).unwrap_or(0); + + if flags & !MSG_NOSIGNAL != 0 { + return Err(SysError::InvalidArgument); + } + if dest != 0 || addrlen != 0 { + return Err(SysError::InvalidArgument); + } + + let len = usize::try_from(len).map_err(|_| SysError::InvalidArgument)?; + if len == 0 { + return Ok(0); + } + let read_len = len.min(MAX_WRITE); + let mut buf = [0u8; MAX_WRITE]; + let user_ptr = VirtAddr::new(ptr as usize); + 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)?; + Ok(written as u64) +} + +fn handle_setsockopt(invocation: &SyscallInvocation) -> SysResult { + let _pid = current_pid()?; + let _fd = invocation.arg(0).ok_or(SysError::InvalidArgument)? as u32; + let level = invocation.arg(1).ok_or(SysError::InvalidArgument)? as u32; + let optname = invocation.arg(2).ok_or(SysError::InvalidArgument)? as u32; + let optval = invocation.arg(3).unwrap_or(0); + let optlen = invocation.arg(4).unwrap_or(0) as u32; + + let supported = match level { + SOL_SOCKET => optname == SO_REUSEADDR || optname == SO_REUSEPORT, + IPPROTO_TCP => optname == TCP_NODELAY, + _ => false, + }; + if !supported { + return Err(SysError::InvalidArgument); + } + + if optval != 0 { + if optlen < 4 { + return Err(SysError::InvalidArgument); + } + let _ = read_u32(optval)?; + } + Ok(0) +} /// Encode directory entries for `getdents64` using the per-fd cursor in `FdTable`. /// /// Implicit dependencies: @@ -1339,13 +1625,25 @@ fn rollback_mapped( } } +fn map_vfs_error(err: crate::fs::VfsError) -> SysError { + match err { + crate::fs::VfsError::NotFound => SysError::NotFound, + crate::fs::VfsError::BadFd => SysError::BadFileDescriptor, + crate::fs::VfsError::NotDirectory => SysError::NotDirectory, + crate::fs::VfsError::InvalidPath => SysError::InvalidArgument, + _ => SysError::InvalidArgument, + } +} + fn errno_for(err: SysError) -> u16 { match err { SysError::NotImplemented => LinuxErrno::NoSys as u16, SysError::InvalidArgument => LinuxErrno::InvalidArgument as u16, SysError::NotFound => LinuxErrno::NoEntry as u16, + SysError::BadFileDescriptor => LinuxErrno::BadFileDescriptor as u16, SysError::BadAddress => LinuxErrno::BadAddress as u16, SysError::NotTty => LinuxErrno::NotTty as u16, + SysError::NotDirectory => LinuxErrno::NotDirectory as u16, SysError::IllegalSeek => LinuxErrno::IllegalSeek as u16, } } @@ -1393,6 +1691,50 @@ fn read_u64_with_user( user.read_u64(addr).map_err(|_| SysError::BadAddress) } +fn read_u32(ptr: u64) -> Result { + let mut buf = [0u8; 4]; + let addr = VirtAddr::new(ptr as usize); + copy_from_user(&mut buf, addr).map_err(|_| SysError::BadAddress)?; + Ok(u32::from_ne_bytes(buf)) +} + +fn write_u32(ptr: u64, value: u32) -> Result<(), SysError> { + let addr = VirtAddr::new(ptr as usize); + copy_to_user(addr, &value.to_ne_bytes()).map_err(|_| SysError::BadAddress) +} + +fn read_sockaddr_in(ptr: u64, len: usize) -> Result { + if len < SOCKADDR_IN_LEN { + return Err(SysError::InvalidArgument); + } + let mut buf = [0u8; SOCKADDR_IN_LEN]; + let addr = VirtAddr::new(ptr as usize); + copy_from_user(&mut buf, addr).map_err(|_| SysError::BadAddress)?; + + let family = u16::from_ne_bytes([buf[0], buf[1]]); + if family != AF_INET { + return Err(SysError::InvalidArgument); + } + let port = u16::from_be_bytes([buf[2], buf[3]]); + let ip = Ipv4Addr::new(buf[4], buf[5], buf[6], buf[7]); + Ok(SocketAddr::new(IpAddr::V4(ip), port)) +} + +fn write_sockaddr_in(addr: SocketAddr, ptr: u64) -> Result<(), SysError> { + let ip = match addr.ip() { + IpAddr::V4(ip) => ip, + IpAddr::V6(_) => return Err(SysError::InvalidArgument), + }; + let port = addr.port(); + let mut buf = [0u8; SOCKADDR_IN_LEN]; + buf[0..2].copy_from_slice(&AF_INET.to_ne_bytes()); + buf[2..4].copy_from_slice(&port.to_be_bytes()); + let octets = ip.octets(); + buf[4..8].copy_from_slice(&octets); + let dst = VirtAddr::new(ptr as usize); + copy_to_user(dst, &buf).map_err(|_| SysError::BadAddress) +} + fn read_cstring_array_with_user( user: &UserMemoryAccess<'_, T>, ptr: u64, diff --git a/kernel/src/syscall/mod.rs b/kernel/src/syscall/mod.rs index 98bd4f0..5c6245d 100644 --- a/kernel/src/syscall/mod.rs +++ b/kernel/src/syscall/mod.rs @@ -26,8 +26,10 @@ pub enum SysError { NotImplemented, InvalidArgument, NotFound, + BadFileDescriptor, BadAddress, NotTty, + NotDirectory, IllegalSeek, } diff --git a/xtask-assets/fixtures/linux-syscall-adv/DESIGN.md b/xtask-assets/fixtures/linux-syscall-adv/DESIGN.md index cdc5273..9a1bf78 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`, `lstat`, `getdents64`, `ioctl` (TIOCGWINSZ), `mmap`, `munmap`, `brk`, - `arch_prctl`, `fork`, `execve`, and `wait4` in one deterministic run. +- Cover `writev`, `stat`, `lstat`, `openat`, `newfstatat`, `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 71b404a..8671487 100644 --- a/xtask-assets/fixtures/linux-syscall-adv/libsyscall.c +++ b/xtask-assets/fixtures/linux-syscall-adv/libsyscall.c @@ -76,6 +76,10 @@ isize sys_open(const char *path, int flags, int mode) { return sys_call3(SYS_open, (isize)path, flags, mode); } +isize sys_openat(int dirfd, const char *path, int flags, int mode) { + return sys_call4(SYS_openat, dirfd, (isize)path, flags, mode); +} + isize sys_close(int fd) { return sys_call1(SYS_close, fd); } @@ -88,6 +92,10 @@ isize sys_lstat(const char *path, struct linux_stat *statbuf) { return sys_call3(SYS_lstat, (isize)path, (isize)statbuf, 0); } +isize sys_newfstatat(int dirfd, const char *path, struct linux_stat *statbuf, int flags) { + return sys_call4(SYS_newfstatat, dirfd, (isize)path, (isize)statbuf, flags); +} + 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 3f44c21..c796555 100644 --- a/xtask-assets/fixtures/linux-syscall-adv/libsyscall.h +++ b/xtask-assets/fixtures/linux-syscall-adv/libsyscall.h @@ -15,6 +15,8 @@ enum { SYS_close = 3, SYS_stat = 4, SYS_lstat = 6, + SYS_openat = 257, + SYS_newfstatat = 262, SYS_mmap = 9, SYS_munmap = 11, SYS_brk = 12, @@ -32,6 +34,11 @@ enum { ARCH_SET_FS = 0x1002, }; +enum { + AT_FDCWD = -100, + AT_SYMLINK_NOFOLLOW = 0x100, +}; + struct iovec { void *iov_base; usize iov_len; @@ -68,9 +75,11 @@ 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_openat(int dirfd, 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_newfstatat(int dirfd, const char *path, struct linux_stat *statbuf, int flags); 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 bbdcae2..ebbf165 100644 --- a/xtask-assets/fixtures/linux-syscall-adv/main.c +++ b/xtask-assets/fixtures/linux-syscall-adv/main.c @@ -100,7 +100,29 @@ void _start(void) { write_str("LSTAT:BAD\n"); } + int at_fd = (int)sys_openat(AT_FDCWD, stat_path, 0, 0); + if (at_fd >= 0) { + sys_close(at_fd); + write_str("OPENAT:OK\n"); + } else { + write_str("OPENAT:BAD\n"); + } + int dir_fd = (int)sys_open("/", 0, 0); + int fstat_ok = 0; + if (dir_fd >= 0 && + sys_newfstatat(dir_fd, stat_path, &st, 0) == 0 && + st.st_size == 8 && + sys_newfstatat(dir_fd, lstat_path, &st, AT_SYMLINK_NOFOLLOW) == 0 && + (st.st_mode & 0170000) == 0120000) { + fstat_ok = 1; + } + if (fstat_ok) { + write_str("FSTATAT:OK\n"); + } else { + write_str("FSTATAT:BAD\n"); + } + if (dir_fd < 0) { write_str("DENTS:BAD\n"); } else { diff --git a/xtask-assets/fixtures/linux-syscall-net/DESIGN.md b/xtask-assets/fixtures/linux-syscall-net/DESIGN.md new file mode 100644 index 0000000..ac0cf31 --- /dev/null +++ b/xtask-assets/fixtures/linux-syscall-net/DESIGN.md @@ -0,0 +1,12 @@ +# linux-syscall-net Fixture + +## Role +- Validates the minimal TCP socket syscalls (`socket`, `setsockopt`, `bind`, `listen`, `accept4`, + `sendto`) from a libc-free Linux ELF. +- Reads one payload from the accepted connection, replies with `PONG` via `sendto(MSG_NOSIGNAL)`, + then writes `NET:OK` to stdout for the kernel test to assert. + +## Notes +- Uses a fixed TCP port (12346) and binds `0.0.0.0` so the kernel-side test client can connect. +- Enables `SO_REUSEADDR` on the listening socket before binding. +- Built via `xtask-assets` into `target/xtask-assets/linux-syscall-net.elf`. diff --git a/xtask-assets/fixtures/linux-syscall-net/libsyscall.c b/xtask-assets/fixtures/linux-syscall-net/libsyscall.c new file mode 100644 index 0000000..1fa9286 --- /dev/null +++ b/xtask-assets/fixtures/linux-syscall-net/libsyscall.c @@ -0,0 +1,80 @@ +#include "libsyscall.h" + +isize sys_call1(isize num, isize arg1) { + isize ret; + __asm__ volatile( + "syscall" + : "=a"(ret) + : "a"(num), "D"(arg1) + : "rcx", "r11", "memory"); + return ret; +} + +isize sys_call3(isize num, isize arg1, isize arg2, isize arg3) { + isize ret; + __asm__ volatile( + "syscall" + : "=a"(ret) + : "a"(num), "D"(arg1), "S"(arg2), "d"(arg3) + : "rcx", "r11", "memory"); + return ret; +} + +isize sys_call6(isize num, isize arg1, isize arg2, isize arg3, isize arg4, isize arg5, isize arg6) { + isize ret; + register isize r10 __asm__("r10") = arg4; + register isize r8 __asm__("r8") = arg5; + register isize r9 __asm__("r9") = arg6; + __asm__ volatile( + "syscall" + : "=a"(ret) + : "a"(num), "D"(arg1), "S"(arg2), "d"(arg3), "r"(r10), "r"(r8), "r"(r9) + : "rcx", "r11", "memory"); + return ret; +} + +isize sys_read(int fd, void *buf, usize len) { + return sys_call3(SYS_read, fd, (isize)buf, (isize)len); +} + +isize sys_write(int fd, const void *buf, usize len) { + return sys_call3(SYS_write, fd, (isize)buf, (isize)len); +} + +isize sys_close(int fd) { + return sys_call1(SYS_close, fd); +} + +isize sys_socket(int domain, int type, int protocol) { + return sys_call3(SYS_socket, domain, type, protocol); +} + +isize sys_setsockopt(int fd, int level, int optname, const void *optval, usize optlen) { + return sys_call6(SYS_setsockopt, fd, level, optname, (isize)optval, (isize)optlen, 0); +} + +isize sys_bind(int fd, const struct sockaddr_in *addr, usize len) { + return sys_call3(SYS_bind, fd, (isize)addr, (isize)len); +} + +isize sys_listen(int fd, int backlog) { + return sys_call3(SYS_listen, fd, backlog, 0); +} + +isize sys_accept(int fd, struct sockaddr_in *addr, u32 *addrlen) { + return sys_call3(SYS_accept, fd, (isize)addr, (isize)addrlen); +} + +isize sys_accept4(int fd, struct sockaddr_in *addr, u32 *addrlen, int flags) { + return sys_call6(SYS_accept4, fd, (isize)addr, (isize)addrlen, flags, 0, 0); +} + +isize sys_sendto(int fd, const void *buf, usize len, int flags, const void *addr, usize addrlen) { + return sys_call6(SYS_sendto, fd, (isize)buf, (isize)len, flags, (isize)addr, (isize)addrlen); +} + +__attribute__((noreturn)) void sys_exit(int code) { + sys_call1(SYS_exit, code); + for (;;) { + } +} diff --git a/xtask-assets/fixtures/linux-syscall-net/libsyscall.h b/xtask-assets/fixtures/linux-syscall-net/libsyscall.h new file mode 100644 index 0000000..02911f3 --- /dev/null +++ b/xtask-assets/fixtures/linux-syscall-net/libsyscall.h @@ -0,0 +1,58 @@ +#ifndef CYRIUS_LIBSYSCALL_NET_H +#define CYRIUS_LIBSYSCALL_NET_H + +typedef unsigned long usize; +typedef long isize; +typedef unsigned short u16; +typedef unsigned int u32; +typedef unsigned char u8; + +enum { + SYS_read = 0, + SYS_write = 1, + SYS_close = 3, + SYS_socket = 41, + SYS_accept = 43, + SYS_sendto = 44, + SYS_bind = 49, + SYS_listen = 50, + SYS_setsockopt = 54, + SYS_accept4 = 288, + SYS_exit = 60, +}; + +enum { + AF_INET = 2, + SOCK_STREAM = 1, +}; + +enum { + SOL_SOCKET = 1, + SO_REUSEADDR = 2, + SOCK_CLOEXEC = 0x80000, + MSG_NOSIGNAL = 0x4000, +}; + +struct sockaddr_in { + u16 sin_family; + u16 sin_port; + u32 sin_addr; + u8 sin_zero[8]; +}; + +isize sys_call1(isize num, isize arg1); +isize sys_call3(isize num, isize arg1, isize arg2, isize arg3); + +isize sys_read(int fd, void *buf, usize len); +isize sys_write(int fd, const void *buf, usize len); +isize sys_close(int fd); +isize sys_socket(int domain, int type, int protocol); +isize sys_setsockopt(int fd, int level, int optname, const void *optval, usize optlen); +isize sys_bind(int fd, const struct sockaddr_in *addr, usize len); +isize sys_listen(int fd, int backlog); +isize sys_accept(int fd, struct sockaddr_in *addr, u32 *addrlen); +isize sys_accept4(int fd, struct sockaddr_in *addr, u32 *addrlen, int flags); +isize sys_sendto(int fd, const void *buf, usize len, int flags, const void *addr, usize addrlen); +__attribute__((noreturn)) void sys_exit(int code); + +#endif diff --git a/xtask-assets/fixtures/linux-syscall-net/main.c b/xtask-assets/fixtures/linux-syscall-net/main.c new file mode 100644 index 0000000..9ae3f57 --- /dev/null +++ b/xtask-assets/fixtures/linux-syscall-net/main.c @@ -0,0 +1,60 @@ +#include "libsyscall.h" + +static u16 to_be16(u16 value) { + return (u16)((value << 8) | (value >> 8)); +} + +static u32 to_be32(u32 value) { + return ((value & 0x000000ffU) << 24) | + ((value & 0x0000ff00U) << 8) | + ((value & 0x00ff0000U) >> 8) | + ((value & 0xff000000U) >> 24); +} + +static const char ok_msg[] = "NET:OK\n"; +static const char pong_msg[] = "PONG"; + +void _start(void) { + struct sockaddr_in addr; + addr.sin_family = AF_INET; + addr.sin_port = to_be16(12346); + addr.sin_addr = to_be32(0); + for (int i = 0; i < 8; ++i) { + addr.sin_zero[i] = 0; + } + + int fd = (int)sys_socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) { + sys_exit(1); + } + + int reuse = 1; + if (sys_setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) { + sys_exit(6); + } + + if (sys_bind(fd, &addr, sizeof(addr)) < 0) { + sys_exit(2); + } + + if (sys_listen(fd, 1) < 0) { + sys_exit(3); + } + + u32 addrlen = sizeof(addr); + int client = (int)sys_accept4(fd, &addr, &addrlen, SOCK_CLOEXEC); + if (client < 0) { + sys_exit(4); + } + + char buf[16]; + isize n = sys_read(client, buf, sizeof(buf)); + if (n > 0) { + sys_sendto(client, pong_msg, sizeof(pong_msg) - 1, MSG_NOSIGNAL, 0, 0); + } + + sys_close(client); + sys_close(fd); + sys_write(1, ok_msg, sizeof(ok_msg) - 1); + sys_exit(0); +} diff --git a/xtask-assets/src/lib.rs b/xtask-assets/src/lib.rs index ecc7711..daabd91 100644 --- a/xtask-assets/src/lib.rs +++ b/xtask-assets/src/lib.rs @@ -108,6 +108,30 @@ pub fn ensure_linux_syscall_child_elf(out_dir: &Path) -> io::Result { Ok(out_path) } +pub fn ensure_linux_syscall_net_elf(out_dir: &Path) -> io::Result { + fs::create_dir_all(out_dir)?; + let out_path = out_dir.join("linux-syscall-net.elf"); + + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let src_dir = manifest_dir.join("fixtures").join("linux-syscall-net"); + let src = src_dir.join("main.c"); + let lib = src_dir.join("libsyscall.c"); + if !src.exists() { + return Err(io::Error::new( + io::ErrorKind::NotFound, + "linux-syscall-net fixture missing", + )); + } + + if !needs_rebuild_multi(&[&src, &lib], &out_path)? { + return Ok(out_path); + } + + build_linux_syscall_elf(&[&src, &lib], &out_path)?; + + 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"); diff --git a/xtask/src/lib.rs b/xtask/src/lib.rs index 5f1aa27..ef213d1 100644 --- a/xtask/src/lib.rs +++ b/xtask/src/lib.rs @@ -236,8 +236,9 @@ pub fn run_qemu(image: &Path, test: bool, block_images: &[PathBuf]) -> Result guest 0.0.0.0:8080. // Host access: tcp://127.0.0.1:12345 -> guest 0.0.0.0:12345. - "user,id=net0,hostfwd=tcp::12345-:12345" + "user,id=net0,hostfwd=tcp::8080-:8080,hostfwd=tcp::12345-:12345" }; qemu.args([ "-netdev", @@ -709,6 +710,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_syscall_net_elf(&assets_dir) + .with_context(|| format!("ensure linux syscall net 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)