10792 lines · 370.5 KB · Rust
RawDownload
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.