use crate::bridge::LifecycleState;use crate::command_registry::{CommandDriver, CommandRegistry};use crate::device_layer::{create_device_layer, DeviceLayer};use crate::dns::{ format_dns_resource, resolve_dns, resolve_dns_records, DnsConfig, DnsLookupPolicy, DnsRecordResolution, DnsResolution, DnsResolverErrorKind, HickoryDnsResolver, SharedDnsResolver,};use crate::fd_table::{ AnonymousFile, AnonymousFileUsage, FdEntry, FdStat, FdTableError, FdTableManager, FileDescription, FileLockManager, FileLockTarget, FlockOperation, ProcessFdTable, RecordLock, RecordLockType, SharedAnonymousFile, TransferredFd, FD_CLOEXEC, FILETYPE_CHARACTER_DEVICE, FILETYPE_DIRECTORY, FILETYPE_PIPE, FILETYPE_REGULAR_FILE, FILETYPE_SOCKET_DGRAM, FILETYPE_SOCKET_STREAM, FILETYPE_SYMBOLIC_LINK, F_DUPFD, O_APPEND, O_CREAT, O_DIRECT, O_DIRECTORY, O_EXCL, O_NOFOLLOW, O_NONBLOCK, O_RDONLY, O_RDWR, O_TRUNC, O_WRONLY,};use crate::mount_table::{MountEntry, MountOptions, MountTable, MountedFileSystem};use crate::network_policy::format_tcp_resource;use crate::permissions::{ check_command_execution, check_network_access, FsOperation, NetworkOperation, PermissionError, PermissionedFileSystem, Permissions,};use crate::pipe_manager::{PipeError, PipeManager};use crate::poll::{ PollEvents, PollFd, PollNotifier, PollResult, PollTarget, PollTargetEntry, PollTargetResult, POLLERR, POLLHUP, POLLIN, POLLNVAL, POLLOUT,};use crate::process_table::{ DriverProcess, ProcessContext, ProcessExitCallback, ProcessInfo, ProcessStatus, ProcessTable, ProcessTableError, ProcessWaitResult, SigmaskHow, SignalSet, DEFAULT_PROCESS_UMASK, SIGCONT, SIGPIPE, SIGSTOP, SIGTSTP, SIGWINCH,};use crate::pty::{ LineDisciplineConfig, PartialTermios, PtyError, PtyManager, PtyWindowSize, Termios,};use crate::resource_accounting::{ measure_filesystem_usage, FileSystemStats, FileSystemUsage, ResourceAccountant, ResourceError, ResourceLimits, ResourceSnapshot, DEFAULT_MAX_OPEN_FDS,};use crate::root_fs::{ encode_snapshot, RootFileSystem, RootFilesystemError, RootFilesystemSnapshot,};use crate::socket_table::{ DatagramSocketOption, InetSocketAddress, OpaqueTransferredRight, ReceivedDatagram, SocketId, SocketMulticastMembership, SocketReadiness, SocketRecord, SocketShutdown, SocketSpec, SocketState, SocketTable, SocketTableError, SocketType, TransferredSocketRight,};use crate::user::{ProcessIdentity, UserConfig, UserManager};use crate::vfs::{ normalize_path, VfsError, VfsResult, VirtualDirEntry, VirtualFileSystem, VirtualStat, VirtualTimeSpec, VirtualUtimeSpec, MAX_PATH_LENGTH, RENAME_EXCHANGE, S_IFDIR, S_IFLNK, S_IFREG,};use hickory_proto::rr::RecordType;use std::any::Any;use std::collections::{BTreeMap, BTreeSet, VecDeque};use std::error::Error;use std::fmt;#[cfg(test)]use std::sync::OnceLock;use std::sync::{Arc, Condvar, Mutex, MutexGuard, WaitTimeoutResult};use std::time::Duration;use web_time::{Instant, SystemTime, UNIX_EPOCH}; pub type KernelResult<T> = Result<T, KernelError>;pub use crate::process_table::{ProcessWaitEvent as WaitPidEvent, WaitPidFlags}; pub const SEEK_SET: u8 = 0;pub const SEEK_CUR: u8 = 1;pub const SEEK_END: u8 = 2;const EXECUTABLE_PERMISSION_BITS: u32 = 0o111;const SHEBANG_LINE_MAX_BYTES: usize = 256;const MAX_EXEC_INTERPRETER_DEPTH: usize = 4;const MAX_UNIX_SOCKET_SYMLINKS: usize = 40;const UNIX_SOCKET_FILE_TYPE: u32 = 0o140000;const UNIX_DAC_WRITE: u32 = 0o2;const UNIX_DAC_SEARCH: u32 = 0o1; #[derive(Debug, Clone, PartialEq, Eq)]pub struct KernelError { code: &'static str, message: String,} impl KernelError { pub fn code(&self) -> &'static str { self.code } fn new(code: &'static str, message: impl Into<String>) -> Self { Self { code, message: message.into(), } } fn disposed() -> Self { Self::new("EINVAL", "kernel VM is disposed") } fn no_such_process(pid: u32) -> Self { Self::new("ESRCH", format!("no such process {pid}")) } fn bad_file_descriptor(fd: u32) -> Self { Self::new("EBADF", format!("bad file descriptor {fd}")) } fn permission_denied(message: impl Into<String>) -> Self { Self::new("EPERM", message) } fn command_not_found(command: &str) -> Self { Self::new("ENOENT", format!("command not found: {command}")) }} impl fmt::Display for KernelError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}: {}", self.code, self.message) }} impl Error for KernelError {} fn linux_shebang_interpreter(header: &[u8], path: &str) -> KernelResult<Option<String>> { if !header.starts_with(b"#!") { return Ok(None); } let payload = &header[2..]; let newline = payload.iter().position(|byte| *byte == b'\n'); let line = newline.map_or(payload, |index| &payload[..index]); let line_end = line .iter() .rposition(|byte| !matches!(*byte, b' ' | b'\t')) .map(|index| index + 1) .ok_or_else(|| KernelError::new("ENOEXEC", format!("invalid shebang line: {path}")))?; let line = &line[..line_end]; let interpreter_start = line .iter() .position(|byte| !matches!(*byte, b' ' | b'\t')) .ok_or_else(|| KernelError::new("ENOEXEC", format!("invalid shebang line: {path}")))?; let interpreter_tail = &line[interpreter_start..]; let separator = interpreter_tail .iter() .position(|byte| matches!(*byte, b' ' | b'\t')); // Linux accepts a truncated optional argument, but it rejects a shebang // whose interpreter pathname itself reaches the end of BINPRM_BUF_SIZE. if newline.is_none() && header.len() >= SHEBANG_LINE_MAX_BYTES && separator.is_none() { return Err(KernelError::new( "ENOEXEC", format!("shebang interpreter path exceeds the Linux header limit: {path}"), )); } let interpreter_end = separator.unwrap_or(interpreter_tail.len()); let interpreter = std::str::from_utf8(&interpreter_tail[..interpreter_end]) .map_err(|_| KernelError::new("ENOEXEC", format!("invalid shebang line: {path}")))?; if interpreter.is_empty() { return Err(KernelError::new( "ENOEXEC", format!("invalid shebang line: {path}"), )); } Ok(Some(interpreter.to_owned()))} #[derive(Clone)]pub struct KernelVmConfig { pub vm_id: String, pub env: BTreeMap<String, String>, pub cwd: String, pub user: UserConfig, pub permissions: Permissions, pub loopback_exempt_ports: BTreeSet<u16>, pub dns: DnsConfig, pub dns_resolver: SharedDnsResolver, pub resources: ResourceLimits, pub zombie_ttl: Duration,} impl KernelVmConfig { pub fn new(vm_id: impl Into<String>) -> Self { Self { vm_id: vm_id.into(), env: BTreeMap::new(), cwd: String::from("/workspace"), user: UserConfig::default(), permissions: Permissions::default(), loopback_exempt_ports: BTreeSet::new(), dns: DnsConfig::default(), dns_resolver: Arc::new(HickoryDnsResolver::default()), resources: ResourceLimits::default(), zombie_ttl: Duration::from_secs(60), } }} #[derive(Debug, Clone, Default)]pub struct SpawnOptions { pub requester_driver: Option<String>, pub parent_pid: Option<u32>, pub env: BTreeMap<String, String>, pub cwd: Option<String>,} #[derive(Debug, Clone, Default, PartialEq, Eq)]pub struct VirtualProcessOptions { pub parent_pid: Option<u32>, pub env: BTreeMap<String, String>, pub cwd: Option<String>,} #[derive(Debug, Clone, Default, PartialEq, Eq)]pub struct ExecOptions { pub requester_driver: Option<String>, pub parent_pid: Option<u32>, pub env: BTreeMap<String, String>, pub cwd: Option<String>,} #[derive(Debug, Clone, PartialEq, Eq)]pub struct RecursiveDirEntry { pub path: String, pub is_directory: bool, pub is_symbolic_link: bool, pub size: u64,} #[derive(Debug, Clone, Default, PartialEq, Eq)]pub struct OpenShellOptions { pub requester_driver: Option<String>, pub command: Option<String>, pub args: Vec<String>, pub env: BTreeMap<String, String>, pub cwd: Option<String>,} #[derive(Debug, Clone, PartialEq, Eq)]pub struct WaitPidResult { pub pid: u32, pub status: i32,} #[derive(Debug, Clone, PartialEq, Eq)]pub struct WaitPidEventResult { pub pid: u32, pub status: i32, pub event: WaitPidEvent,} #[derive(Debug)]pub struct ReceivedFdMessage { pub payload: Vec<u8>, pub rights: Vec<ReceivedFdRight>, pub payload_truncated: bool, pub control_truncated: bool, pub full_length: usize,} #[derive(Clone)]pub enum FdTransferRequest { Fd(u32), Opaque(OpaqueTransferredRight),} impl fmt::Debug for FdTransferRequest { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Fd(fd) => f.debug_tuple("Fd").field(fd).finish(), Self::Opaque(resource) => f .debug_tuple("Opaque") .field(&(Arc::as_ptr(resource) as *const ())) .finish(), } }} pub enum ReceivedFdRight { Fd(u32), Opaque(OpaqueTransferredRight),} impl fmt::Debug for ReceivedFdRight { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Fd(fd) => f.debug_tuple("Fd").field(fd).finish(), Self::Opaque(resource) => f .debug_tuple("Opaque") .field(&(Arc::as_ptr(resource) as *const ())) .finish(), } }} #[derive(Debug, Clone, Copy, PartialEq, Eq)]pub struct ProcessFdSnapshotEntry { pub fd: u32, pub fd_flags: u32, pub status_flags: u32, pub filetype: u8, pub is_socket: bool, pub is_pipe: bool, pub is_pty: bool,} #[derive(Debug, Clone, PartialEq, Eq)]pub struct ProcessFdDirEntry { pub name: String, pub ino: u64, pub is_directory: bool, pub is_symbolic_link: bool,} /// The canonical VFS object selected by Linux-style AF_UNIX pathname lookup.////// `canonical_path` preserves the actual directory reached through symlinks/// while `stat` carries the `(dev, ino)` identity the sidecar must use to/// distinguish socket nodes across mounts.#[derive(Debug, Clone, PartialEq, Eq)]pub struct UnixSocketPathNode { pub canonical_path: String, pub stat: VirtualStat,} struct UnixSocketBindTarget { canonical_path: String, parent: UnixSocketPathNode, identity: ProcessIdentity,} #[derive(Debug, Clone)]struct FdSocketEntry { description: Arc<FileDescription>, socket_id: SocketId, mode: u32, uid: u32, gid: u32,} type FdSocketRegistry = Arc<Mutex<BTreeMap<u64, FdSocketEntry>>>; enum OpenFileRemovalBacking { Anonymous { descriptions: Vec<Arc<FileDescription>>, backing: SharedAnonymousFile, }, LinkedAlias { descriptions: Vec<Arc<FileDescription>>, live_path: String, },} #[derive(Debug, Clone)]struct ResolvedSpawnCommand { command: String, args: Vec<String>, driver: CommandDriver,} #[derive(Debug, Clone)]struct ShebangCommand { interpreter: String, args: Vec<String>,} #[derive(Clone)]pub struct KernelProcessHandle { pid: u32, driver: String, process: Arc<StubDriverProcess>,} impl fmt::Debug for KernelProcessHandle { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("KernelProcessHandle") .field("pid", &self.pid) .field("driver", &self.driver) .finish_non_exhaustive() }} impl KernelProcessHandle { pub fn pid(&self) -> u32 { self.pid } pub fn driver(&self) -> &str { &self.driver } pub fn finish(&self, exit_code: i32) { self.process.finish(exit_code); } pub fn kill(&self, signal: i32) { self.process.kill(signal); } pub fn wait(&self, timeout: Duration) -> Option<i32> { self.process.wait(timeout) } pub fn kill_signals(&self) -> Vec<i32> { self.process.kill_signals() }} #[derive(Debug, Clone)]pub struct OpenShellHandle { process: KernelProcessHandle, master_fd: u32, slave_fd: u32, pty_path: String,} impl OpenShellHandle { pub fn process(&self) -> &KernelProcessHandle { &self.process } pub fn pid(&self) -> u32 { self.process.pid() } pub fn master_fd(&self) -> u32 { self.master_fd } pub fn slave_fd(&self) -> u32 { self.slave_fd } pub fn pty_path(&self) -> &str { &self.pty_path }} pub struct KernelVm<F> { vm_id: String, boot_time_ms: u64, boot_instant: Instant, filesystem: PermissionedFileSystem<DeviceLayer<F>>, permissions: Permissions, loopback_exempt_ports: BTreeSet<u16>, dns: DnsConfig, dns_resolver: SharedDnsResolver, env: BTreeMap<String, String>, cwd: String, commands: CommandRegistry, fd_tables: Arc<Mutex<FdTableManager>>, processes: ProcessTable, pipes: PipeManager, ptys: PtyManager, sockets: SocketTable, fd_sockets: FdSocketRegistry, poll_notifier: PollNotifier, users: UserManager, resources: ResourceAccountant, filesystem_usage_cache: Option<FileSystemUsage>, anonymous_file_usage: Arc<AnonymousFileUsage>, file_locks: FileLockManager, unnamed_files: BTreeMap<u64, UnnamedFile>, next_unnamed_file_id: u64, driver_pids: Arc<Mutex<BTreeMap<String, BTreeSet<u32>>>>, terminated: bool,} const UNNAMED_FILE_PREFIX: &str = ".agentos-tmpfile-"; #[derive(Debug, Clone)]struct UnnamedFile { path: String, linkable: bool,} pub fn is_internal_unnamed_file_name(name: &str) -> bool { name.starts_with(UNNAMED_FILE_PREFIX)} // Cleanup spans every independently owned kernel resource table. Keeping the// tables explicit makes teardown ordering visible at the call sites.#[allow(clippy::too_many_arguments)]fn cleanup_process_resources( fd_tables: &Mutex<FdTableManager>, file_locks: &FileLockManager, pipes: &PipeManager, ptys: &PtyManager, sockets: &SocketTable, fd_sockets: &FdSocketRegistry, driver_pids: &Mutex<BTreeMap<String, BTreeSet<u32>>>, pid: u32,) { let mut cleanup = Vec::new(); { let mut tables = lock_or_recover(fd_tables); let descriptors = tables .get(pid) .map(|table| {Lines 1–500 of 10792. Load more
use crate::bridge::LifecycleState; use crate::command_registry::{CommandDriver, CommandRegistry}; use crate::device_layer::{create_device_layer, DeviceLayer}; use crate::dns::{ format_dns_resource, resolve_dns, resolve_dns_records, DnsConfig, DnsLookupPolicy, DnsRecordResolution, DnsResolution, DnsResolverErrorKind, HickoryDnsResolver, SharedDnsResolver, }; use crate::fd_table::{ AnonymousFile, AnonymousFileUsage, FdEntry, FdStat, FdTableError, FdTableManager, FileDescription, FileLockManager, FileLockTarget, FlockOperation, ProcessFdTable, RecordLock, RecordLockType, SharedAnonymousFile, TransferredFd, FD_CLOEXEC, FILETYPE_CHARACTER_DEVICE, FILETYPE_DIRECTORY, FILETYPE_PIPE, FILETYPE_REGULAR_FILE, FILETYPE_SOCKET_DGRAM, FILETYPE_SOCKET_STREAM, FILETYPE_SYMBOLIC_LINK, F_DUPFD, O_APPEND, O_CREAT, O_DIRECT, O_DIRECTORY, O_EXCL, O_NOFOLLOW, O_NONBLOCK, O_RDONLY, O_RDWR, O_TRUNC, O_WRONLY, }; use crate::mount_table::{MountEntry, MountOptions, MountTable, MountedFileSystem}; use crate::network_policy::format_tcp_resource; use crate::permissions::{ check_command_execution, check_network_access, FsOperation, NetworkOperation, PermissionError, PermissionedFileSystem, Permissions, }; use crate::pipe_manager::{PipeError, PipeManager}; use crate::poll::{ PollEvents, PollFd, PollNotifier, PollResult, PollTarget, PollTargetEntry, PollTargetResult, POLLERR, POLLHUP, POLLIN, POLLNVAL, POLLOUT, }; use crate::process_table::{ DriverProcess, ProcessContext, ProcessExitCallback, ProcessInfo, ProcessStatus, ProcessTable, ProcessTableError, ProcessWaitResult, SigmaskHow, SignalSet, DEFAULT_PROCESS_UMASK, SIGCONT, SIGPIPE, SIGSTOP, SIGTSTP, SIGWINCH, }; use crate::pty::{ LineDisciplineConfig, PartialTermios, PtyError, PtyManager, PtyWindowSize, Termios, }; use crate::resource_accounting::{ measure_filesystem_usage, FileSystemStats, FileSystemUsage, ResourceAccountant, ResourceError, ResourceLimits, ResourceSnapshot, DEFAULT_MAX_OPEN_FDS, }; use crate::root_fs::{ encode_snapshot, RootFileSystem, RootFilesystemError, RootFilesystemSnapshot, }; use crate::socket_table::{ DatagramSocketOption, InetSocketAddress, OpaqueTransferredRight, ReceivedDatagram, SocketId, SocketMulticastMembership, SocketReadiness, SocketRecord, SocketShutdown, SocketSpec, SocketState, SocketTable, SocketTableError, SocketType, TransferredSocketRight, }; use crate::user::{ProcessIdentity, UserConfig, UserManager}; use crate::vfs::{ normalize_path, VfsError, VfsResult, VirtualDirEntry, VirtualFileSystem, VirtualStat, VirtualTimeSpec, VirtualUtimeSpec, MAX_PATH_LENGTH, RENAME_EXCHANGE, S_IFDIR, S_IFLNK, S_IFREG, }; use hickory_proto::rr::RecordType; use std::any::Any; use std::collections::{BTreeMap, BTreeSet, VecDeque}; use std::error::Error; use std::fmt; #[cfg(test)] use std::sync::OnceLock; use std::sync::{Arc, Condvar, Mutex, MutexGuard, WaitTimeoutResult}; use std::time::Duration; use web_time::{Instant, SystemTime, UNIX_EPOCH}; pub type KernelResult<T> = Result<T, KernelError>; pub use crate::process_table::{ProcessWaitEvent as WaitPidEvent, WaitPidFlags}; pub const SEEK_SET: u8 = 0; pub const SEEK_CUR: u8 = 1; pub const SEEK_END: u8 = 2; const EXECUTABLE_PERMISSION_BITS: u32 = 0o111; const SHEBANG_LINE_MAX_BYTES: usize = 256; const MAX_EXEC_INTERPRETER_DEPTH: usize = 4; const MAX_UNIX_SOCKET_SYMLINKS: usize = 40; const UNIX_SOCKET_FILE_TYPE: u32 = 0o140000; const UNIX_DAC_WRITE: u32 = 0o2; const UNIX_DAC_SEARCH: u32 = 0o1; #[derive(Debug, Clone, PartialEq, Eq)] pub struct KernelError { code: &'static str, message: String, } impl KernelError { pub fn code(&self) -> &'static str { self.code } fn new(code: &'static str, message: impl Into<String>) -> Self { Self { code, message: message.into(), } } fn disposed() -> Self { Self::new("EINVAL", "kernel VM is disposed") } fn no_such_process(pid: u32) -> Self { Self::new("ESRCH", format!("no such process {pid}")) } fn bad_file_descriptor(fd: u32) -> Self { Self::new("EBADF", format!("bad file descriptor {fd}")) } fn permission_denied(message: impl Into<String>) -> Self { Self::new("EPERM", message) } fn command_not_found(command: &str) -> Self { Self::new("ENOENT", format!("command not found: {command}")) } } impl fmt::Display for KernelError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}: {}", self.code, self.message) } } impl Error for KernelError {} fn linux_shebang_interpreter(header: &[u8], path: &str) -> KernelResult<Option<String>> { if !header.starts_with(b"#!") { return Ok(None); } let payload = &header[2..]; let newline = payload.iter().position(|byte| *byte == b'\n'); let line = newline.map_or(payload, |index| &payload[..index]); let line_end = line .iter() .rposition(|byte| !matches!(*byte, b' ' | b'\t')) .map(|index| index + 1) .ok_or_else(|| KernelError::new("ENOEXEC", format!("invalid shebang line: {path}")))?; let line = &line[..line_end]; let interpreter_start = line .iter() .position(|byte| !matches!(*byte, b' ' | b'\t')) .ok_or_else(|| KernelError::new("ENOEXEC", format!("invalid shebang line: {path}")))?; let interpreter_tail = &line[interpreter_start..]; let separator = interpreter_tail .iter() .position(|byte| matches!(*byte, b' ' | b'\t')); // Linux accepts a truncated optional argument, but it rejects a shebang // whose interpreter pathname itself reaches the end of BINPRM_BUF_SIZE. if newline.is_none() && header.len() >= SHEBANG_LINE_MAX_BYTES && separator.is_none() { return Err(KernelError::new( "ENOEXEC", format!("shebang interpreter path exceeds the Linux header limit: {path}"), )); } let interpreter_end = separator.unwrap_or(interpreter_tail.len()); let interpreter = std::str::from_utf8(&interpreter_tail[..interpreter_end]) .map_err(|_| KernelError::new("ENOEXEC", format!("invalid shebang line: {path}")))?; if interpreter.is_empty() { return Err(KernelError::new( "ENOEXEC", format!("invalid shebang line: {path}"), )); } Ok(Some(interpreter.to_owned())) } #[derive(Clone)] pub struct KernelVmConfig { pub vm_id: String, pub env: BTreeMap<String, String>, pub cwd: String, pub user: UserConfig, pub permissions: Permissions, pub loopback_exempt_ports: BTreeSet<u16>, pub dns: DnsConfig, pub dns_resolver: SharedDnsResolver, pub resources: ResourceLimits, pub zombie_ttl: Duration, } impl KernelVmConfig { pub fn new(vm_id: impl Into<String>) -> Self { Self { vm_id: vm_id.into(), env: BTreeMap::new(), cwd: String::from("/workspace"), user: UserConfig::default(), permissions: Permissions::default(), loopback_exempt_ports: BTreeSet::new(), dns: DnsConfig::default(), dns_resolver: Arc::new(HickoryDnsResolver::default()), resources: ResourceLimits::default(), zombie_ttl: Duration::from_secs(60), } } } #[derive(Debug, Clone, Default)] pub struct SpawnOptions { pub requester_driver: Option<String>, pub parent_pid: Option<u32>, pub env: BTreeMap<String, String>, pub cwd: Option<String>, } #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct VirtualProcessOptions { pub parent_pid: Option<u32>, pub env: BTreeMap<String, String>, pub cwd: Option<String>, } #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct ExecOptions { pub requester_driver: Option<String>, pub parent_pid: Option<u32>, pub env: BTreeMap<String, String>, pub cwd: Option<String>, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct RecursiveDirEntry { pub path: String, pub is_directory: bool, pub is_symbolic_link: bool, pub size: u64, } #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct OpenShellOptions { pub requester_driver: Option<String>, pub command: Option<String>, pub args: Vec<String>, pub env: BTreeMap<String, String>, pub cwd: Option<String>, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct WaitPidResult { pub pid: u32, pub status: i32, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct WaitPidEventResult { pub pid: u32, pub status: i32, pub event: WaitPidEvent, } #[derive(Debug)] pub struct ReceivedFdMessage { pub payload: Vec<u8>, pub rights: Vec<ReceivedFdRight>, pub payload_truncated: bool, pub control_truncated: bool, pub full_length: usize, } #[derive(Clone)] pub enum FdTransferRequest { Fd(u32), Opaque(OpaqueTransferredRight), } impl fmt::Debug for FdTransferRequest { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Fd(fd) => f.debug_tuple("Fd").field(fd).finish(), Self::Opaque(resource) => f .debug_tuple("Opaque") .field(&(Arc::as_ptr(resource) as *const ())) .finish(), } } } pub enum ReceivedFdRight { Fd(u32), Opaque(OpaqueTransferredRight), } impl fmt::Debug for ReceivedFdRight { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Fd(fd) => f.debug_tuple("Fd").field(fd).finish(), Self::Opaque(resource) => f .debug_tuple("Opaque") .field(&(Arc::as_ptr(resource) as *const ())) .finish(), } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ProcessFdSnapshotEntry { pub fd: u32, pub fd_flags: u32, pub status_flags: u32, pub filetype: u8, pub is_socket: bool, pub is_pipe: bool, pub is_pty: bool, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProcessFdDirEntry { pub name: String, pub ino: u64, pub is_directory: bool, pub is_symbolic_link: bool, } /// The canonical VFS object selected by Linux-style AF_UNIX pathname lookup. /// /// `canonical_path` preserves the actual directory reached through symlinks /// while `stat` carries the `(dev, ino)` identity the sidecar must use to /// distinguish socket nodes across mounts. #[derive(Debug, Clone, PartialEq, Eq)] pub struct UnixSocketPathNode { pub canonical_path: String, pub stat: VirtualStat, } struct UnixSocketBindTarget { canonical_path: String, parent: UnixSocketPathNode, identity: ProcessIdentity, } #[derive(Debug, Clone)] struct FdSocketEntry { description: Arc<FileDescription>, socket_id: SocketId, mode: u32, uid: u32, gid: u32, } type FdSocketRegistry = Arc<Mutex<BTreeMap<u64, FdSocketEntry>>>; enum OpenFileRemovalBacking { Anonymous { descriptions: Vec<Arc<FileDescription>>, backing: SharedAnonymousFile, }, LinkedAlias { descriptions: Vec<Arc<FileDescription>>, live_path: String, }, } #[derive(Debug, Clone)] struct ResolvedSpawnCommand { command: String, args: Vec<String>, driver: CommandDriver, } #[derive(Debug, Clone)] struct ShebangCommand { interpreter: String, args: Vec<String>, } #[derive(Clone)] pub struct KernelProcessHandle { pid: u32, driver: String, process: Arc<StubDriverProcess>, } impl fmt::Debug for KernelProcessHandle { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("KernelProcessHandle") .field("pid", &self.pid) .field("driver", &self.driver) .finish_non_exhaustive() } } impl KernelProcessHandle { pub fn pid(&self) -> u32 { self.pid } pub fn driver(&self) -> &str { &self.driver } pub fn finish(&self, exit_code: i32) { self.process.finish(exit_code); } pub fn kill(&self, signal: i32) { self.process.kill(signal); } pub fn wait(&self, timeout: Duration) -> Option<i32> { self.process.wait(timeout) } pub fn kill_signals(&self) -> Vec<i32> { self.process.kill_signals() } } #[derive(Debug, Clone)] pub struct OpenShellHandle { process: KernelProcessHandle, master_fd: u32, slave_fd: u32, pty_path: String, } impl OpenShellHandle { pub fn process(&self) -> &KernelProcessHandle { &self.process } pub fn pid(&self) -> u32 { self.process.pid() } pub fn master_fd(&self) -> u32 { self.master_fd } pub fn slave_fd(&self) -> u32 { self.slave_fd } pub fn pty_path(&self) -> &str { &self.pty_path } } pub struct KernelVm<F> { vm_id: String, boot_time_ms: u64, boot_instant: Instant, filesystem: PermissionedFileSystem<DeviceLayer<F>>, permissions: Permissions, loopback_exempt_ports: BTreeSet<u16>, dns: DnsConfig, dns_resolver: SharedDnsResolver, env: BTreeMap<String, String>, cwd: String, commands: CommandRegistry, fd_tables: Arc<Mutex<FdTableManager>>, processes: ProcessTable, pipes: PipeManager, ptys: PtyManager, sockets: SocketTable, fd_sockets: FdSocketRegistry, poll_notifier: PollNotifier, users: UserManager, resources: ResourceAccountant, filesystem_usage_cache: Option<FileSystemUsage>, anonymous_file_usage: Arc<AnonymousFileUsage>, file_locks: FileLockManager, unnamed_files: BTreeMap<u64, UnnamedFile>, next_unnamed_file_id: u64, driver_pids: Arc<Mutex<BTreeMap<String, BTreeSet<u32>>>>, terminated: bool, } const UNNAMED_FILE_PREFIX: &str = ".agentos-tmpfile-"; #[derive(Debug, Clone)] struct UnnamedFile { path: String, linkable: bool, } pub fn is_internal_unnamed_file_name(name: &str) -> bool { name.starts_with(UNNAMED_FILE_PREFIX) } // Cleanup spans every independently owned kernel resource table. Keeping the // tables explicit makes teardown ordering visible at the call sites. #[allow(clippy::too_many_arguments)] fn cleanup_process_resources( fd_tables: &Mutex<FdTableManager>, file_locks: &FileLockManager, pipes: &PipeManager, ptys: &PtyManager, sockets: &SocketTable, fd_sockets: &FdSocketRegistry, driver_pids: &Mutex<BTreeMap<String, BTreeSet<u32>>>, pid: u32, ) { let mut cleanup = Vec::new(); { let mut tables = lock_or_recover(fd_tables); let descriptors = tables .get(pid) .map(|table| {