diff --git a/crates/kernel/src/process_table.rs b/crates/kernel/src/process_table.rs index 6632161df..cbab8907f 100644 --- a/crates/kernel/src/process_table.rs +++ b/crates/kernel/src/process_table.rs @@ -846,11 +846,25 @@ impl ProcessTable { self.processes.get(&pid) } - /// Collect all active PIDs. + /// Collect every retained PID, including internal limbo identities. pub fn all_pids(&self) -> Vec { self.processes.keys().copied().collect() } + /// Collect PIDs that should be visible through procfs. + /// + /// Exited processes remain visible as zombies until their parent reaps + /// them. Limbo entries are already reaped and retained only as internal + /// process-group/session identities, so exposing them as `/proc/` + /// would resurrect a process that no longer exists. + pub fn procfs_pids(&self) -> Vec { + self.processes + .iter() + .filter(|(_, proc)| proc.state != ProcessState::Limbo) + .map(|(&pid, _)| pid) + .collect() + } + /// Collect PIDs of all processes in a given process group. pub fn pids_in_group(&self, pgid: u32) -> Vec { self.processes @@ -982,12 +996,22 @@ mod wait_tests { table.processes.get_mut(&102).unwrap().pgid = 101; table.processes.get_mut(&101).unwrap().state = ProcessState::Exited; + assert!( + table.procfs_pids().contains(&101), + "an unreaped zombie remains visible through procfs" + ); + table.reap_process(101).expect("reap group leader"); let limbo = table.get(101).expect("limbo leader retained"); assert_eq!(limbo.state, ProcessState::Limbo); assert_eq!(limbo.pgid, 101); assert_eq!(limbo.ppid, 100); + assert!(table.all_pids().contains(&101)); + assert!( + !table.procfs_pids().contains(&101), + "a reaped limbo identity must not remain visible through procfs" + ); table.processes.get_mut(&102).unwrap().state = ProcessState::Exited; table.reap_process(102).expect("reap final member"); diff --git a/crates/kernel/src/procfs.rs b/crates/kernel/src/procfs.rs index 84844e3d6..14da1f697 100644 --- a/crates/kernel/src/procfs.rs +++ b/crates/kernel/src/procfs.rs @@ -63,9 +63,9 @@ pub enum ProcfsEntry { Cwd(u32), // /proc//cwd (symlink) Exe(u32), // /proc//exe (symlink) Root_(u32), // /proc//root (symlink) - NetDir, // /proc/net - NetTcp, // /proc/net/tcp - NetUnix, // /proc/net/unix + NetDir(Option), // /proc/net or /proc//net + NetTcp(Option), // /proc/net/tcp or /proc//net/tcp + NetUnix(Option), // /proc/net/unix or /proc//net/unix } impl ProcfsEntry { @@ -90,7 +90,7 @@ impl ProcfsEntry { | ProcfsEntry::PidDir(_) | ProcfsEntry::FdDir(_) | ProcfsEntry::FdInfoDir(_) - | ProcfsEntry::NetDir + | ProcfsEntry::NetDir(_) ) } } @@ -106,9 +106,32 @@ pub const MOUNTS_CONTENT: &[u8] = const MOUNTINFO_CONTENT: &[u8] = b"1 0 0:1 / / rw - kandelo-vfs kandelo-root rw\n2 1 0:2 / /proc rw,nosuid,nodev,noexec - proc proc rw,nosuid,nodev,noexec\n3 1 0:3 / /dev rw,nosuid - devfs devfs rw,nosuid\n"; -/// Extract the pid from a ProcfsEntry (0 for root/net entries). -pub fn entry_pid(entry: &ProcfsEntry) -> u32 { - entry_ids(entry).0 +/// Return the process scope of an entry, if it is under `/proc/`. +/// +/// `Option` is intentional: PID 0 is a process-scoped path that must be +/// validated and rejected, not a sentinel for a global procfs entry. +pub fn entry_pid(entry: &ProcfsEntry) -> Option { + match entry { + ProcfsEntry::PidDir(pid) + | ProcfsEntry::PidMounts(pid) + | ProcfsEntry::PidMountinfo(pid) + | ProcfsEntry::FdDir(pid) + | ProcfsEntry::FdInfoDir(pid) + | ProcfsEntry::Stat(pid) + | ProcfsEntry::Status(pid) + | ProcfsEntry::Cmdline(pid) + | ProcfsEntry::Environ(pid) + | ProcfsEntry::Maps(pid) + | ProcfsEntry::Cwd(pid) + | ProcfsEntry::Exe(pid) + | ProcfsEntry::Root_(pid) => Some(*pid), + ProcfsEntry::FdLink(pid, _) | ProcfsEntry::FdInfo(pid, _) => Some(*pid), + ProcfsEntry::NetDir(pid) | ProcfsEntry::NetTcp(pid) | ProcfsEntry::NetUnix(pid) => *pid, + ProcfsEntry::Root + | ProcfsEntry::Mounts + | ProcfsEntry::SelfLink + | ProcfsEntry::ThreadSelfLink => None, + } } // ── Path matching ─────────────────────────────────────────────────────────── @@ -181,7 +204,7 @@ pub fn match_procfs(path: &[u8], current_pid: u32) -> Option { (current_pid, &after[1..]) } } else if rest == b"net" || rest.starts_with(b"net/") { - return match_net_path(rest); + return match_net_path(rest, None); } else { match_pid_path(rest) }; @@ -245,7 +268,7 @@ fn match_pid_subpath(pid: u32, remainder: &[u8]) -> Option { let fd_str = &rem[7..]; parse_i32(fd_str).map(|fd| ProcfsEntry::FdInfo(pid, fd)) } else if rem == b"net" || rem.starts_with(b"net/") { - match_net_path(rem) + match_net_path(rem, Some(pid)) } else { None } @@ -254,14 +277,14 @@ fn match_pid_subpath(pid: u32, remainder: &[u8]) -> Option { } /// Match /proc/net/* paths. -fn match_net_path(rest: &[u8]) -> Option { +fn match_net_path(rest: &[u8], pid: Option) -> Option { if rest == b"net" || rest == b"net/" { - return Some(ProcfsEntry::NetDir); + return Some(ProcfsEntry::NetDir(pid)); } if rest.starts_with(b"net/") { match &rest[4..] { - b"tcp" => return Some(ProcfsEntry::NetTcp), - b"unix" => return Some(ProcfsEntry::NetUnix), + b"tcp" => return Some(ProcfsEntry::NetTcp(pid)), + b"unix" => return Some(ProcfsEntry::NetUnix(pid)), _ => return None, } } @@ -285,9 +308,16 @@ pub fn generate_stat(proc: &Process) -> Vec { // pid (comm) state ppid pgrp session tty_nr tpgid flags // minflt cminflt majflt cmajflt utime stime cutime cstime // priority nice num_threads itrealvalue starttime vsize rss ... + // + // Keep both scheduling fields distinct. Several user-space tools (ps, + // procps-compatible libraries, PHP's proc_nice() tests) read the nice + // value from field 19; field 18 is the scheduler priority. Kandelo does + // not have a host CPU scheduler, but exposing the stored POSIX nice value + // in the Linux-compatible procfs slot is observable process metadata. + let priority = 20 + proc.nice; let line = format!( - "{} ({}) {} {} {} {} 0 0 0 0 0 0 0 0 0 0 {} 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n", - proc.pid, name, state, proc.ppid, proc.pgid, proc.sid, proc.nice, + "{} ({}) {} {} {} {} 0 0 0 0 0 0 0 0 0 0 0 {} {} 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n", + proc.pid, name, state, proc.ppid, proc.pgid, proc.sid, priority, proc.nice, ); line.into_bytes() } @@ -504,9 +534,9 @@ fn entry_ids(entry: &ProcfsEntry) -> (u32, u8) { ProcfsEntry::Cwd(pid) => (*pid, 16), ProcfsEntry::Exe(pid) => (*pid, 17), ProcfsEntry::Root_(pid) => (*pid, 18), - ProcfsEntry::NetDir => (0, 19), - ProcfsEntry::NetTcp => (0, 20), - ProcfsEntry::NetUnix => (0, 21), + ProcfsEntry::NetDir(pid) => (pid.unwrap_or(0), 19), + ProcfsEntry::NetTcp(pid) => (pid.unwrap_or(0), 20), + ProcfsEntry::NetUnix(pid) => (pid.unwrap_or(0), 21), } } @@ -558,8 +588,7 @@ pub fn procfs_open( if entry.is_dir() { // Validate that the target pid exists for pid-scoped directories - let target_pid = entry_pid(entry); - if target_pid != 0 { + if let Some(target_pid) = entry_pid(entry) { validate_pid(proc, target_pid)?; } let ofd_idx = proc.ofd_table.create( @@ -630,10 +659,16 @@ fn generate_content(proc: &Process, entry: &ProcfsEntry) -> Result, Errn validate_pid(proc, *pid)?; Ok(MOUNTINFO_CONTENT.to_vec()) } - ProcfsEntry::NetTcp => { + ProcfsEntry::NetTcp(pid) => { + if let Some(pid) = pid { + validate_pid(proc, *pid)?; + } Ok(b" sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode\n".to_vec()) } - ProcfsEntry::NetUnix => { + ProcfsEntry::NetUnix(pid) => { + if let Some(pid) = pid { + validate_pid(proc, *pid)?; + } Ok(b"Num RefCount Protocol Flags Type St Inode Path\n".to_vec()) } _ => Err(Errno::ENOENT), @@ -657,6 +692,20 @@ fn validate_pid(proc: &Process, pid: u32) -> Result<(), Errno> { Err(Errno::ENOENT) } +/// Validate that a parsed procfs entry names an existing process. +/// +/// Path parsing alone is not existence: Linux procfs only exposes +/// `/proc//...` while that pid has a process-table entry. Callers that +/// service metadata-only operations (stat/access/chdir) must perform the same +/// validation as `procfs_open`, otherwise probes such as +/// `test -r /proc/123/stat` incorrectly succeed for already-reaped pids. +pub fn validate_entry(proc: &Process, entry: &ProcfsEntry) -> Result<(), Errno> { + if let Some(pid) = entry_pid(entry) { + validate_pid(proc, pid)?; + } + Ok(()) +} + /// Allocate a procfs buffer slot, reusing freed slots. fn alloc_procfs_buf(proc: &mut Process, data: Vec) -> usize { for (i, slot) in proc.procfs_bufs.iter().enumerate() { @@ -883,9 +932,10 @@ fn dir_entries( } } } - ProcfsEntry::NetDir => { - entries.push((b"tcp".to_vec(), DT_REG, procfs_ino(0, 20))); - entries.push((b"unix".to_vec(), DT_REG, procfs_ino(0, 21))); + ProcfsEntry::NetDir(pid) => { + let ino_pid = pid.unwrap_or(0); + entries.push((b"tcp".to_vec(), DT_REG, procfs_ino(ino_pid, 20))); + entries.push((b"unix".to_vec(), DT_REG, procfs_ino(ino_pid, 21))); } _ => return Err(Errno::ENOTDIR), } @@ -1019,12 +1069,33 @@ mod tests { #[test] fn test_match_procfs_net() { assert_eq!(match_procfs(b"/proc/mounts", 1), Some(ProcfsEntry::Mounts)); - assert_eq!(match_procfs(b"/proc/net", 1), Some(ProcfsEntry::NetDir)); - assert_eq!(match_procfs(b"/proc/net/tcp", 1), Some(ProcfsEntry::NetTcp)); + assert_eq!( + match_procfs(b"/proc/net", 1), + Some(ProcfsEntry::NetDir(None)) + ); + assert_eq!( + match_procfs(b"/proc/net/tcp", 1), + Some(ProcfsEntry::NetTcp(None)) + ); assert_eq!( match_procfs(b"/proc/net/unix", 1), - Some(ProcfsEntry::NetUnix) + Some(ProcfsEntry::NetUnix(None)) + ); + assert_eq!( + match_procfs(b"/proc/42/net", 1), + Some(ProcfsEntry::NetDir(Some(42))) ); + assert_eq!( + match_procfs(b"/proc/42/net/tcp", 1), + Some(ProcfsEntry::NetTcp(Some(42))) + ); + } + + #[test] + fn test_entry_pid_distinguishes_global_entries_from_pid_zero() { + assert_eq!(entry_pid(&ProcfsEntry::NetDir(None)), None); + assert_eq!(entry_pid(&ProcfsEntry::PidDir(0)), Some(0)); + assert_eq!(entry_pid(&ProcfsEntry::NetDir(Some(0))), Some(0)); } #[test] @@ -1046,7 +1117,10 @@ mod tests { let stat = generate_stat(&proc); let stat_str = core::str::from_utf8(&stat).unwrap(); assert!(stat_str.starts_with("42 (test_program) R 1 42 1")); - assert!(stat_str.contains(" 5 ")); // nice value + let after_comm = stat_str.split(") ").nth(1).unwrap(); + let mut fields = after_comm.split_whitespace(); + assert_eq!(fields.nth(15).unwrap(), "25"); // field 18: scheduler priority + assert_eq!(fields.next().unwrap(), "5"); // field 19: nice value } #[test] diff --git a/crates/kernel/src/syscalls.rs b/crates/kernel/src/syscalls.rs index 175960c9a..7e775344d 100644 --- a/crates/kernel/src/syscalls.rs +++ b/crates/kernel/src/syscalls.rs @@ -2680,9 +2680,19 @@ pub fn sys_write( return Err(Errno::EAGAIN); } } - // External path: delegate to host + // External path: delegate to host. A stream write that + // fails with EPIPE must also generate SIGPIPE; the host + // only reports the errno, so mirror the POSIX side effect + // in the kernel just like the in-kernel pipe-backed TCP + // path above. let net_handle = sock.host_net_handle.ok_or(Errno::ENOTCONN)?; - host.host_net_send(net_handle, buf, 0) + match host.host_net_send(net_handle, buf, 0) { + Err(Errno::EPIPE) => { + proc.signals.raise(wasm_posix_shared::signal::SIGPIPE); + Err(Errno::EPIPE) + } + other => other, + } } SocketDomain::Unix => { if sock.send_buf_idx.is_none() @@ -4010,6 +4020,31 @@ fn match_pty_stat(resolved: &[u8], uid: u32, gid: u32) -> Option { } } +fn unix_socket_path_stat( + proc: &Process, + host: &mut dyn HostIO, + resolved: &[u8], + follow: bool, +) -> Result, Errno> { + let registry = unsafe { crate::unix_socket::global_unix_socket_registry() }; + if !registry.contains(resolved) { + return Ok(None); + } + + // Filesystem-backed AF_UNIX sockets have ordinary path metadata. The + // socket registry tells Kandelo that the path should report S_IFSOCK, but + // uid/gid/mode/timestamps still come from the underlying VFS inode so + // chown(2), chmod(2), and stat(2) round-trip like a POSIX socket node. + check_search_path(proc, host, resolved)?; + let mut st = if follow { + host.host_stat(resolved)? + } else { + host.host_lstat(resolved)? + }; + st.st_mode = wasm_posix_shared::mode::S_IFSOCK | (st.st_mode & 0o7777); + Ok(Some(st)) +} + pub fn sys_stat(proc: &mut Process, host: &mut dyn HostIO, path: &[u8]) -> Result { let resolved = resolve_path(path, &proc.cwd); if let Some(dev) = match_virtual_device(&resolved) { @@ -4038,6 +4073,7 @@ pub fn sys_stat(proc: &mut Process, host: &mut dyn HostIO, path: &[u8]) -> Resul }); } if let Some(entry) = crate::procfs::match_procfs(&resolved, proc.pid) { + crate::procfs::validate_entry(proc, &entry)?; return Ok(crate::procfs::procfs_stat(&entry, 0, true)); } if let Some(st) = crate::devfs::match_devfs_stat(&resolved, proc.euid, proc.egid) { @@ -4046,27 +4082,8 @@ pub fn sys_stat(proc: &mut Process, host: &mut dyn HostIO, path: &[u8]) -> Resul if let Some(st) = synthetic_file_stat(&resolved, proc.euid, proc.egid) { return Ok(st); } - // Check Unix socket registry - { - let registry = unsafe { crate::unix_socket::global_unix_socket_registry() }; - if registry.contains(&resolved) { - return Ok(WasmStat { - st_dev: 0, - st_ino: 0x554E5800, // "UNX\0" - st_mode: wasm_posix_shared::mode::S_IFSOCK | 0o755, - st_nlink: 1, - st_uid: proc.euid, - st_gid: proc.egid, - st_size: 0, - st_atime_sec: 0, - st_atime_nsec: 0, - st_mtime_sec: 0, - st_mtime_nsec: 0, - st_ctime_sec: 0, - st_ctime_nsec: 0, - _pad: 0, - }); - } + if let Some(st) = unix_socket_path_stat(proc, host, &resolved, true)? { + return Ok(st); } // VFS is the source of truth for ownership: host_stat already returns the // file's real uid/gid, so just propagate. @@ -4106,6 +4123,7 @@ pub fn sys_lstat( }); } if let Some(entry) = crate::procfs::match_procfs(&resolved, proc.pid) { + crate::procfs::validate_entry(proc, &entry)?; return Ok(crate::procfs::procfs_stat(&entry, 0, false)); } if let Some(st) = crate::devfs::match_devfs_stat(&resolved, proc.euid, proc.egid) { @@ -4114,27 +4132,8 @@ pub fn sys_lstat( if let Some(st) = synthetic_file_stat(&resolved, proc.euid, proc.egid) { return Ok(st); } - // Check Unix socket registry - { - let registry = unsafe { crate::unix_socket::global_unix_socket_registry() }; - if registry.contains(&resolved) { - return Ok(WasmStat { - st_dev: 0, - st_ino: 0x554E5800, // "UNX\0" - st_mode: wasm_posix_shared::mode::S_IFSOCK | 0o755, - st_nlink: 1, - st_uid: proc.euid, - st_gid: proc.egid, - st_size: 0, - st_atime_sec: 0, - st_atime_nsec: 0, - st_mtime_sec: 0, - st_mtime_nsec: 0, - st_ctime_sec: 0, - st_ctime_nsec: 0, - _pad: 0, - }); - } + if let Some(st) = unix_socket_path_stat(proc, host, &resolved, false)? { + return Ok(st); } // VFS is the source of truth for ownership: host_lstat already returns the // link's real uid/gid, so just propagate. @@ -4246,6 +4245,7 @@ pub fn sys_readlink( // Procfs symlinks — /proc/self, /proc/self/fd/N, /proc/self/cwd, /proc/self/exe, etc. if let Some(entry) = crate::procfs::match_procfs(&resolved, proc.pid) { + crate::procfs::validate_entry(proc, &entry)?; if entry.is_symlink() { return crate::procfs::procfs_readlink(proc, &entry, buf); } @@ -4302,7 +4302,8 @@ pub fn sys_access( { return Ok(()); } - if crate::procfs::match_procfs(&resolved, proc.pid).is_some() { + if let Some(entry) = crate::procfs::match_procfs(&resolved, proc.pid) { + crate::procfs::validate_entry(proc, &entry)?; // Procfs entries are read-only: allow R_OK/F_OK/X_OK(dirs), deny W_OK if amode & 0o2 != 0 { return Err(Errno::EACCES); @@ -4320,6 +4321,7 @@ pub fn sys_chdir(proc: &mut Process, host: &mut dyn HostIO, path: &[u8]) -> Resu let resolved = crate::path::resolve_path(path, &proc.cwd); // Check virtual filesystems first (procfs, devfs), then fall through to host if let Some(entry) = crate::procfs::match_procfs(&resolved, proc.pid) { + crate::procfs::validate_entry(proc, &entry)?; let st = crate::procfs::procfs_stat(&entry, 0, true); if st.st_mode & wasm_posix_shared::mode::S_IFMT != wasm_posix_shared::mode::S_IFDIR { return Err(Errno::ENOTDIR); @@ -4661,20 +4663,26 @@ pub fn sys_getdents64( // Check if this is a cross-process directory (e.g. /proc//fd) #[cfg(any(target_arch = "wasm32", target_arch = "wasm64"))] if let Some(entry) = crate::procfs::match_procfs(&path, proc.pid) { - let target_pid = crate::procfs::entry_pid(&entry); - if target_pid != 0 && target_pid != proc.pid { - if let Some((bytes, new_offset, exhausted)) = - crate::wasm_api::procfs_getdents64_for_pid(target_pid, &path, buf, entry_offset) - { - if let Some(ofd) = proc.ofd_table.get_mut(ofd_idx) { - ofd.dir_entry_offset = new_offset; - if exhausted { - ofd.dir_host_handle = -2; + if let Some(target_pid) = crate::procfs::entry_pid(&entry) { + if target_pid != proc.pid { + if let Some((bytes, new_offset, exhausted)) = + crate::wasm_api::procfs_getdents64_for_pid( + target_pid, + &path, + buf, + entry_offset, + ) + { + if let Some(ofd) = proc.ofd_table.get_mut(ofd_idx) { + ofd.dir_entry_offset = new_offset; + if exhausted { + ofd.dir_host_handle = -2; + } } + return Ok(bytes); } - return Ok(bytes); + return Err(Errno::ENOENT); } - return Err(Errno::ENOENT); } } @@ -5595,10 +5603,19 @@ pub fn sys_mremap( flags: u32, ) -> Result { const MREMAP_MAYMOVE: u32 = 1; + const SUPPORTED_FLAGS: u32 = MREMAP_MAYMOVE; if old_len == 0 || new_len == 0 { return Err(Errno::EINVAL); } + if flags & !SUPPORTED_FLAGS != 0 { + // The current wasm libc import ABI passes the Linux mremap syscall's + // first four arguments. MREMAP_FIXED requires the fifth new-address + // argument, and MREMAP_DONTUNMAP has Linux-specific aliasing semantics + // that the kernel does not implement. Reject unsupported flags + // explicitly instead of silently treating them as an ordinary remap. + return Err(Errno::EINVAL); + } // Page-align both sizes let aligned_old = (old_len + 0xFFFF) & !0xFFFF; @@ -6677,10 +6694,26 @@ pub fn sys_send( SocketDomain::Inet | SocketDomain::Inet6 => { // Loopback path: use pipe buffers if available if sock.send_buf_idx.is_some() { - return sys_write(proc, host, fd, buf); + let nosignal = flags & MSG_NOSIGNAL != 0; + let sigpipe_was_pending = + proc.signals.is_pending(wasm_posix_shared::signal::SIGPIPE); + let result = sys_write(proc, host, fd, buf); + // MSG_NOSIGNAL: suppress SIGPIPE raised by write. + if nosignal && !sigpipe_was_pending { + proc.signals.clear(wasm_posix_shared::signal::SIGPIPE); + } + return result; } let net_handle = sock.host_net_handle.ok_or(Errno::ENOTCONN)?; - host.host_net_send(net_handle, buf, flags) + match host.host_net_send(net_handle, buf, flags) { + Err(Errno::EPIPE) => { + if flags & MSG_NOSIGNAL == 0 { + proc.signals.raise(wasm_posix_shared::signal::SIGPIPE); + } + Err(Errno::EPIPE) + } + other => other, + } } SocketDomain::Unix => { // DGRAM bit-bucket (syslog pattern): data is discarded @@ -7073,7 +7106,16 @@ pub fn sys_bind( // rebased branch.) use wasm_posix_shared::flags::{O_CREAT, O_EXCL, O_WRONLY}; check_open_permissions(proc, host, &resolved, O_CREAT | O_EXCL | O_WRONLY)?; - let h = match host.host_open(&resolved, O_CREAT | O_EXCL | O_WRONLY, 0o600) { + // Linux pathname sockets start with every permission bit enabled, + // filtered through the creating process's umask. The backing VFS + // inode owns these bits so later chmod/stat operations observe one + // authoritative value. + let socket_mode = 0o777 & !proc.umask; + let h = match host.host_open( + &resolved, + O_CREAT | O_EXCL | O_WRONLY, + socket_mode, + ) { Ok(h) => h, Err(Errno::EEXIST) => return Err(Errno::EADDRINUSE), Err(e) => return Err(e), @@ -8246,6 +8288,7 @@ pub fn sys_fstatat( }); } if let Some(entry) = crate::procfs::match_procfs(&resolved, proc.pid) { + crate::procfs::validate_entry(proc, &entry)?; let follow = flags & AT_SYMLINK_NOFOLLOW == 0; return Ok(crate::procfs::procfs_stat(&entry, 0, follow)); } @@ -8255,27 +8298,10 @@ pub fn sys_fstatat( if let Some(st) = synthetic_file_stat(&resolved, proc.euid, proc.egid) { return Ok(st); } - // Check Unix socket registry + if let Some(st) = + unix_socket_path_stat(proc, host, &resolved, flags & AT_SYMLINK_NOFOLLOW == 0)? { - let registry = unsafe { crate::unix_socket::global_unix_socket_registry() }; - if registry.contains(&resolved) { - return Ok(WasmStat { - st_dev: 0, - st_ino: 0x554E5800, // "UNX\0" - st_mode: wasm_posix_shared::mode::S_IFSOCK | 0o755, - st_nlink: 1, - st_uid: proc.euid, - st_gid: proc.egid, - st_size: 0, - st_atime_sec: 0, - st_atime_nsec: 0, - st_mtime_sec: 0, - st_mtime_nsec: 0, - st_ctime_sec: 0, - st_ctime_nsec: 0, - _pad: 0, - }); - } + return Ok(st); } // VFS is the source of truth for ownership: host_stat / host_lstat // already return the real uid/gid, so just propagate. @@ -10174,7 +10200,8 @@ pub fn sys_faccessat( { return Ok(()); } - if crate::procfs::match_procfs(&resolved, proc.pid).is_some() { + if let Some(entry) = crate::procfs::match_procfs(&resolved, proc.pid) { + crate::procfs::validate_entry(proc, &entry)?; if amode & 0o2 != 0 { return Err(Errno::EACCES); } @@ -10276,6 +10303,7 @@ pub fn sys_readlinkat( // Procfs symlinks — /proc/self, /proc/self/fd/N, /proc/self/cwd, etc. if let Some(entry) = crate::procfs::match_procfs(&resolved, proc.pid) { + crate::procfs::validate_entry(proc, &entry)?; if entry.is_symlink() { return crate::procfs::procfs_readlink(proc, &entry, buf); } @@ -11086,6 +11114,11 @@ mod tests { /// Override for `gl_submit`'s return value (0 = success, negative /// = errno). Defaults to 0. gl_submit_rc: i32, + net_connect_result: Result<(), Errno>, + net_connect_status_result: Result<(), Errno>, + net_send_result: Result, + net_connect_calls: Vec<(i32, Vec, u16)>, + net_listen_calls: Vec<(i32, u16, [u8; 4])>, } impl MockHostIO { @@ -11109,6 +11142,11 @@ mod tests { gl_unbind_calls: Vec::new(), gbm_bo_bind_rc: 0, gl_submit_rc: 0, + net_connect_result: Err(Errno::ECONNREFUSED), + net_connect_status_result: Err(Errno::ECONNREFUSED), + net_send_result: Err(Errno::ENOTCONN), + net_connect_calls: Vec::new(), + net_listen_calls: Vec::new(), } } @@ -11465,14 +11503,15 @@ mod tests { } fn host_net_connect( &mut self, - _handle: i32, - _addr: &[u8], - _port: u16, + handle: i32, + addr: &[u8], + port: u16, ) -> Result<(), Errno> { - Err(Errno::ECONNREFUSED) + self.net_connect_calls.push((handle, addr.to_vec(), port)); + self.net_connect_result } fn host_net_connect_status(&mut self, _handle: i32) -> Result<(), Errno> { - Err(Errno::ECONNREFUSED) + self.net_connect_status_result } fn host_net_send( &mut self, @@ -11480,7 +11519,7 @@ mod tests { _data: &[u8], _flags: u32, ) -> Result { - Err(Errno::ENOTCONN) + self.net_send_result } fn host_net_recv( &mut self, @@ -11494,7 +11533,8 @@ mod tests { fn host_net_close(&mut self, _handle: i32) -> Result<(), Errno> { Ok(()) } - fn host_net_listen(&mut self, _fd: i32, _port: u16, _addr: &[u8; 4]) -> Result<(), Errno> { + fn host_net_listen(&mut self, fd: i32, port: u16, addr: &[u8; 4]) -> Result<(), Errno> { + self.net_listen_calls.push((fd, port, *addr)); Ok(()) } fn host_getaddrinfo(&mut self, _name: &[u8], _result: &mut [u8]) -> Result { @@ -13450,6 +13490,25 @@ mod tests { assert!(proc.signals.is_pending(wasm_posix_shared::signal::SIGPIPE)); } + #[test] + fn test_write_external_tcp_epipe_raises_sigpipe() { + let mut proc = Process::new(1); + let mut host = MockHostIO::new(); + use wasm_posix_shared::socket::*; + + host.net_connect_result = Ok(()); + host.net_connect_status_result = Ok(()); + host.net_send_result = Err(Errno::EPIPE); + + let fd = sys_socket(&mut proc, &mut host, AF_INET, SOCK_STREAM, 0).unwrap(); + let addr = [2, 0, 0, 80, 93, 184, 216, 34, 0, 0, 0, 0, 0, 0, 0, 0]; + sys_connect(&mut proc, &mut host, fd, &addr).unwrap(); + + let result = sys_write(&mut proc, &mut host, fd, b"test"); + assert_eq!(result, Err(Errno::EPIPE)); + assert!(proc.signals.is_pending(wasm_posix_shared::signal::SIGPIPE)); + } + #[test] fn test_socketpair_close_both_frees_pipes() { let mut proc = Process::new(1); @@ -13658,6 +13717,46 @@ mod tests { assert!(!proc.signals.is_pending(wasm_posix_shared::signal::SIGPIPE)); } + #[test] + fn test_send_external_tcp_msg_nosignal_suppresses_sigpipe() { + let mut proc = Process::new(1); + let mut host = MockHostIO::new(); + use wasm_posix_shared::socket::*; + + host.net_connect_result = Ok(()); + host.net_connect_status_result = Ok(()); + host.net_send_result = Err(Errno::EPIPE); + + let fd = sys_socket(&mut proc, &mut host, AF_INET, SOCK_STREAM, 0).unwrap(); + let addr = [2, 0, 0, 80, 93, 184, 216, 34, 0, 0, 0, 0, 0, 0, 0, 0]; + sys_connect(&mut proc, &mut host, fd, &addr).unwrap(); + + let result = sys_send(&mut proc, &mut host, fd, b"test", MSG_NOSIGNAL); + assert_eq!(result, Err(Errno::EPIPE)); + assert!(!proc.signals.is_pending(wasm_posix_shared::signal::SIGPIPE)); + } + + #[test] + fn test_send_external_tcp_epipe_raises_sigpipe() { + // Default flags (no MSG_NOSIGNAL): an EPIPE from the host bridge on an + // external TCP send must raise SIGPIPE, mirroring sys_write's path. + let mut proc = Process::new(1); + let mut host = MockHostIO::new(); + use wasm_posix_shared::socket::*; + + host.net_connect_result = Ok(()); + host.net_connect_status_result = Ok(()); + host.net_send_result = Err(Errno::EPIPE); + + let fd = sys_socket(&mut proc, &mut host, AF_INET, SOCK_STREAM, 0).unwrap(); + let addr = [2, 0, 0, 80, 93, 184, 216, 34, 0, 0, 0, 0, 0, 0, 0, 0]; + sys_connect(&mut proc, &mut host, fd, &addr).unwrap(); + + let result = sys_send(&mut proc, &mut host, fd, b"test", 0); + assert_eq!(result, Err(Errno::EPIPE)); + assert!(proc.signals.is_pending(wasm_posix_shared::signal::SIGPIPE)); + } + #[test] fn test_send_not_connected() { let mut proc = Process::new(1); @@ -16683,10 +16782,21 @@ mod tests { #[test] fn test_stat_unix_socket_path() { + use wasm_posix_shared::flags::AT_FDCWD; + use wasm_posix_shared::mode::{S_IFMT, S_IFSOCK}; + + fn assert_socket_metadata(st: WasmStat, mode: u32, uid: u32, gid: u32) { + assert_eq!(st.st_mode & S_IFMT, S_IFSOCK); + assert_eq!(st.st_mode & 0o7777, mode); + assert_eq!(st.st_uid, uid); + assert_eq!(st.st_gid, gid); + } + let _lock = UNIX_REGISTRY_LOCK.lock().unwrap(); let mut proc = Process::new(1); let mut host = MockHostIO::new(); proc.pid = 9020; + proc.umask = 0o027; let fd = sys_socket(&mut proc, &mut host, 1, 1, 0).unwrap(); let mut addr = [0u8; 110]; addr[0] = 1; @@ -16694,10 +16804,44 @@ mod tests { addr[2..2 + path.len()].copy_from_slice(path); sys_bind(&mut proc, &mut host, fd, &addr[..2 + path.len() + 1]).unwrap(); - let st = sys_stat(&mut proc, &mut host, b"/tmp/stat.sock").unwrap(); - assert_eq!( - st.st_mode & wasm_posix_shared::mode::S_IFMT, - wasm_posix_shared::mode::S_IFSOCK + assert_socket_metadata( + sys_stat(&mut proc, &mut host, path).unwrap(), + 0o750, + 0, + 0, + ); + assert_socket_metadata( + sys_lstat(&mut proc, &mut host, path).unwrap(), + 0o750, + 0, + 0, + ); + assert_socket_metadata( + sys_fstatat(&mut proc, &mut host, AT_FDCWD, path, 0).unwrap(), + 0o750, + 0, + 0, + ); + + sys_chmod(&mut proc, &mut host, path, 0o640).unwrap(); + sys_chown(&mut proc, &mut host, b"/tmp/stat.sock", 1234, 5678).unwrap(); + assert_socket_metadata( + sys_stat(&mut proc, &mut host, path).unwrap(), + 0o640, + 1234, + 5678, + ); + assert_socket_metadata( + sys_lstat(&mut proc, &mut host, path).unwrap(), + 0o640, + 1234, + 5678, + ); + assert_socket_metadata( + sys_fstatat(&mut proc, &mut host, AT_FDCWD, path, 0).unwrap(), + 0o640, + 1234, + 5678, ); // Cleanup @@ -19757,6 +19901,29 @@ mod tests { // in `host/src/kernel-worker.ts`'s SYS_MREMAP post-syscall fixup. } + #[test] + fn test_mremap_rejects_unsupported_flags() { + let mut proc = Process::new(1); + use wasm_posix_shared::mmap::{MAP_ANONYMOUS, MAP_PRIVATE, PROT_READ, PROT_WRITE}; + let addr = proc.memory.mmap_anonymous( + 0, + 0x10000, + PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, + ); + + // Linux MREMAP_FIXED requires the fifth syscall argument (new_addr), + // which the current wasm libc import ABI does not pass through. The + // kernel must reject it rather than accidentally treating the request + // as an in-place grow at old_addr. + assert_eq!( + sys_mremap(&mut proc, addr, 0x10000, 0x20000, 2).unwrap_err(), + Errno::EINVAL + ); + assert!(proc.memory.is_mapped(addr)); + assert!(!proc.memory.is_mapped(addr + 0x10000)); + } + // ---- O_CLOFORK / FD_CLOFORK tests ---- #[test] @@ -20059,6 +20226,74 @@ mod tests { ); } + #[test] + fn test_procfs_metadata_rejects_missing_pid_scopes() { + let mut proc = Process::new(1); + let mut host = MockHostIO::new(); + let missing_paths: &[&[u8]] = &[ + b"/proc/0", + b"/proc/999", + b"/proc/999/stat", + b"/proc/999/net", + b"/proc/999/net/tcp", + ]; + + for path in missing_paths { + assert_eq!( + sys_stat(&mut proc, &mut host, path).unwrap_err(), + Errno::ENOENT + ); + assert_eq!( + sys_lstat(&mut proc, &mut host, path).unwrap_err(), + Errno::ENOENT + ); + assert_eq!( + sys_access(&mut proc, &mut host, path, 0), + Err(Errno::ENOENT) + ); + assert_eq!( + sys_fstatat(&mut proc, &mut host, AT_FDCWD, path, 0).unwrap_err(), + Errno::ENOENT + ); + assert_eq!( + sys_faccessat(&mut proc, &mut host, AT_FDCWD, path, 0, 0), + Err(Errno::ENOENT) + ); + let mut link_buf = [0u8; 64]; + assert_eq!( + sys_readlink(&mut proc, &mut host, path, &mut link_buf).unwrap_err(), + Errno::ENOENT + ); + assert_eq!( + sys_readlinkat(&mut proc, &mut host, AT_FDCWD, path, &mut link_buf).unwrap_err(), + Errno::ENOENT + ); + } + + assert_eq!( + sys_chdir(&mut proc, &mut host, b"/proc/999"), + Err(Errno::ENOENT) + ); + assert_eq!( + sys_chdir(&mut proc, &mut host, b"/proc/999/net"), + Err(Errno::ENOENT) + ); + } + + #[test] + fn test_procfs_metadata_accepts_global_and_live_pid_net_paths() { + let mut proc = Process::new(1); + let mut host = MockHostIO::new(); + + for path in [b"/proc/net".as_slice(), b"/proc/1/net".as_slice()] { + let st = sys_stat(&mut proc, &mut host, path).unwrap(); + assert_eq!(st.st_mode & S_IFMT, S_IFDIR); + } + + let st = sys_stat(&mut proc, &mut host, b"/proc/1/net/tcp").unwrap(); + assert_eq!(st.st_mode & S_IFMT, S_IFREG); + } + #[test] fn test_procfs_open_write_fails() { let mut proc = Process::new(1); diff --git a/crates/kernel/src/wasm_api.rs b/crates/kernel/src/wasm_api.rs index 93f9c8ad9..01a6a5a37 100644 --- a/crates/kernel/src/wasm_api.rs +++ b/crates/kernel/src/wasm_api.rs @@ -1058,7 +1058,7 @@ use crate::process_table::GLOBAL_PROCESS_TABLE as PROCESS_TABLE; /// Get all active PIDs from the process table. pub(crate) fn procfs_all_pids() -> Vec { let table = unsafe { &*PROCESS_TABLE.0.get() }; - table.all_pids() + table.procfs_pids() } /// Generate procfs content for a foreign process (cross-process access). @@ -1101,7 +1101,7 @@ pub(crate) fn procfs_getdents64_for_pid( ) -> Option<(usize, i64, bool)> { let table = unsafe { &*PROCESS_TABLE.0.get() }; let proc = table.get(pid)?; - let pids = table.all_pids(); + let pids = table.procfs_pids(); crate::procfs::procfs_getdents64(proc, ofd_path, buf, offset, &pids).ok() } @@ -6037,7 +6037,8 @@ pub extern "C" fn kernel_utimensat( result } -/// Remap memory. Returns MAP_FAILED (-1 as u32) since Wasm doesn't support this. +/// Remap memory. Supports in-place resize and MREMAP_MAYMOVE; unsupported +/// Linux-specific flag combinations are rejected by sys_mremap with EINVAL. #[unsafe(no_mangle)] pub extern "C" fn kernel_mremap( old_addr: usize, diff --git a/docs/posix-status.md b/docs/posix-status.md index dbec136ee..c95f37412 100644 --- a/docs/posix-status.md +++ b/docs/posix-status.md @@ -109,6 +109,7 @@ Kandelo uses a single kernel Wasm instance that holds a `ProcessTable` and serve | `getgid()` / `getegid()` | Full | Simulated; defaults to gid=0 (root). Configurable via setgid/setegid. | | `setuid()` / `seteuid()` | Full | POSIX semantics (no saved-set-uid tracked). As root: setuid sets both uid and euid; seteuid sets any euid. Non-root: setuid only to own uid; seteuid only to own ruid. Returns EPERM otherwise. | | `setgid()` / `setegid()` | Full | POSIX semantics mirroring setuid — gated on euid==0 for privileged changes. Returns EPERM for non-root trying to change to a foreign gid. | +| `getpriority()` / `setpriority()` | Partial | Stores a per-process nice value; WebAssembly has no host CPU scheduler to apply it to. Linux-compatible `/proc//stat` exposes scheduler priority in field 18 and nice in field 19. Procfs metadata operations reject missing or reaped PID scopes. | | `getpgrp()` | Full | Returns process group ID (simulated, defaults to pid). | | `setpgid()` | Partial | Sets process group ID. pid=0 means self. pgid=0 means use target pid. Only supports setting own pgid; other processes return ESRCH. | | `getsid()` | Full | Returns session ID (simulated, defaults to pid). pid=0 means self. | @@ -193,7 +194,7 @@ Kandelo uses a single kernel Wasm instance that holds a `ProcessTable` and serve | `chdir()` / `getcwd()` | Partial | Kernel-maintained cwd. chdir validates via host_stat that target is S_IFDIR. getcwd returns ERANGE if buffer too small. | | `link()` / `unlink()` | Partial | Host-delegated. Relative paths resolved via kernel cwd. | | `rename()` | Partial | Host-delegated. Both paths resolved via kernel cwd. | -| `stat()` / `lstat()` | Partial | Host-delegated. stat follows symlinks, lstat does not. | +| `stat()` / `lstat()` | Partial | Host-delegated. stat follows symlinks, lstat does not. Registered AF_UNIX pathname sockets preserve the backing VFS inode's uid, gid, permissions, timestamps, and link count while reporting `S_IFSOCK`. | | `chmod()` / `chown()` | Partial | VFS metadata updates. Node host-backed files receive native mode only at file/directory creation; later mode changes and all ownership changes stay virtual. Browser memory-backed mounts store metadata in the VFS. | | `access()` | Partial | Host-delegated. Checks real filesystem permissions. | | `realpath()` | Full | Resolves path against cwd, normalizes `.`/`..`, resolves symlinks via iterative lstat/readlink (ELOOP after 40 resolutions), verifies existence. | @@ -229,11 +230,11 @@ shortcuts. |----------|--------|-------| | `socket()` | Partial | AF_UNIX and AF_INET supported for SOCK_STREAM and SOCK_DGRAM. SOCK_NONBLOCK and SOCK_CLOEXEC flags handled. AF_INET SOCK_DGRAM is implemented as a kernel-level datagram socket for loopback/virtual routes; external raw UDP is not exposed directly to userspace. | | `socketpair()` | Full | AF_UNIX SOCK_STREAM. Bidirectional ring buffers (64KB each). Returns pre-connected pair. | -| `bind()` | Partial | AF_UNIX paths, AF_INET TCP host-backed bind/listen, and AF_INET UDP in-kernel bind for INADDR_ANY, loopback, and broadcast addresses. The browser local virtual-network backend supports AF_INET TCP/UDP binds between attached Kandelo machines. UDP ephemeral ports, getsockname, bind conflicts, and SO_REUSEADDR conflict cases are covered by Sortix UDP tests. | +| `bind()` | Partial | AF_UNIX paths, AF_INET TCP host-backed bind/listen, and AF_INET UDP in-kernel bind for INADDR_ANY, loopback, and broadcast addresses. AF_UNIX pathname bind creates a VFS inode with mode `0777 & ~umask`; while the path remains registered, `stat`/`lstat`/`fstatat`, `chmod`, and `chown` share that inode metadata. Registry state does not yet follow pathname hard links or renames, and process cleanup can leave an ordinary-file placeholder behind. The browser local virtual-network backend supports AF_INET TCP/UDP binds between attached Kandelo machines. UDP ephemeral ports, getsockname, bind conflicts, and SO_REUSEADDR conflict cases are covered by Sortix UDP tests. | | `listen()` | Partial | AF_INET TCP delegates to the active HostIO networking backend, including Node `net` and the browser local virtual-network backend. Datagram listen rejects as unsupported. AF_UNIX stream listen remains EOPNOTSUPP. | | `accept()` / `accept4()` | Partial | AF_INET TCP delegates to the active HostIO networking backend and returns connected sockets. Datagram accept rejects as unsupported. SOCK_NONBLOCK and SOCK_CLOEXEC flags on accept4. | | `connect()` | Partial | AF_UNIX SOCK_DGRAM connects to a bit-bucket pattern. AF_INET TCP is host-backed and works over Node external TCP or the browser local virtual-network backend. AF_INET UDP connect stores the peer, auto-binds an ephemeral local port when needed, filters receives to the connected peer, and supports AF_UNSPEC unconnect. UDP loopback and local virtual routes are supported; external raw UDP routes return ENETUNREACH without a HostIO UDP proxy/backend. | -| `send()` / `recv()` | Partial | Unix domain streams, AF_INET TCP, and connected AF_INET UDP. TCP send/recv works over Node external TCP and the local virtual-network backend. UDP preserves datagram boundaries and supports MSG_PEEK/MSG_DONTWAIT through recvfrom. MSG_NOSIGNAL is honored for stream/broken-pipe paths. | +| `send()` / `recv()` | Partial | Unix domain streams, AF_INET TCP, and connected AF_INET UDP. TCP send/recv works over Node external TCP and the local virtual-network backend. UDP preserves datagram boundaries and supports MSG_PEEK/MSG_DONTWAIT through recvfrom. A send to a stream socket whose peer has closed returns EPIPE and raises SIGPIPE, including host-bridged (external and loopback) TCP; MSG_NOSIGNAL suppresses the signal while still returning EPIPE. | | `sendto()` / `recvfrom()` | Partial | AF_INET UDP loopback and local virtual-network send/receive, connected and unconnected sendto, source address reporting, and connected receive filtering are implemented. External raw UDP routes return ENETUNREACH and are narrowly xfailed in the Sortix UDP suite. | | `setsockopt()` / `getsockopt()` | Partial | SOL_SOCKET: SO_TYPE, SO_DOMAIN, SO_ERROR, SO_ACCEPTCONN, SO_RCVBUF, SO_SNDBUF readable; SO_REUSEADDR affects UDP bind conflicts; SO_KEEPALIVE, SO_LINGER, SO_RCVTIMEO, SO_SNDTIMEO, SO_BROADCAST accepted/stored. IPPROTO_TCP: TCP_NODELAY stored. | | `shutdown()` | Partial | SHUT_RD, SHUT_WR, SHUT_RDWR for stream sockets and UDP readiness/error behavior. UDP write shutdown returns EPIPE on datagram send; read shutdown is EOF-like for recv/poll. | diff --git a/packages/registry/posix-utils-lite/build-posix-utils-lite.sh b/packages/registry/posix-utils-lite/build-posix-utils-lite.sh index 4c616a45d..115637b0f 100755 --- a/packages/registry/posix-utils-lite/build-posix-utils-lite.sh +++ b/packages/registry/posix-utils-lite/build-posix-utils-lite.sh @@ -10,6 +10,9 @@ SRC="$SCRIPT_DIR/src/posix-utils-lite.c" BIN_DIR="$SCRIPT_DIR/bin" SYSROOT="$REPO_ROOT/sysroot" +# Keep direct and resolver-driven builds pinned to this worktree's SDK. +source "$REPO_ROOT/sdk/activate.sh" + UTILITIES=( ar asa cal cflow compress ctags cxref ed ex fuser gencat getconf gettext iconv ipcrm ipcs lex locale logger man more msgfmt ngettext nm patch pax diff --git a/packages/registry/posix-utils-lite/build.toml b/packages/registry/posix-utils-lite/build.toml index 22d203eea..49e50603f 100644 --- a/packages/registry/posix-utils-lite/build.toml +++ b/packages/registry/posix-utils-lite/build.toml @@ -1,7 +1,7 @@ script_path = "packages/registry/posix-utils-lite/build-posix-utils-lite.sh" repo_url = "https://github.com/brandonpayton/kandelo.git" commit = "13930e1d2352048372847b6ec6cede8ebb25b5f8" -revision = 2 +revision = 3 [binary] index_url = "https://github.com/Automattic/kandelo/releases/download/binaries-abi-v{abi}/index.toml" diff --git a/packages/registry/posix-utils-lite/src/posix-utils-lite.c b/packages/registry/posix-utils-lite/src/posix-utils-lite.c index 505c975ac..2a3a598fd 100644 --- a/packages/registry/posix-utils-lite/src/posix-utils-lite.c +++ b/packages/registry/posix-utils-lite/src/posix-utils-lite.c @@ -415,13 +415,208 @@ static int util_logger(int argc, char **argv) { return 0; } +struct ps_fields { + int pid; + int nice; + int command; +}; + +static int ps_pid_selected(long pid, long *pids, int npids) { + if (npids == 0) { + return 1; + } + for (int i = 0; i < npids; i++) { + if (pids[i] == pid) { + return 1; + } + } + return 0; +} + +static int ps_add_pid_list(const char *arg, long *pids, int *npids, int max_pids) { + if (!arg || !*arg) { + return -1; + } + const char *p = arg; + for (;;) { + if (*npids >= max_pids) { + return -1; + } + errno = 0; + char *end = NULL; + long pid = strtol(p, &end, 10); + if (errno != 0 || end == p || pid <= 0 || (*end != '\0' && *end != ',')) { + return -1; + } + pids[(*npids)++] = pid; + if (*end == '\0') { + return 0; + } + p = end + 1; + if (*p == '\0') { + return -1; + } + } +} + +static void ps_parse_fields(const char *arg, struct ps_fields *fields) { + fields->pid = 0; + fields->nice = 0; + fields->command = 0; + + char *copy = xstrdup(arg); + for (char *tok = strtok(copy, ", "); tok; tok = strtok(NULL, ", ")) { + if (streq(tok, "pid")) { + fields->pid = 1; + } else if (streq(tok, "nice") || streq(tok, "ni")) { + fields->nice = 1; + } else if (streq(tok, "comm") || streq(tok, "command") || + streq(tok, "args")) { + fields->command = 1; + } + } + if (!fields->pid && !fields->nice && !fields->command) { + fields->pid = 1; + fields->command = 1; + } + free(copy); +} + +static int ps_read_stat(long pid, char *comm, size_t comm_len, long *nice_value) { + char path[PATH_MAX]; + snprintf(path, sizeof(path), "/proc/%ld/stat", pid); + FILE *f = fopen(path, "r"); + if (!f) { + return -1; + } + char line[1024]; + if (!fgets(line, sizeof(line), f)) { + fclose(f); + return -1; + } + fclose(f); + + char *open = strchr(line, '('); + char *close = strrchr(line, ')'); + if (!open || !close || close <= open) { + return -1; + } + size_t n = (size_t)(close - open - 1); + if (n >= comm_len) { + n = comm_len - 1; + } + memcpy(comm, open + 1, n); + comm[n] = '\0'; + + // Fields after comm begin with field 3 (state). Nice is field 19, so it + // is token index 16 in this suffix. This follows Linux /proc//stat + // and matches the kernel procfs generator. + char *suffix = close + 2; + char *save = NULL; + int index = 0; + for (char *tok = strtok_r(suffix, " \t\r\n", &save); tok; + tok = strtok_r(NULL, " \t\r\n", &save), index++) { + if (index == 16) { + char *end = NULL; + errno = 0; + long parsed = strtol(tok, &end, 10); + if (errno != 0 || end == tok || *end != '\0') { + return -1; + } + if (nice_value) { + *nice_value = parsed; + } + return 0; + } + } + return -1; +} + +static void ps_read_cmdline(long pid, char *cmd, size_t cmd_len) { + char path[PATH_MAX]; + snprintf(path, sizeof(path), "/proc/%ld/cmdline", pid); + FILE *f = fopen(path, "rb"); + cmd[0] = '\0'; + if (!f) { + return; + } + size_t n = fread(cmd, 1, cmd_len - 1, f); + fclose(f); + for (size_t i = 0; i < n; i++) { + if (cmd[i] == '\0') { + cmd[i] = ' '; + } + } + cmd[n] = '\0'; +} + +static void ps_print_header(const struct ps_fields *fields) { + if (fields->pid) { + printf("%5s", "PID"); + } + if (fields->nice) { + printf("%s%4s", fields->pid ? " " : "", "NICE"); + } + if (fields->command) { + printf("%s%s", (fields->pid || fields->nice) ? " " : "", "COMMAND"); + } + putchar('\n'); +} + +static void ps_print_row(const struct ps_fields *fields, long pid, long nice_value, + const char *cmd) { + if (fields->pid) { + printf("%5ld", pid); + } + if (fields->nice) { + printf("%s%4ld", fields->pid ? " " : "", nice_value); + } + if (fields->command) { + printf("%s%s", (fields->pid || fields->nice) ? " " : "", cmd && cmd[0] ? cmd : "?"); + } + putchar('\n'); +} + static int util_ps(int argc, char **argv) { + long selected_pids[64]; + int nselected = 0; + struct ps_fields fields = {1, 0, 1}; + + for (int i = 1; i < argc; i++) { + if (streq(argv[i], "-p") || streq(argv[i], "--pid")) { + if (i + 1 >= argc || + ps_add_pid_list(argv[++i], selected_pids, &nselected, + (int)(sizeof(selected_pids) / sizeof(selected_pids[0]))) != 0) { + fprintf(stderr, "ps: invalid pid list\n"); + return 2; + } + } else if (strncmp(argv[i], "-p", 2) == 0 && argv[i][2]) { + if (ps_add_pid_list(argv[i] + 2, selected_pids, &nselected, + (int)(sizeof(selected_pids) / sizeof(selected_pids[0]))) != 0) { + fprintf(stderr, "ps: invalid pid list\n"); + return 2; + } + } else if (streq(argv[i], "-o") || streq(argv[i], "--format")) { + if (i + 1 >= argc) { + fprintf(stderr, "ps: option requires a format\n"); + return 2; + } + ps_parse_fields(argv[++i], &fields); + } else if (strncmp(argv[i], "-o", 2) == 0 && argv[i][2]) { + ps_parse_fields(argv[i] + 2, &fields); + } else if (!streq(argv[i], "-A") && !streq(argv[i], "-e")) { + fprintf(stderr, "ps: unsupported option: %s\n", argv[i]); + return 2; + } + } + DIR *proc = opendir("/proc"); - puts(" PID COMMAND"); if (!proc) { - printf("%5ld %s\n", (long)getpid(), program_name(argv[0])); - return 0; + perror("ps: /proc"); + return 1; } + ps_print_header(&fields); + int matched = 0; struct dirent *de; while ((de = readdir(proc)) != NULL) { char *end = NULL; @@ -429,32 +624,28 @@ static int util_ps(int argc, char **argv) { if (!end || *end != '\0') { continue; } - char path[PATH_MAX]; - snprintf(path, sizeof(path), "/proc/%ld/cmdline", pid); - FILE *f = fopen(path, "rb"); + if (!ps_pid_selected(pid, selected_pids, nselected)) { + continue; + } char cmd[256] = ""; - if (f) { - size_t n = fread(cmd, 1, sizeof(cmd) - 1, f); - fclose(f); - for (size_t i = 0; i < n; i++) { - if (cmd[i] == '\0') { - cmd[i] = ' '; - } - } - cmd[n] = '\0'; + char comm[256] = ""; + long nice_value = 0; + if (ps_read_stat(pid, comm, sizeof(comm), &nice_value) != 0) { + // The process may have exited between readdir() and fopen(). Do + // not fabricate a row or a nice value for a vanished process. + continue; } if (cmd[0] == '\0') { - snprintf(path, sizeof(path), "/proc/%ld/stat", pid); - f = fopen(path, "r"); - if (f) { - fscanf(f, "%*d (%255[^)])", cmd); - fclose(f); - } + ps_read_cmdline(pid, cmd, sizeof(cmd)); } - printf("%5ld %s\n", pid, cmd[0] ? cmd : "?"); + if (cmd[0] == '\0' && comm[0] != '\0') { + snprintf(cmd, sizeof(cmd), "%s", comm); + } + ps_print_row(&fields, pid, nice_value, cmd); + matched++; } closedir(proc); - return 0; + return nselected > 0 && matched == 0 ? 1 : 0; } static int util_renice(int argc, char **argv) {