Skip to main content

secure_exec_sidecar/
execution.rs

1//! Process execution, networking, and runtime event handling extracted from service.rs.
2
3use secure_exec_vm_config as vm_config;
4
5use crate::filesystem::{
6    handle_python_vfs_rpc_request as filesystem_handle_python_vfs_rpc_request,
7    service_javascript_fs_sync_rpc, service_javascript_module_sync_rpc,
8};
9use crate::protocol::{
10    BoundUdpSnapshotResponse, CloseStdinRequest, EventFrame, EventPayload, ExecuteRequest,
11    FindBoundUdpRequest, FindListenerRequest, GetProcessSnapshotRequest, GetSignalStateRequest,
12    GetZombieTimerCountRequest, GuestRuntimeKind, JavascriptChildProcessSpawnOptions,
13    JavascriptChildProcessSpawnRequest, JavascriptDgramBindRequest,
14    JavascriptDgramCreateSocketRequest, JavascriptDgramSendRequest, JavascriptDnsLookupRequest,
15    JavascriptDnsResolveRequest, JavascriptNetConnectRequest, JavascriptNetListenRequest,
16    JavascriptNetReserveTcpPortRequest, KillProcessRequest, ListenerSnapshotResponse,
17    OwnershipScope, ProcessExitedEvent, ProcessKilledResponse, ProcessOutputEvent,
18    ProcessSnapshotEntry, ProcessSnapshotResponse, ProcessSnapshotStatus, ProcessStartedResponse,
19    RequestFrame, ResponseFrame, ResponsePayload, SidecarRequestPayload, SignalDispositionAction,
20    SignalHandlerRegistration, SignalStateResponse, SocketStateEntry, StdinClosedResponse,
21    StdinWrittenResponse, StreamChannel, VmFetchRequest, VmFetchResponse, WasmPermissionTier,
22    WriteStdinRequest, ZombieTimerCountResponse,
23};
24use crate::service::{
25    audit_fields, dirname, emit_security_audit_event, emit_structured_event, javascript_error,
26    kernel_error, log_stale_process_event, normalize_host_path, normalize_path,
27    parse_javascript_child_process_spawn_request, path_is_within_root,
28    process_event_queue_overflow_error, python_error, wasm_error, MAX_PROCESS_EVENT_QUEUE,
29};
30use crate::state::{
31    ActiveCipherSession, ActiveDhSession, ActiveDiffieHellmanSession, ActiveEcdhSession,
32    ActiveExecution, ActiveExecutionEvent, ActiveHttp2Server, ActiveHttp2Session,
33    ActiveHttp2Stream, ActiveHttpServer, ActiveMappedHostFd, ActiveProcess, ActiveSqliteDatabase,
34    ActiveSqliteStatement, ActiveTcpListener, ActiveTcpSocket, ActiveTlsState, ActiveTlsStream,
35    ActiveUdpSocket, ActiveUnixListener, ActiveUnixSocket, BridgeError, ExitedProcessSnapshot,
36    Http2BridgeEvent, Http2RuntimeSnapshot, Http2SessionCommand, Http2SessionSnapshot,
37    Http2SocketSnapshot, JavascriptSocketFamily, JavascriptSocketPathContext,
38    JavascriptTcpListenerEvent, JavascriptTcpSocketEvent, JavascriptTlsBridgeOptions,
39    JavascriptTlsClientHello, JavascriptTlsDataValue, JavascriptTlsMaterial, JavascriptUdpFamily,
40    JavascriptUdpSocketEvent, JavascriptUnixListenerEvent, NetworkResourceCounts, PendingTcpSocket,
41    PendingUnixSocket, ProcNetEntry, ProcessEventEnvelope, ResolvedChildProcessExecution,
42    ResolvedTcpConnectAddr, SharedBridge, SharedSidecarRequestClient, SidecarKernel,
43    SocketQueryKind, ToolExecution, VmDnsConfig, VmListenPolicy, VmState,
44    DEFAULT_JAVASCRIPT_NET_BACKLOG, EXECUTION_DRIVER_NAME, EXECUTION_SANDBOX_ROOT_ENV,
45    JAVASCRIPT_COMMAND, LOOPBACK_EXEMPT_PORTS_ENV, MAPPED_HOST_FD_START, PYTHON_COMMAND,
46    TOOL_DRIVER_NAME, VM_LISTEN_ALLOW_PRIVILEGED_METADATA_KEY, WASM_COMMAND,
47    WASM_STDIO_SYNC_RPC_ENV,
48};
49use crate::tools::{
50    format_tool_failure_output, is_tool_command, normalized_tool_command_name,
51    resolve_tool_command, ToolCommandResolution,
52};
53use crate::wire::{ProtocolFrame as WireProtocolFrame, WireFrameCodec, DEFAULT_MAX_FRAME_BYTES};
54use crate::{DispatchResult, NativeSidecar, NativeSidecarBridge, SidecarError};
55
56use base64::Engine;
57use bytes::Bytes;
58use h2::{client, server, Reason};
59use hickory_resolver::proto::rr::{RData, Record, RecordType};
60use hmac::{Hmac, Mac};
61use http::{HeaderMap, HeaderName, HeaderValue, Method, Request, Response, Uri};
62use md5::Md5;
63use nix::libc;
64use nix::sys::signal::{kill as send_signal, Signal};
65use nix::sys::wait::{waitid as wait_on_child, Id as WaitId, WaitPidFlag, WaitStatus};
66use nix::unistd::Pid;
67use openssl::bn::{BigNum, BigNumContext};
68use openssl::derive::Deriver;
69use openssl::dh::Dh;
70use openssl::ec::{EcGroup, EcKey, EcPoint, PointConversionForm};
71use openssl::hash::MessageDigest;
72use openssl::nid::Nid;
73use openssl::pkey::{Id as PKeyId, PKey, Params, Private, Public};
74use openssl::rand::rand_bytes;
75use openssl::rsa::{Padding, Rsa};
76use openssl::sign::{Signer, Verifier};
77use openssl::symm::{Cipher, Crypter, Mode};
78use pbkdf2::pbkdf2_hmac;
79use rusqlite::types::ValueRef as SqliteValueRef;
80use rusqlite::{
81    Connection as SqliteConnection, OpenFlags as SqliteOpenFlags, Statement as SqliteStatement,
82};
83use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
84use rustls::crypto::aws_lc_rs;
85use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName};
86use rustls::{
87    ClientConfig, ClientConnection, DigitallySignedStruct, RootCertStore, ServerConfig,
88    ServerConnection, SignatureScheme,
89};
90use scrypt::{scrypt, Params as ScryptParams};
91use secure_exec_bridge::LifecycleState;
92use secure_exec_execution::wasm::{
93    WasmExecutionError, WASM_MAX_FUEL_ENV, WASM_MAX_MEMORY_BYTES_ENV, WASM_MAX_STACK_BYTES_ENV,
94};
95use secure_exec_execution::{
96    javascript::handle_internal_bridge_call_from_host_context, v8_host::V8SessionHandle,
97    v8_runtime, CreateJavascriptContextRequest, CreatePythonContextRequest,
98    CreateWasmContextRequest, JavascriptExecutionEvent, JavascriptSyncRpcRequest, ModuleFsReader,
99    NodeSignalDispositionAction, NodeSignalHandlerRegistration, PythonExecutionEvent,
100    PythonVfsRpcMethod, PythonVfsRpcRequest, PythonVfsRpcResponsePayload,
101    StartJavascriptExecutionRequest, StartPythonExecutionRequest, StartWasmExecutionRequest,
102    WasmExecutionEvent, WasmPermissionTier as ExecutionWasmPermissionTier,
103};
104use secure_exec_kernel::dns::{
105    DnsLookupPolicy, DnsRecordResolution, DnsResolutionSource as KernelDnsResolutionSource,
106};
107use secure_exec_kernel::kernel::{KernelProcessHandle, SpawnOptions, VirtualProcessOptions};
108use secure_exec_kernel::permissions::NetworkOperation;
109use secure_exec_kernel::poll::{PollEvents, PollFd, PollTargetEntry, POLLERR, POLLHUP, POLLIN};
110use secure_exec_kernel::process_table::{ProcessStatus, WaitPidFlags, SIGKILL, SIGTERM};
111use secure_exec_kernel::pty::LineDisciplineConfig;
112use secure_exec_kernel::resource_accounting::ResourceLimits;
113use secure_exec_kernel::root_fs::RootFilesystemMode;
114use secure_exec_kernel::socket_table::{
115    InetSocketAddress, SocketDomain, SocketId, SocketShutdown as KernelSocketShutdown, SocketSpec,
116    SocketState, SocketType,
117};
118use serde::{Deserialize, Serialize};
119use serde_json::{json, Map, Value};
120use sha1::Sha1;
121use sha2::{digest::Digest, Sha256, Sha512};
122use socket2::{SockRef, TcpKeepalive};
123use std::collections::VecDeque;
124use std::collections::{BTreeMap, BTreeSet};
125use std::fmt;
126use std::fs;
127use std::io::{Cursor, Read, Write};
128use std::net::{
129    IpAddr, Ipv4Addr, Ipv6Addr, Shutdown, SocketAddr, TcpListener, TcpStream, ToSocketAddrs,
130    UdpSocket,
131};
132use std::os::unix::fs::{MetadataExt, PermissionsExt};
133use std::os::unix::net::{SocketAddr as UnixSocketAddr, UnixListener, UnixStream};
134use std::path::{Path, PathBuf};
135use std::pin::Pin;
136use std::sync::atomic::{AtomicBool, Ordering};
137use std::sync::mpsc::{self, RecvTimeoutError, Sender};
138use std::sync::{Arc, Mutex, OnceLock, Weak};
139use std::thread;
140use std::time::{Duration, Instant};
141use tokio::io::{AsyncRead, AsyncWrite};
142use tokio::runtime::Builder as TokioRuntimeBuilder;
143use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver};
144use tokio_rustls::{TlsAcceptor, TlsConnector};
145use url::Url;
146
147const DEFAULT_KERNEL_STDIN_READ_MAX_BYTES: usize = 64 * 1024;
148const DEFAULT_KERNEL_STDIN_READ_TIMEOUT_MS: u64 = 100;
149const JAVASCRIPT_NET_TIMEOUT_SENTINEL: &str = "__secure_exec_net_timeout__";
150const PYTHON_PYODIDE_GUEST_ROOT: &str = "/__agent_os_pyodide";
151const PYTHON_PYODIDE_CACHE_GUEST_ROOT: &str = "/__agent_os_pyodide_cache";
152const TCP_SOCKET_POLL_TIMEOUT: Duration = Duration::from_millis(100);
153const TLS_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(5);
154const HTTP_LOOPBACK_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
155pub(crate) const MAX_PER_PROCESS_STATE_HANDLES: usize = 1024;
156const VM_FETCH_BUFFER_LIMIT_BYTES: usize = DEFAULT_MAX_FRAME_BYTES;
157const DEFAULT_SCRYPT_COST: u64 = 16_384;
158const DEFAULT_SCRYPT_BLOCK_SIZE: u32 = 8;
159const DEFAULT_SCRYPT_PARALLELIZATION: u32 = 1;
160const SQLITE_JS_SAFE_INTEGER_MAX: i64 = 9_007_199_254_740_991;
161
162trait Http2AsyncIo: AsyncRead + AsyncWrite + Unpin + Send {}
163
164impl<T> Http2AsyncIo for T where T: AsyncRead + AsyncWrite + Unpin + Send {}
165
166const DEFAULT_ALLOWED_NODE_BUILTINS: &[&str] = &[
167    "assert",
168    "buffer",
169    "console",
170    "child_process",
171    "crypto",
172    "dns",
173    "events",
174    "fs",
175    "http",
176    "http2",
177    "https",
178    "module",
179    "os",
180    "path",
181    "perf_hooks",
182    "querystring",
183    "sqlite",
184    "stream",
185    "string_decoder",
186    "timers",
187    "tls",
188    "tty",
189    "url",
190    "util",
191    "zlib",
192];
193
194#[derive(Debug, Clone, Copy, PartialEq, Eq)]
195enum JavascriptCryptoDigestAlgorithm {
196    Md5,
197    Sha1,
198    Sha256,
199    Sha512,
200}
201
202#[derive(Debug, Default, Deserialize)]
203#[serde(default, rename_all = "camelCase")]
204struct JavascriptScryptOptions {
205    #[serde(alias = "N")]
206    cost: Option<u64>,
207    #[serde(alias = "r")]
208    block_size: Option<u32>,
209    #[serde(alias = "p")]
210    parallelization: Option<u32>,
211}
212
213#[derive(Debug, Deserialize)]
214#[serde(rename_all = "camelCase")]
215struct JavascriptHttpListenRequest {
216    server_id: u64,
217    #[serde(default)]
218    port: Option<u16>,
219    #[serde(default)]
220    hostname: Option<String>,
221}
222
223#[derive(Debug, Default, Deserialize)]
224#[serde(default, rename_all = "camelCase")]
225struct JavascriptHttpRequestOptions {
226    method: Option<String>,
227    headers: BTreeMap<String, Value>,
228    body: Option<String>,
229    reject_unauthorized: Option<bool>,
230}
231
232#[derive(Debug, Default, Deserialize)]
233#[serde(default, rename_all = "camelCase")]
234struct JavascriptHttp2ServerListenRequest {
235    server_id: u64,
236    secure: bool,
237    port: Option<u16>,
238    host: Option<String>,
239    backlog: Option<u32>,
240    timeout: Option<u64>,
241    settings: BTreeMap<String, Value>,
242    tls: Option<JavascriptTlsBridgeOptions>,
243}
244
245#[derive(Debug, Default, Deserialize)]
246#[serde(default, rename_all = "camelCase")]
247struct JavascriptHttp2SessionConnectRequest {
248    authority: Option<String>,
249    protocol: Option<String>,
250    host: Option<String>,
251    port: Option<u16>,
252    settings: BTreeMap<String, Value>,
253    tls: Option<JavascriptTlsBridgeOptions>,
254}
255
256#[derive(Debug, Default, Deserialize)]
257#[serde(default, rename_all = "camelCase")]
258struct JavascriptHttp2RequestOptions {
259    end_stream: bool,
260}
261
262#[derive(Debug, Default, Deserialize)]
263#[serde(default, rename_all = "camelCase")]
264struct JavascriptHttp2FileResponseOptions {
265    offset: Option<u64>,
266    length: Option<i64>,
267}
268
269#[derive(Debug, Clone)]
270struct HttpHeaderCollection {
271    normalized: BTreeMap<String, Vec<String>>,
272    raw_pairs: Vec<(String, String)>,
273}
274
275#[derive(Debug)]
276struct InsecureTlsVerifier {
277    supported_schemes: Vec<SignatureScheme>,
278}
279
280impl ServerCertVerifier for InsecureTlsVerifier {
281    fn verify_server_cert(
282        &self,
283        _end_entity: &CertificateDer<'_>,
284        _intermediates: &[CertificateDer<'_>],
285        _server_name: &ServerName<'_>,
286        _ocsp_response: &[u8],
287        _now: rustls::pki_types::UnixTime,
288    ) -> Result<ServerCertVerified, rustls::Error> {
289        Ok(ServerCertVerified::assertion())
290    }
291
292    fn verify_tls12_signature(
293        &self,
294        _message: &[u8],
295        _cert: &CertificateDer<'_>,
296        _dss: &DigitallySignedStruct,
297    ) -> Result<HandshakeSignatureValid, rustls::Error> {
298        Ok(HandshakeSignatureValid::assertion())
299    }
300
301    fn verify_tls13_signature(
302        &self,
303        _message: &[u8],
304        _cert: &CertificateDer<'_>,
305        _dss: &DigitallySignedStruct,
306    ) -> Result<HandshakeSignatureValid, rustls::Error> {
307        Ok(HandshakeSignatureValid::assertion())
308    }
309
310    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
311        self.supported_schemes.clone()
312    }
313}
314
315impl ActiveProcess {
316    pub(crate) fn new(
317        kernel_pid: u32,
318        kernel_handle: KernelProcessHandle,
319        runtime: GuestRuntimeKind,
320        execution: ActiveExecution,
321    ) -> Self {
322        Self {
323            kernel_pid,
324            kernel_handle,
325            kernel_stdin_writer_fd: None,
326            runtime,
327            detached: false,
328            execution,
329            guest_cwd: String::from("/"),
330            env: BTreeMap::new(),
331            host_cwd: PathBuf::from("/"),
332            mapped_host_fds: BTreeMap::new(),
333            next_mapped_host_fd: MAPPED_HOST_FD_START,
334            pending_execution_events: VecDeque::new(),
335            pending_self_signal_exit: None,
336            child_processes: BTreeMap::new(),
337            next_child_process_id: 0,
338            http_servers: BTreeMap::new(),
339            pending_http_requests: BTreeMap::new(),
340            http2: Default::default(),
341            tcp_listeners: BTreeMap::new(),
342            next_tcp_listener_id: 0,
343            tcp_sockets: BTreeMap::new(),
344            next_tcp_socket_id: 0,
345            tcp_port_reservations: BTreeMap::new(),
346            next_tcp_port_reservation_id: 0,
347            unix_listeners: BTreeMap::new(),
348            next_unix_listener_id: 0,
349            unix_sockets: BTreeMap::new(),
350            next_unix_socket_id: 0,
351            udp_sockets: BTreeMap::new(),
352            next_udp_socket_id: 0,
353            cipher_sessions: BTreeMap::new(),
354            next_cipher_session_id: 0,
355            diffie_hellman_sessions: BTreeMap::new(),
356            next_diffie_hellman_session_id: 0,
357            sqlite_databases: BTreeMap::new(),
358            next_sqlite_database_id: 0,
359            sqlite_statements: BTreeMap::new(),
360            next_sqlite_statement_id: 0,
361            module_resolution_cache: secure_exec_execution::LocalModuleResolutionCache::default(),
362        }
363    }
364
365    pub(crate) fn queue_pending_execution_event(
366        &mut self,
367        event: ActiveExecutionEvent,
368    ) -> Result<(), SidecarError> {
369        if self.pending_execution_events.len() >= MAX_PROCESS_EVENT_QUEUE {
370            return Err(process_event_queue_overflow_error());
371        }
372        self.pending_execution_events.push_back(event);
373        Ok(())
374    }
375
376    pub(crate) fn with_host_cwd(mut self, host_cwd: PathBuf) -> Self {
377        self.host_cwd = host_cwd;
378        self
379    }
380
381    pub(crate) fn with_guest_cwd(mut self, guest_cwd: String) -> Self {
382        self.guest_cwd = guest_cwd;
383        self
384    }
385
386    pub(crate) fn with_env(mut self, env: BTreeMap<String, String>) -> Self {
387        self.env = env;
388        self
389    }
390
391    pub(crate) fn with_kernel_stdin_writer_fd(mut self, fd: u32) -> Self {
392        self.kernel_stdin_writer_fd = Some(fd);
393        self
394    }
395
396    pub(crate) fn with_detached(mut self, detached: bool) -> Self {
397        self.detached = detached;
398        self
399    }
400
401    pub(crate) fn allocate_mapped_host_fd(&mut self, fd: ActiveMappedHostFd) -> u32 {
402        let handle = self.next_mapped_host_fd;
403        self.next_mapped_host_fd = self
404            .next_mapped_host_fd
405            .checked_add(1)
406            .unwrap_or(MAPPED_HOST_FD_START);
407        self.mapped_host_fds.insert(handle, fd);
408        handle
409    }
410
411    pub(crate) fn mapped_host_fd(&self, fd: u32) -> Option<&ActiveMappedHostFd> {
412        self.mapped_host_fds.get(&fd)
413    }
414
415    pub(crate) fn mapped_host_fd_mut(&mut self, fd: u32) -> Option<&mut ActiveMappedHostFd> {
416        self.mapped_host_fds.get_mut(&fd)
417    }
418
419    pub(crate) fn close_mapped_host_fd(&mut self, fd: u32) -> bool {
420        self.mapped_host_fds.remove(&fd).is_some()
421    }
422
423    pub(crate) fn allocate_child_process_id(&mut self) -> String {
424        self.next_child_process_id += 1;
425        format!("child-{}", self.next_child_process_id)
426    }
427
428    fn allocate_tcp_listener_id(&mut self) -> String {
429        self.next_tcp_listener_id += 1;
430        format!("listener-{}", self.next_tcp_listener_id)
431    }
432
433    fn allocate_tcp_socket_id(&mut self) -> String {
434        self.next_tcp_socket_id += 1;
435        format!("socket-{}", self.next_tcp_socket_id)
436    }
437
438    fn allocate_tcp_port_reservation_id(&mut self) -> String {
439        self.next_tcp_port_reservation_id += 1;
440        format!("tcp-port-reservation-{}", self.next_tcp_port_reservation_id)
441    }
442
443    fn allocate_unix_listener_id(&mut self) -> String {
444        self.next_unix_listener_id += 1;
445        format!("unix-listener-{}", self.next_unix_listener_id)
446    }
447
448    fn allocate_unix_socket_id(&mut self) -> String {
449        self.next_unix_socket_id += 1;
450        format!("unix-socket-{}", self.next_unix_socket_id)
451    }
452
453    fn allocate_udp_socket_id(&mut self) -> String {
454        self.next_udp_socket_id += 1;
455        format!("udp-socket-{}", self.next_udp_socket_id)
456    }
457
458    pub(crate) fn network_resource_counts(&self) -> NetworkResourceCounts {
459        let mut counts = NetworkResourceCounts {
460            sockets: self.http_servers.len()
461                + self.tcp_listeners.len()
462                + self.tcp_sockets.len()
463                + self.unix_listeners.len()
464                + self.unix_sockets.len()
465                + self.udp_sockets.len(),
466            connections: self.tcp_sockets.len() + self.unix_sockets.len(),
467        };
468        if let Ok(http2) = self.http2.shared.lock() {
469            counts.sockets += http2.servers.len() + http2.sessions.len();
470            counts.connections += http2.sessions.len();
471        }
472
473        for child in self.child_processes.values() {
474            let child_counts = child.network_resource_counts();
475            counts.sockets += child_counts.sockets;
476            counts.connections += child_counts.connections;
477        }
478
479        counts
480    }
481
482    fn sidecar_only_network_resource_counts(&self) -> NetworkResourceCounts {
483        let mut counts = NetworkResourceCounts {
484            sockets: self.http_servers.len()
485                + self
486                    .tcp_listeners
487                    .values()
488                    .filter(|listener| listener.kernel_socket_id.is_none())
489                    .count()
490                + self
491                    .tcp_sockets
492                    .values()
493                    .filter(|socket| socket.kernel_socket_id.is_none())
494                    .count()
495                + self.unix_listeners.len()
496                + self.unix_sockets.len()
497                + self
498                    .udp_sockets
499                    .values()
500                    .filter(|socket| socket.kernel_socket_id.is_none())
501                    .count(),
502            connections: self
503                .tcp_sockets
504                .values()
505                .filter(|socket| socket.kernel_socket_id.is_none())
506                .count()
507                + self.unix_sockets.len(),
508        };
509        if let Ok(http2) = self.http2.shared.lock() {
510            counts.sockets += http2.servers.len() + http2.sessions.len();
511            counts.connections += http2.sessions.len();
512        }
513
514        for child in self.child_processes.values() {
515            let child_counts = child.sidecar_only_network_resource_counts();
516            counts.sockets += child_counts.sockets;
517            counts.connections += child_counts.connections;
518        }
519
520        counts
521    }
522}
523
524fn poll_tool_process_event(
525    execution: &ToolExecution,
526) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
527    let event = execution
528        .pending_events
529        .lock()
530        .unwrap_or_else(|poisoned| poisoned.into_inner())
531        .pop_front();
532    if event.is_some() {
533        return Ok(event);
534    }
535    if execution.events_overflowed.load(Ordering::Relaxed) {
536        return Err(process_event_queue_overflow_error());
537    }
538    Ok(None)
539}
540
541fn descendant_pending_execution_event_capacity(
542    root: &ActiveProcess,
543    child_path: &[&str],
544) -> Option<usize> {
545    let mut child = root;
546    for child_process_id in child_path {
547        child = child.child_processes.get(*child_process_id)?;
548    }
549    Some(MAX_PROCESS_EVENT_QUEUE.saturating_sub(child.pending_execution_events.len()))
550}
551
552fn poll_child_execution_after_exit(
553    child: &mut ActiveProcess,
554    wait: Duration,
555) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
556    match child.execution.poll_event_blocking(wait) {
557        Ok(event) => Ok(event),
558        Err(SidecarError::Execution(message))
559            if child.runtime == GuestRuntimeKind::WebAssembly
560                && message == WasmExecutionError::EventChannelClosed.to_string() =>
561        {
562            Ok(None)
563        }
564        Err(error) => Err(error),
565    }
566}
567
568fn closed_javascript_event_channel(message: &str) -> bool {
569    message == "guest JavaScript event channel closed unexpectedly"
570}
571
572fn closed_python_event_channel(message: &str) -> bool {
573    message == "guest Python event channel closed unexpectedly"
574}
575
576fn closed_wasm_event_channel(message: &str) -> bool {
577    message == WasmExecutionError::EventChannelClosed.to_string()
578}
579
580fn missing_vm_error(vm_id: &str) -> SidecarError {
581    SidecarError::InvalidState(format!("VM {vm_id} is no longer active"))
582}
583
584fn missing_process_error(vm_id: &str, process_id: &str) -> SidecarError {
585    SidecarError::InvalidState(format!(
586        "VM {vm_id} no longer has active process {process_id}"
587    ))
588}
589
590fn is_broken_pipe_error(error: &SidecarError) -> bool {
591    matches!(error, SidecarError::Execution(message) if message.contains("Broken pipe") || message.contains("os error 32") || message.contains("EPIPE"))
592}
593
594fn javascript_child_process_gone_error(process_id: &str, child_path: &[&str]) -> SidecarError {
595    let child_label = if child_path.is_empty() {
596        process_id.to_owned()
597    } else {
598        format!("{process_id}/{}", child_path.join("/"))
599    };
600    SidecarError::Execution(format!(
601        "ECHILD: child_process {child_label} is no longer available"
602    ))
603}
604
605fn is_javascript_child_process_gone_error(error: &SidecarError) -> bool {
606    matches!(
607        error,
608        SidecarError::Execution(message) if guest_errno_code(message) == Some("ECHILD")
609    )
610}
611
612fn loopback_tls_transport_registry(
613) -> &'static Mutex<BTreeMap<String, Weak<crate::state::LoopbackTlsTransportPair>>> {
614    static REGISTRY: OnceLock<
615        Mutex<BTreeMap<String, Weak<crate::state::LoopbackTlsTransportPair>>>,
616    > = OnceLock::new();
617    REGISTRY.get_or_init(|| Mutex::new(BTreeMap::new()))
618}
619
620fn loopback_tls_transport_key(
621    vm_id: &str,
622    socket_id: SocketId,
623    peer_socket_id: SocketId,
624) -> String {
625    let (lower, higher) = if socket_id <= peer_socket_id {
626        (socket_id, peer_socket_id)
627    } else {
628        (peer_socket_id, socket_id)
629    };
630    format!("{vm_id}:{lower}:{higher}")
631}
632
633fn loopback_tls_endpoint(
634    vm_id: &str,
635    socket_id: SocketId,
636    peer_socket_id: SocketId,
637) -> Result<crate::state::LoopbackTlsEndpoint, SidecarError> {
638    let key = loopback_tls_transport_key(vm_id, socket_id, peer_socket_id);
639    let registry = loopback_tls_transport_registry();
640    let mut transports = registry.lock().map_err(|_| {
641        SidecarError::InvalidState(String::from(
642            "loopback TLS transport registry lock poisoned",
643        ))
644    })?;
645    transports.retain(|_, pair| pair.strong_count() > 0);
646    let pair = transports
647        .get(&key)
648        .and_then(Weak::upgrade)
649        .unwrap_or_else(|| {
650            let pair = Arc::new(crate::state::LoopbackTlsTransportPair {
651                state: Mutex::new(crate::state::LoopbackTlsTransportPairState::default()),
652                ready: std::sync::Condvar::new(),
653            });
654            transports.insert(key, Arc::downgrade(&pair));
655            pair
656        });
657    Ok(crate::state::LoopbackTlsEndpoint {
658        pair,
659        is_lower_socket: socket_id <= peer_socket_id,
660    })
661}
662
663impl crate::state::LoopbackTlsEndpoint {
664    fn shutdown_write(&self) -> Result<(), SidecarError> {
665        let mut state = self.pair.state.lock().map_err(|_| {
666            SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
667        })?;
668        if self.is_lower_socket {
669            state.lower_write_closed = true;
670        } else {
671            state.higher_write_closed = true;
672        }
673        self.pair.ready.notify_all();
674        Ok(())
675    }
676
677    fn close_endpoint(&self) -> Result<(), SidecarError> {
678        let mut state = self.pair.state.lock().map_err(|_| {
679            SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
680        })?;
681        if self.is_lower_socket {
682            state.lower_write_closed = true;
683            state.lower_closed = true;
684        } else {
685            state.higher_write_closed = true;
686            state.higher_closed = true;
687        }
688        self.pair.ready.notify_all();
689        Ok(())
690    }
691}
692
693fn parse_tls_client_hello_from_bytes(
694    buffer: &[u8],
695) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
696    if buffer.is_empty() {
697        return Ok(None);
698    }
699
700    let mut acceptor = rustls::server::Acceptor::default();
701    let mut cursor = Cursor::new(buffer);
702    acceptor.read_tls(&mut cursor).map_err(sidecar_net_error)?;
703    let Some(accepted) = acceptor.accept().map_err(|(error, _)| {
704        SidecarError::Execution(format!("failed to parse TLS client hello: {error}"))
705    })?
706    else {
707        return Ok(None);
708    };
709    let client_hello = accepted.client_hello();
710    let alpn_protocols = client_hello.alpn().map(|protocols| {
711        protocols
712            .filter_map(|protocol| String::from_utf8(protocol.to_vec()).ok())
713            .collect::<Vec<_>>()
714    });
715    Ok(Some(JavascriptTlsClientHello {
716        servername: client_hello.server_name().map(str::to_owned),
717        alpn_protocols,
718    }))
719}
720
721fn peek_loopback_tls_client_hello(
722    vm_id: &str,
723    socket_id: SocketId,
724    peer_socket_id: SocketId,
725) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
726    let key = loopback_tls_transport_key(vm_id, socket_id, peer_socket_id);
727    let registry = loopback_tls_transport_registry();
728    let pair = registry
729        .lock()
730        .map_err(|_| {
731            SidecarError::InvalidState(String::from(
732                "loopback TLS transport registry lock poisoned",
733            ))
734        })?
735        .get(&key)
736        .and_then(Weak::upgrade);
737    let Some(pair) = pair else {
738        return Ok(None);
739    };
740    let is_lower_socket = socket_id <= peer_socket_id;
741    let state = pair.state.lock().map_err(|_| {
742        SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
743    })?;
744    let buffered = if is_lower_socket {
745        state.higher_to_lower.iter().copied().collect::<Vec<_>>()
746    } else {
747        state.lower_to_higher.iter().copied().collect::<Vec<_>>()
748    };
749    drop(state);
750    parse_tls_client_hello_from_bytes(&buffered)
751}
752
753fn wait_for_loopback_peer_socket_id(
754    kernel: &SidecarKernel,
755    socket_id: SocketId,
756) -> Option<SocketId> {
757    for _ in 0..50 {
758        if let Some(peer_socket_id) = kernel
759            .socket_get(socket_id)
760            .and_then(|record| record.peer_socket_id())
761        {
762            return Some(peer_socket_id);
763        }
764        std::thread::sleep(Duration::from_millis(10));
765    }
766    None
767}
768
769impl Drop for crate::state::LoopbackTlsEndpoint {
770    fn drop(&mut self) {
771        let _ = self.close_endpoint();
772    }
773}
774
775impl Read for crate::state::LoopbackTlsEndpoint {
776    fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
777        let mut state = self
778            .pair
779            .state
780            .lock()
781            .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
782
783        loop {
784            let (peer_write_closed, peer_closed) = if self.is_lower_socket {
785                (state.higher_write_closed, state.higher_closed)
786            } else {
787                (state.lower_write_closed, state.lower_closed)
788            };
789
790            let incoming = if self.is_lower_socket {
791                &mut state.higher_to_lower
792            } else {
793                &mut state.lower_to_higher
794            };
795
796            if !incoming.is_empty() {
797                let mut count = 0;
798                while count < buffer.len() {
799                    let Some(byte) = incoming.pop_front() else {
800                        break;
801                    };
802                    buffer[count] = byte;
803                    count += 1;
804                }
805                return Ok(count);
806            }
807
808            if peer_write_closed || peer_closed {
809                return Ok(0);
810            }
811
812            let (next_state, wait_result) = self
813                .pair
814                .ready
815                .wait_timeout(state, TCP_SOCKET_POLL_TIMEOUT)
816                .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
817            state = next_state;
818            if wait_result.timed_out() {
819                return Err(std::io::Error::new(
820                    std::io::ErrorKind::WouldBlock,
821                    "loopback TLS transport read timed out",
822                ));
823            }
824        }
825    }
826}
827
828impl Write for crate::state::LoopbackTlsEndpoint {
829    fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
830        let mut state = self
831            .pair
832            .state
833            .lock()
834            .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
835
836        let peer_closed = if self.is_lower_socket {
837            state.higher_closed
838        } else {
839            state.lower_closed
840        };
841        let outgoing = if self.is_lower_socket {
842            &mut state.lower_to_higher
843        } else {
844            &mut state.higher_to_lower
845        };
846        if peer_closed {
847            return Err(std::io::Error::new(
848                std::io::ErrorKind::BrokenPipe,
849                "loopback TLS peer is closed",
850            ));
851        }
852
853        outgoing.extend(buffer.iter().copied());
854        self.pair.ready.notify_all();
855        Ok(buffer.len())
856    }
857
858    fn flush(&mut self) -> std::io::Result<()> {
859        Ok(())
860    }
861}
862
863// TCP types moved to crate::state
864
865struct ActiveTcpConnectRequest<'a, B> {
866    bridge: &'a SharedBridge<B>,
867    kernel: &'a mut SidecarKernel,
868    kernel_pid: u32,
869    vm_id: &'a str,
870    dns: &'a VmDnsConfig,
871    host: &'a str,
872    port: u16,
873    local_address: Option<&'a str>,
874    local_port: Option<u16>,
875    local_reservation: Option<(JavascriptSocketFamily, u16)>,
876    context: &'a JavascriptSocketPathContext,
877}
878
879struct ActiveUdpSendToRequest<'a, B> {
880    bridge: &'a SharedBridge<B>,
881    kernel: &'a mut SidecarKernel,
882    kernel_pid: u32,
883    vm_id: &'a str,
884    dns: &'a VmDnsConfig,
885    host: &'a str,
886    port: u16,
887    context: &'a JavascriptSocketPathContext,
888    contents: &'a [u8],
889}
890
891struct UdpRemoteAddrRequest<'a, B> {
892    bridge: &'a SharedBridge<B>,
893    kernel: &'a SidecarKernel,
894    vm_id: &'a str,
895    dns: &'a VmDnsConfig,
896    host: &'a str,
897    port: u16,
898    family: JavascriptUdpFamily,
899    context: &'a JavascriptSocketPathContext,
900}
901
902pub(crate) struct JavascriptSyncRpcServiceRequest<'a, B> {
903    pub(crate) bridge: &'a SharedBridge<B>,
904    pub(crate) vm_id: &'a str,
905    pub(crate) dns: &'a VmDnsConfig,
906    pub(crate) socket_paths: &'a JavascriptSocketPathContext,
907    pub(crate) kernel: &'a mut SidecarKernel,
908    pub(crate) process: &'a mut ActiveProcess,
909    pub(crate) sync_request: &'a JavascriptSyncRpcRequest,
910    pub(crate) resource_limits: &'a ResourceLimits,
911    pub(crate) network_counts: NetworkResourceCounts,
912}
913
914pub(crate) struct JavascriptNetSyncRpcServiceRequest<'a, B> {
915    pub(crate) bridge: &'a SharedBridge<B>,
916    pub(crate) vm_id: &'a str,
917    pub(crate) dns: &'a VmDnsConfig,
918    pub(crate) socket_paths: &'a JavascriptSocketPathContext,
919    pub(crate) kernel: &'a mut SidecarKernel,
920    pub(crate) process: &'a mut ActiveProcess,
921    pub(crate) sync_request: &'a JavascriptSyncRpcRequest,
922    pub(crate) resource_limits: &'a ResourceLimits,
923    pub(crate) network_counts: NetworkResourceCounts,
924}
925
926struct LoopbackHttpResponseWaitRequest<'a, B> {
927    bridge: &'a SharedBridge<B>,
928    vm_id: &'a str,
929    dns: &'a VmDnsConfig,
930    socket_paths: &'a JavascriptSocketPathContext,
931    kernel: &'a mut SidecarKernel,
932    process: &'a mut ActiveProcess,
933    resource_limits: &'a ResourceLimits,
934    request_key: (u64, u64),
935}
936
937struct JavascriptDgramSyncRpcServiceRequest<'a, B> {
938    bridge: &'a SharedBridge<B>,
939    kernel: &'a mut SidecarKernel,
940    vm_id: &'a str,
941    dns: &'a VmDnsConfig,
942    socket_paths: &'a JavascriptSocketPathContext,
943    process: &'a mut ActiveProcess,
944    sync_request: &'a JavascriptSyncRpcRequest,
945    resource_limits: &'a ResourceLimits,
946    network_counts: NetworkResourceCounts,
947}
948
949struct JavascriptHttp2SyncRpcServiceRequest<'a, B> {
950    bridge: &'a SharedBridge<B>,
951    kernel: &'a mut SidecarKernel,
952    vm_id: &'a str,
953    dns: &'a VmDnsConfig,
954    socket_paths: &'a JavascriptSocketPathContext,
955    process: &'a mut ActiveProcess,
956    sync_request: &'a JavascriptSyncRpcRequest,
957    resource_limits: &'a ResourceLimits,
958    network_counts: NetworkResourceCounts,
959}
960
961impl ActiveTcpSocket {
962    fn connect<B>(request: ActiveTcpConnectRequest<'_, B>) -> Result<Self, SidecarError>
963    where
964        B: NativeSidecarBridge + Send + 'static,
965        BridgeError<B>: fmt::Debug + Send + Sync + 'static,
966    {
967        let ActiveTcpConnectRequest {
968            bridge,
969            kernel,
970            kernel_pid,
971            vm_id,
972            dns,
973            host,
974            port,
975            local_address,
976            local_port,
977            local_reservation,
978            context,
979        } = request;
980        let resolved = resolve_tcp_connect_addr(bridge, kernel, vm_id, dns, host, port, context)?;
981        if resolved.use_kernel_loopback {
982            let family = JavascriptSocketFamily::from_ip(resolved.guest_remote_addr.ip());
983            let requested_local_port = local_port.unwrap_or(0);
984            let local_port = if requested_local_port != 0
985                && local_reservation == Some((family, requested_local_port))
986            {
987                requested_local_port
988            } else {
989                allocate_guest_listen_port(
990                    requested_local_port,
991                    family,
992                    &context.used_tcp_guest_ports,
993                    context.listen_policy,
994                )?
995            };
996            let local_ip = match (family, local_address) {
997                (JavascriptSocketFamily::Ipv4, Some("0.0.0.0")) => {
998                    IpAddr::V4(Ipv4Addr::UNSPECIFIED)
999                }
1000                (JavascriptSocketFamily::Ipv4, Some("127.0.0.1") | Some("localhost") | None) => {
1001                    IpAddr::V4(Ipv4Addr::LOCALHOST)
1002                }
1003                (JavascriptSocketFamily::Ipv6, Some("::")) => IpAddr::V6(Ipv6Addr::UNSPECIFIED),
1004                (JavascriptSocketFamily::Ipv6, Some("::1") | Some("localhost") | None) => {
1005                    IpAddr::V6(Ipv6Addr::LOCALHOST)
1006                }
1007                (JavascriptSocketFamily::Ipv4, Some(other)) => {
1008                    return Err(SidecarError::Execution(format!(
1009                        "EACCES: TCP sockets must bind to loopback or unspecified addresses, got {other}"
1010                    )));
1011                }
1012                (JavascriptSocketFamily::Ipv6, Some(other)) => {
1013                    return Err(SidecarError::Execution(format!(
1014                        "EACCES: TCP sockets must bind to loopback or unspecified addresses, got {other}"
1015                    )));
1016                }
1017            };
1018            let local_addr = SocketAddr::new(local_ip, local_port);
1019            let spec = match family {
1020                JavascriptSocketFamily::Ipv4 => SocketSpec::tcp(),
1021                JavascriptSocketFamily::Ipv6 => {
1022                    SocketSpec::new(SocketDomain::Inet6, SocketType::Stream)
1023                }
1024            };
1025            let socket_id = kernel
1026                .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
1027                .map_err(kernel_error)?;
1028            kernel
1029                .socket_bind_inet(
1030                    EXECUTION_DRIVER_NAME,
1031                    kernel_pid,
1032                    socket_id,
1033                    InetSocketAddress::new(local_ip.to_string(), local_port),
1034                )
1035                .map_err(kernel_error)?;
1036            kernel
1037                .socket_connect_inet_loopback(
1038                    EXECUTION_DRIVER_NAME,
1039                    kernel_pid,
1040                    socket_id,
1041                    InetSocketAddress::new(
1042                        resolved.guest_remote_addr.ip().to_string(),
1043                        resolved.guest_remote_addr.port(),
1044                    ),
1045                )
1046                .map_err(kernel_error)?;
1047            return Ok(Self::from_kernel(
1048                socket_id,
1049                None,
1050                local_addr,
1051                resolved.guest_remote_addr,
1052            ));
1053        }
1054
1055        let stream = TcpStream::connect_timeout(&resolved.actual_addr, Duration::from_secs(30))
1056            .map_err(sidecar_net_error)?;
1057        let guest_local_addr = stream.local_addr().map_err(sidecar_net_error)?;
1058        Self::from_stream(stream, None, guest_local_addr, resolved.guest_remote_addr)
1059    }
1060
1061    fn from_stream(
1062        stream: TcpStream,
1063        listener_id: Option<String>,
1064        guest_local_addr: SocketAddr,
1065        guest_remote_addr: SocketAddr,
1066    ) -> Result<Self, SidecarError> {
1067        let read_stream = stream.try_clone().map_err(sidecar_net_error)?;
1068        read_stream
1069            .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
1070            .map_err(sidecar_net_error)?;
1071        let stream = Arc::new(Mutex::new(stream));
1072        let pending_read_stream = Arc::new(Mutex::new(Some(read_stream)));
1073        let (sender, events) = mpsc::channel();
1074        let tls_mode = Arc::new(AtomicBool::new(false));
1075        let tls_stream = Arc::new(Mutex::new(None));
1076        let tls_state = Arc::new(Mutex::new(None));
1077        let saw_local_shutdown = Arc::new(AtomicBool::new(false));
1078        let saw_remote_end = Arc::new(AtomicBool::new(false));
1079        let close_notified = Arc::new(AtomicBool::new(false));
1080
1081        Ok(Self {
1082            stream: Some(stream),
1083            pending_read_stream: Some(pending_read_stream),
1084            events: Some(events),
1085            event_sender: Some(sender),
1086            kernel_socket_id: None,
1087            no_delay: false,
1088            keep_alive: false,
1089            keep_alive_initial_delay_secs: None,
1090            guest_local_addr,
1091            guest_remote_addr,
1092            listener_id,
1093            tls_mode,
1094            tls_stream,
1095            tls_state,
1096            saw_local_shutdown,
1097            saw_remote_end,
1098            close_notified,
1099        })
1100    }
1101
1102    fn from_kernel(
1103        socket_id: SocketId,
1104        listener_id: Option<String>,
1105        guest_local_addr: SocketAddr,
1106        guest_remote_addr: SocketAddr,
1107    ) -> Self {
1108        let (sender, events) = mpsc::channel();
1109        Self {
1110            stream: None,
1111            pending_read_stream: None,
1112            events: Some(events),
1113            event_sender: Some(sender),
1114            kernel_socket_id: Some(socket_id),
1115            no_delay: false,
1116            keep_alive: false,
1117            keep_alive_initial_delay_secs: None,
1118            guest_local_addr,
1119            guest_remote_addr,
1120            listener_id,
1121            tls_mode: Arc::new(AtomicBool::new(false)),
1122            tls_stream: Arc::new(Mutex::new(None)),
1123            tls_state: Arc::new(Mutex::new(None)),
1124            saw_local_shutdown: Arc::new(AtomicBool::new(false)),
1125            saw_remote_end: Arc::new(AtomicBool::new(false)),
1126            close_notified: Arc::new(AtomicBool::new(false)),
1127        }
1128    }
1129
1130    fn poll(
1131        &mut self,
1132        kernel: &mut SidecarKernel,
1133        kernel_pid: u32,
1134        wait: Duration,
1135    ) -> Result<Option<JavascriptTcpSocketEvent>, SidecarError> {
1136        if self.tls_mode.load(Ordering::SeqCst) {
1137            self.ensure_tcp_reader()?;
1138            return match self
1139                .events
1140                .as_ref()
1141                .ok_or_else(|| {
1142                    SidecarError::InvalidState(String::from("TCP socket event channel missing"))
1143                })?
1144                .recv_timeout(wait)
1145            {
1146                Ok(event) => Ok(Some(event)),
1147                Err(RecvTimeoutError::Timeout) => Ok(None),
1148                Err(RecvTimeoutError::Disconnected) => Ok(None),
1149            };
1150        }
1151
1152        if let Some(socket_id) = self.kernel_socket_id {
1153            let result = kernel
1154                .poll_targets(
1155                    EXECUTION_DRIVER_NAME,
1156                    kernel_pid,
1157                    vec![PollTargetEntry::socket(
1158                        socket_id,
1159                        POLLIN | POLLHUP | POLLERR,
1160                    )],
1161                    i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
1162                )
1163                .map_err(kernel_error)?;
1164            let revents = result
1165                .targets
1166                .first()
1167                .map(|entry| entry.revents)
1168                .unwrap_or_else(PollEvents::empty);
1169            if revents.is_empty() {
1170                return Ok(None);
1171            }
1172            if revents.intersects(POLLIN) {
1173                return match kernel.socket_read(
1174                    EXECUTION_DRIVER_NAME,
1175                    kernel_pid,
1176                    socket_id,
1177                    64 * 1024,
1178                ) {
1179                    Ok(Some(bytes)) if !bytes.is_empty() => {
1180                        Ok(Some(JavascriptTcpSocketEvent::Data(bytes)))
1181                    }
1182                    Ok(Some(_)) => Ok(Some(JavascriptTcpSocketEvent::Data(Vec::new()))),
1183                    Ok(None) => Ok(Some(JavascriptTcpSocketEvent::End)),
1184                    Err(error) if error.code() == "EAGAIN" => Ok(None),
1185                    Err(error) => Ok(Some(JavascriptTcpSocketEvent::Error {
1186                        code: Some(error.code().to_string()),
1187                        message: error.to_string(),
1188                    })),
1189                };
1190            }
1191            if revents.intersects(POLLHUP) {
1192                return Ok(Some(JavascriptTcpSocketEvent::End));
1193            }
1194            if revents.intersects(POLLERR) {
1195                return Ok(Some(JavascriptTcpSocketEvent::Error {
1196                    code: Some(String::from("EPIPE")),
1197                    message: String::from("kernel TCP socket reported POLLERR"),
1198                }));
1199            }
1200            return Ok(None);
1201        }
1202
1203        self.ensure_tcp_reader()?;
1204        match self
1205            .events
1206            .as_ref()
1207            .ok_or_else(|| {
1208                SidecarError::InvalidState(String::from("TCP socket event channel missing"))
1209            })?
1210            .recv_timeout(wait)
1211        {
1212            Ok(event) => Ok(Some(event)),
1213            Err(RecvTimeoutError::Timeout) => Ok(None),
1214            Err(RecvTimeoutError::Disconnected) => Ok(None),
1215        }
1216    }
1217
1218    fn ensure_tcp_reader(&self) -> Result<(), SidecarError> {
1219        if self.kernel_socket_id.is_some() {
1220            return Ok(());
1221        }
1222        if self.tls_mode.load(Ordering::SeqCst) {
1223            return Ok(());
1224        }
1225        let read_stream = self
1226            .pending_read_stream
1227            .as_ref()
1228            .ok_or_else(|| {
1229                SidecarError::InvalidState(String::from("TCP socket reader handle missing"))
1230            })?
1231            .lock()
1232            .map_err(|_| {
1233                SidecarError::InvalidState(String::from("TCP socket reader lock poisoned"))
1234            })?
1235            .take();
1236        if let Some(read_stream) = read_stream {
1237            spawn_tcp_socket_reader(
1238                read_stream,
1239                self.event_sender
1240                    .as_ref()
1241                    .ok_or_else(|| {
1242                        SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1243                    })?
1244                    .clone(),
1245                Arc::clone(&self.tls_mode),
1246                Arc::clone(&self.saw_local_shutdown),
1247                Arc::clone(&self.saw_remote_end),
1248                Arc::clone(&self.close_notified),
1249            );
1250        }
1251        Ok(())
1252    }
1253
1254    fn socket_info(&self) -> Value {
1255        json!({
1256            "localAddress": self.guest_local_addr.ip().to_string(),
1257            "localPort": self.guest_local_addr.port(),
1258            "localFamily": socket_addr_family(&self.guest_local_addr),
1259            "remoteAddress": self.guest_remote_addr.ip().to_string(),
1260            "remotePort": self.guest_remote_addr.port(),
1261            "remoteFamily": socket_addr_family(&self.guest_remote_addr),
1262        })
1263    }
1264
1265    fn set_no_delay(&mut self, enable: bool) -> Result<(), SidecarError> {
1266        self.no_delay = enable;
1267        if self.kernel_socket_id.is_some() {
1268            return Ok(());
1269        }
1270        let stream = self
1271            .stream
1272            .as_ref()
1273            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1274            .lock()
1275            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1276        stream.set_nodelay(enable).map_err(sidecar_net_error)
1277    }
1278
1279    fn set_keep_alive(
1280        &mut self,
1281        enable: bool,
1282        initial_delay_secs: Option<u64>,
1283    ) -> Result<(), SidecarError> {
1284        self.keep_alive = enable;
1285        self.keep_alive_initial_delay_secs = initial_delay_secs;
1286        if self.kernel_socket_id.is_some() {
1287            return Ok(());
1288        }
1289        let stream = self
1290            .stream
1291            .as_ref()
1292            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1293            .lock()
1294            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1295        let socket = SockRef::from(&*stream);
1296        socket.set_keepalive(enable).map_err(sidecar_net_error)?;
1297        if enable {
1298            if let Some(delay_secs) = initial_delay_secs.filter(|delay_secs| *delay_secs > 0) {
1299                socket
1300                    .set_tcp_keepalive(
1301                        &TcpKeepalive::new().with_time(Duration::from_secs(delay_secs)),
1302                    )
1303                    .map_err(sidecar_net_error)?;
1304            }
1305        }
1306        Ok(())
1307    }
1308
1309    fn upgrade_tls(
1310        &self,
1311        vm_id: &str,
1312        kernel: &SidecarKernel,
1313        options: JavascriptTlsBridgeOptions,
1314    ) -> Result<(), SidecarError> {
1315        if self.tls_mode.load(Ordering::SeqCst) {
1316            return Ok(());
1317        }
1318
1319        let client_hello = if options.is_server {
1320            self.peek_tls_client_hello(vm_id, kernel)?
1321        } else {
1322            None
1323        };
1324
1325        let tls_stream = if let Some(socket_id) = self.kernel_socket_id {
1326            let peer_socket_id = wait_for_loopback_peer_socket_id(kernel, socket_id)
1327                .ok_or_else(|| {
1328                    SidecarError::Execution(format!(
1329                        "ERR_NOT_IMPLEMENTED: kernel-backed loopback socket {socket_id} has no peer for TLS upgrade"
1330                    ))
1331                })?;
1332            let endpoint = loopback_tls_endpoint(vm_id, socket_id, peer_socket_id)?;
1333            if options.is_server {
1334                ActiveTlsStream::LoopbackServer(build_server_loopback_tls_stream(
1335                    endpoint, &options,
1336                )?)
1337            } else {
1338                ActiveTlsStream::LoopbackClient(build_client_loopback_tls_stream(
1339                    endpoint, &options,
1340                )?)
1341            }
1342        } else {
1343            self.pending_read_stream
1344                .as_ref()
1345                .ok_or_else(|| {
1346                    SidecarError::InvalidState(String::from("TCP socket reader handle missing"))
1347                })?
1348                .lock()
1349                .map_err(|_| {
1350                    SidecarError::InvalidState(String::from("TCP socket reader lock poisoned"))
1351                })?
1352                .take();
1353            let stream = self
1354                .stream
1355                .as_ref()
1356                .ok_or_else(|| {
1357                    SidecarError::InvalidState(String::from("TCP socket stream missing"))
1358                })?
1359                .lock()
1360                .map_err(|_| {
1361                    SidecarError::InvalidState(String::from("TCP socket lock poisoned"))
1362                })?;
1363            let cloned = stream.try_clone().map_err(sidecar_net_error)?;
1364            drop(stream);
1365
1366            if options.is_server {
1367                ActiveTlsStream::Server(build_server_tls_stream(cloned, &options)?)
1368            } else {
1369                ActiveTlsStream::Client(build_client_tls_stream(cloned, &options)?)
1370            }
1371        };
1372
1373        let tls_state = ActiveTlsState {
1374            client_hello,
1375            local_certificates: tls_local_certificates(&options)?,
1376            session_reused: false,
1377        };
1378
1379        self.tls_mode.store(true, Ordering::SeqCst);
1380        {
1381            let mut state = self
1382                .tls_state
1383                .lock()
1384                .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?;
1385            *state = Some(tls_state);
1386        }
1387        {
1388            let mut stream = self.tls_stream.lock().map_err(|_| {
1389                SidecarError::InvalidState(String::from("TLS stream lock poisoned"))
1390            })?;
1391            *stream = Some(tls_stream);
1392        }
1393
1394        spawn_tls_socket_reader(
1395            Arc::clone(&self.tls_stream),
1396            self.event_sender
1397                .as_ref()
1398                .ok_or_else(|| {
1399                    SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1400                })?
1401                .clone(),
1402            Arc::clone(&self.saw_local_shutdown),
1403            Arc::clone(&self.saw_remote_end),
1404            Arc::clone(&self.close_notified),
1405        );
1406        Ok(())
1407    }
1408
1409    fn peek_tls_client_hello(
1410        &self,
1411        vm_id: &str,
1412        kernel: &SidecarKernel,
1413    ) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
1414        if let Some(socket_id) = self.kernel_socket_id {
1415            let Some(peer_socket_id) = kernel
1416                .socket_get(socket_id)
1417                .and_then(|record| record.peer_socket_id())
1418            else {
1419                return Ok(None);
1420            };
1421            return peek_loopback_tls_client_hello(vm_id, socket_id, peer_socket_id);
1422        }
1423
1424        let stream = self
1425            .stream
1426            .as_ref()
1427            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1428            .lock()
1429            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1430        let mut buffer = vec![0_u8; 16 * 1024];
1431        let bytes = match stream.peek(&mut buffer) {
1432            Ok(0) => return Ok(None),
1433            Ok(bytes) => bytes,
1434            Err(error)
1435                if matches!(
1436                    error.kind(),
1437                    std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
1438                ) =>
1439            {
1440                return Ok(None);
1441            }
1442            Err(error) => return Err(sidecar_net_error(error)),
1443        };
1444        parse_tls_client_hello_from_bytes(&buffer[..bytes])
1445    }
1446
1447    fn tls_client_hello_json(
1448        &self,
1449        vm_id: &str,
1450        kernel: &SidecarKernel,
1451    ) -> Result<Value, SidecarError> {
1452        if let Some(client_hello) = self
1453            .tls_state
1454            .lock()
1455            .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?
1456            .as_ref()
1457            .and_then(|state| state.client_hello.clone())
1458        {
1459            return javascript_net_json_string(
1460                serde_json::to_value(client_hello).map_err(|error| {
1461                    SidecarError::InvalidState(format!(
1462                        "failed to serialize TLS client hello: {error}"
1463                    ))
1464                })?,
1465                "net.socket_get_tls_client_hello",
1466            );
1467        }
1468
1469        javascript_net_json_string(
1470            serde_json::to_value(
1471                self.peek_tls_client_hello(vm_id, kernel)?
1472                    .unwrap_or_default(),
1473            )
1474            .map_err(|error| {
1475                SidecarError::InvalidState(format!("failed to serialize TLS client hello: {error}"))
1476            })?,
1477            "net.socket_get_tls_client_hello",
1478        )
1479    }
1480
1481    fn tls_query(&self, query: &str, detailed: bool) -> Result<Value, SidecarError> {
1482        let state = self
1483            .tls_state
1484            .lock()
1485            .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?
1486            .clone();
1487        let mut tls_stream = self
1488            .tls_stream
1489            .lock()
1490            .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?;
1491        let Some(stream) = tls_stream.as_mut() else {
1492            return javascript_net_json_string(
1493                tls_bridge_undefined_value(),
1494                "net.socket_tls_query",
1495            );
1496        };
1497
1498        let payload = match query {
1499            "getSession" => tls_bridge_undefined_value(),
1500            "isSessionReused" => Value::Bool(
1501                state
1502                    .as_ref()
1503                    .is_some_and(|tls_state| tls_state.session_reused),
1504            ),
1505            "getPeerCertificate" => {
1506                let certificate = stream
1507                    .peer_certificates()
1508                    .and_then(|certificates| certificates.first())
1509                    .map(|certificate| {
1510                        tls_certificate_bridge_value(certificate.as_ref(), detailed)
1511                    });
1512                certificate.unwrap_or_else(tls_bridge_undefined_value)
1513            }
1514            "getCertificate" => state
1515                .as_ref()
1516                .and_then(|tls_state| tls_state.local_certificates.first())
1517                .map(|certificate| tls_certificate_bridge_value(certificate, detailed))
1518                .unwrap_or_else(tls_bridge_undefined_value),
1519            "getProtocol" => stream
1520                .protocol_version()
1521                .map(tls_protocol_name)
1522                .map(Value::String)
1523                .unwrap_or(Value::Null),
1524            "getCipher" => stream
1525                .negotiated_cipher_suite()
1526                .map(tls_cipher_bridge_value)
1527                .unwrap_or_else(tls_bridge_undefined_value),
1528            other => {
1529                return Err(SidecarError::InvalidState(format!(
1530                    "unsupported TLS query {other}"
1531                )));
1532            }
1533        };
1534        javascript_net_json_string(payload, "net.socket_tls_query")
1535    }
1536
1537    fn write_all(
1538        &self,
1539        kernel: &mut SidecarKernel,
1540        kernel_pid: u32,
1541        contents: &[u8],
1542    ) -> Result<usize, SidecarError> {
1543        if self.tls_mode.load(Ordering::SeqCst) {
1544            let mut tls_stream = self.tls_stream.lock().map_err(|_| {
1545                SidecarError::InvalidState(String::from("TLS stream lock poisoned"))
1546            })?;
1547            let stream = tls_stream.as_mut().ok_or_else(|| {
1548                SidecarError::InvalidState(String::from("TLS stream missing for upgraded socket"))
1549            })?;
1550            stream.write_all(contents)?;
1551            return Ok(contents.len());
1552        }
1553        if let Some(socket_id) = self.kernel_socket_id {
1554            return kernel
1555                .socket_write(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, contents)
1556                .map_err(kernel_error);
1557        }
1558
1559        let mut stream = self
1560            .stream
1561            .as_ref()
1562            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1563            .lock()
1564            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1565        stream.write_all(contents).map_err(sidecar_net_error)?;
1566        Ok(contents.len())
1567    }
1568
1569    fn shutdown_write(
1570        &self,
1571        kernel: &mut SidecarKernel,
1572        kernel_pid: u32,
1573    ) -> Result<(), SidecarError> {
1574        if self.tls_mode.load(Ordering::SeqCst) {
1575            if let Some(stream) = self
1576                .tls_stream
1577                .lock()
1578                .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?
1579                .as_mut()
1580            {
1581                let _ = stream.send_close_notify();
1582                let _ = stream.shutdown_write();
1583            }
1584            if self.kernel_socket_id.is_some() {
1585                self.saw_local_shutdown.store(true, Ordering::SeqCst);
1586                return Ok(());
1587            }
1588        }
1589        if let Some(socket_id) = self.kernel_socket_id {
1590            return kernel
1591                .socket_shutdown(
1592                    EXECUTION_DRIVER_NAME,
1593                    kernel_pid,
1594                    socket_id,
1595                    KernelSocketShutdown::Write,
1596                )
1597                .map_err(kernel_error);
1598        }
1599        let stream = self
1600            .stream
1601            .as_ref()
1602            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1603            .lock()
1604            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1605        self.saw_local_shutdown.store(true, Ordering::SeqCst);
1606        match stream.shutdown(Shutdown::Write) {
1607            Ok(()) => {}
1608            Err(error) if error.kind() == std::io::ErrorKind::NotConnected => {}
1609            Err(error) => return Err(sidecar_net_error(error)),
1610        }
1611        if self.saw_remote_end.load(Ordering::SeqCst)
1612            && !self.close_notified.swap(true, Ordering::SeqCst)
1613        {
1614            let _ = self
1615                .event_sender
1616                .as_ref()
1617                .ok_or_else(|| {
1618                    SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1619                })?
1620                .send(JavascriptTcpSocketEvent::Close { had_error: false });
1621        }
1622        Ok(())
1623    }
1624
1625    fn close(&self, kernel: &mut SidecarKernel, kernel_pid: u32) -> Result<(), SidecarError> {
1626        if self.tls_mode.load(Ordering::SeqCst) {
1627            if let Some(stream) = self
1628                .tls_stream
1629                .lock()
1630                .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?
1631                .as_mut()
1632            {
1633                let _ = stream.send_close_notify();
1634                let _ = stream.close();
1635            }
1636            if self.kernel_socket_id.is_some() {
1637                return Ok(());
1638            }
1639        }
1640        if let Some(socket_id) = self.kernel_socket_id {
1641            return kernel
1642                .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id)
1643                .map_err(kernel_error);
1644        }
1645        let stream = self
1646            .stream
1647            .as_ref()
1648            .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1649            .lock()
1650            .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1651        stream.shutdown(Shutdown::Both).map_err(sidecar_net_error)
1652    }
1653}
1654
1655impl ActiveTlsStream {
1656    fn write_all(&mut self, contents: &[u8]) -> Result<(), SidecarError> {
1657        match self {
1658            Self::Client(stream) => {
1659                stream.write_all(contents).map_err(sidecar_net_error)?;
1660                stream.flush().map_err(sidecar_net_error)
1661            }
1662            Self::Server(stream) => {
1663                stream.write_all(contents).map_err(sidecar_net_error)?;
1664                stream.flush().map_err(sidecar_net_error)
1665            }
1666            Self::LoopbackClient(stream) => {
1667                stream.write_all(contents).map_err(sidecar_net_error)?;
1668                stream.flush().map_err(sidecar_net_error)
1669            }
1670            Self::LoopbackServer(stream) => {
1671                stream.write_all(contents).map_err(sidecar_net_error)?;
1672                stream.flush().map_err(sidecar_net_error)
1673            }
1674        }
1675    }
1676
1677    fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
1678        match self {
1679            Self::Client(stream) => stream.read(buffer),
1680            Self::Server(stream) => stream.read(buffer),
1681            Self::LoopbackClient(stream) => stream.read(buffer),
1682            Self::LoopbackServer(stream) => stream.read(buffer),
1683        }
1684    }
1685
1686    fn send_close_notify(&mut self) -> Result<(), SidecarError> {
1687        match self {
1688            Self::Client(stream) => {
1689                stream.conn.send_close_notify();
1690                let _ = stream.conn.complete_io(&mut stream.sock);
1691            }
1692            Self::Server(stream) => {
1693                stream.conn.send_close_notify();
1694                let _ = stream.conn.complete_io(&mut stream.sock);
1695            }
1696            Self::LoopbackClient(stream) => {
1697                stream.conn.send_close_notify();
1698                let _ = stream.conn.complete_io(&mut stream.sock);
1699            }
1700            Self::LoopbackServer(stream) => {
1701                stream.conn.send_close_notify();
1702                let _ = stream.conn.complete_io(&mut stream.sock);
1703            }
1704        }
1705        Ok(())
1706    }
1707
1708    fn shutdown_write(&mut self) -> Result<(), SidecarError> {
1709        match self {
1710            Self::Client(stream) => stream
1711                .sock
1712                .shutdown(Shutdown::Write)
1713                .map_err(sidecar_net_error),
1714            Self::Server(stream) => stream
1715                .sock
1716                .shutdown(Shutdown::Write)
1717                .map_err(sidecar_net_error),
1718            Self::LoopbackClient(stream) => stream.sock.shutdown_write(),
1719            Self::LoopbackServer(stream) => stream.sock.shutdown_write(),
1720        }
1721    }
1722
1723    fn close(&mut self) -> Result<(), SidecarError> {
1724        match self {
1725            Self::Client(stream) => stream
1726                .sock
1727                .shutdown(Shutdown::Both)
1728                .map_err(sidecar_net_error),
1729            Self::Server(stream) => stream
1730                .sock
1731                .shutdown(Shutdown::Both)
1732                .map_err(sidecar_net_error),
1733            Self::LoopbackClient(stream) => stream.sock.close_endpoint(),
1734            Self::LoopbackServer(stream) => stream.sock.close_endpoint(),
1735        }
1736    }
1737
1738    fn peer_certificates(&self) -> Option<&[CertificateDer<'static>]> {
1739        match self {
1740            Self::Client(stream) => stream.conn.peer_certificates(),
1741            Self::Server(stream) => stream.conn.peer_certificates(),
1742            Self::LoopbackClient(stream) => stream.conn.peer_certificates(),
1743            Self::LoopbackServer(stream) => stream.conn.peer_certificates(),
1744        }
1745    }
1746
1747    fn negotiated_cipher_suite(&self) -> Option<rustls::SupportedCipherSuite> {
1748        match self {
1749            Self::Client(stream) => stream.conn.negotiated_cipher_suite(),
1750            Self::Server(stream) => stream.conn.negotiated_cipher_suite(),
1751            Self::LoopbackClient(stream) => stream.conn.negotiated_cipher_suite(),
1752            Self::LoopbackServer(stream) => stream.conn.negotiated_cipher_suite(),
1753        }
1754    }
1755
1756    fn protocol_version(&self) -> Option<rustls::ProtocolVersion> {
1757        match self {
1758            Self::Client(stream) => stream.conn.protocol_version(),
1759            Self::Server(stream) => stream.conn.protocol_version(),
1760            Self::LoopbackClient(stream) => stream.conn.protocol_version(),
1761            Self::LoopbackServer(stream) => stream.conn.protocol_version(),
1762        }
1763    }
1764}
1765
1766// ActiveTcpListener moved to crate::state
1767
1768// Unix socket types moved to crate::state
1769
1770impl ActiveUnixSocket {
1771    fn connect(host_path: &Path, guest_path: &str) -> Result<Self, SidecarError> {
1772        let stream = UnixStream::connect(host_path).map_err(sidecar_net_error)?;
1773        Self::from_stream(stream, None, None, Some(guest_path.to_owned()))
1774    }
1775
1776    fn from_stream(
1777        stream: UnixStream,
1778        listener_id: Option<String>,
1779        local_path: Option<String>,
1780        remote_path: Option<String>,
1781    ) -> Result<Self, SidecarError> {
1782        let read_stream = stream.try_clone().map_err(sidecar_net_error)?;
1783        let stream = Arc::new(Mutex::new(stream));
1784        let (sender, events) = mpsc::channel();
1785        let saw_local_shutdown = Arc::new(AtomicBool::new(false));
1786        let saw_remote_end = Arc::new(AtomicBool::new(false));
1787        let close_notified = Arc::new(AtomicBool::new(false));
1788        spawn_unix_socket_reader(
1789            read_stream,
1790            sender.clone(),
1791            Arc::clone(&saw_local_shutdown),
1792            Arc::clone(&saw_remote_end),
1793            Arc::clone(&close_notified),
1794        );
1795
1796        Ok(Self {
1797            stream,
1798            events,
1799            event_sender: sender,
1800            listener_id,
1801            local_path,
1802            remote_path,
1803            saw_local_shutdown,
1804            saw_remote_end,
1805            close_notified,
1806        })
1807    }
1808
1809    fn poll(&mut self, wait: Duration) -> Result<Option<JavascriptTcpSocketEvent>, SidecarError> {
1810        match self.events.recv_timeout(wait) {
1811            Ok(event) => Ok(Some(event)),
1812            Err(RecvTimeoutError::Timeout) => Ok(None),
1813            Err(RecvTimeoutError::Disconnected) => Ok(None),
1814        }
1815    }
1816
1817    fn socket_info(&self) -> Value {
1818        json!({
1819            "localPath": self.local_path.clone(),
1820            "remotePath": self.remote_path.clone(),
1821        })
1822    }
1823
1824    fn write_all(&self, contents: &[u8]) -> Result<usize, SidecarError> {
1825        let mut stream = self
1826            .stream
1827            .lock()
1828            .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
1829        stream.write_all(contents).map_err(sidecar_net_error)?;
1830        Ok(contents.len())
1831    }
1832
1833    fn shutdown_write(&self) -> Result<(), SidecarError> {
1834        let stream = self
1835            .stream
1836            .lock()
1837            .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
1838        self.saw_local_shutdown.store(true, Ordering::SeqCst);
1839        stream
1840            .shutdown(Shutdown::Write)
1841            .map_err(sidecar_net_error)?;
1842        if self.saw_remote_end.load(Ordering::SeqCst)
1843            && !self.close_notified.swap(true, Ordering::SeqCst)
1844        {
1845            let _ = self
1846                .event_sender
1847                .send(JavascriptTcpSocketEvent::Close { had_error: false });
1848        }
1849        Ok(())
1850    }
1851
1852    fn close(&self) -> Result<(), SidecarError> {
1853        let stream = self
1854            .stream
1855            .lock()
1856            .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
1857        stream.shutdown(Shutdown::Both).map_err(sidecar_net_error)
1858    }
1859}
1860
1861// ActiveUnixListener moved to crate::state
1862
1863impl ActiveUnixListener {
1864    fn bind(
1865        host_path: &Path,
1866        guest_path: &str,
1867        backlog: Option<u32>,
1868    ) -> Result<Self, SidecarError> {
1869        if let Some(parent) = host_path.parent() {
1870            fs::create_dir_all(parent).map_err(sidecar_net_error)?;
1871        }
1872        let listener = UnixListener::bind(host_path).map_err(sidecar_net_error)?;
1873        listener.set_nonblocking(true).map_err(sidecar_net_error)?;
1874        Ok(Self {
1875            listener,
1876            path: guest_path.to_owned(),
1877            backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
1878                .expect("default backlog fits within usize"),
1879            active_connection_ids: BTreeSet::new(),
1880        })
1881    }
1882
1883    fn path(&self) -> &str {
1884        &self.path
1885    }
1886
1887    fn poll(
1888        &mut self,
1889        wait: Duration,
1890    ) -> Result<Option<JavascriptUnixListenerEvent>, SidecarError> {
1891        let deadline = Instant::now() + wait;
1892        loop {
1893            match self.listener.accept() {
1894                Ok((stream, remote_addr)) => {
1895                    if self.active_connection_ids.len() >= self.backlog {
1896                        let _ = stream.shutdown(Shutdown::Both);
1897                        if wait.is_zero() || Instant::now() >= deadline {
1898                            return Ok(None);
1899                        }
1900                        continue;
1901                    }
1902
1903                    let local_path = Some(self.path.clone());
1904                    let remote_path = unix_socket_path(&remote_addr);
1905                    return Ok(Some(JavascriptUnixListenerEvent::Connection(
1906                        PendingUnixSocket {
1907                            stream,
1908                            local_path,
1909                            remote_path,
1910                        },
1911                    )));
1912                }
1913                Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
1914                    if wait.is_zero() || Instant::now() >= deadline {
1915                        return Ok(None);
1916                    }
1917                    thread::sleep(Duration::from_millis(10));
1918                }
1919                Err(error) => {
1920                    return Ok(Some(JavascriptUnixListenerEvent::Error {
1921                        code: io_error_code(&error),
1922                        message: error.to_string(),
1923                    }));
1924                }
1925            }
1926        }
1927    }
1928
1929    fn close(&self) -> Result<(), SidecarError> {
1930        Ok(())
1931    }
1932
1933    fn active_connection_count(&self) -> usize {
1934        self.active_connection_ids.len()
1935    }
1936
1937    fn register_connection(&mut self, socket_id: &str) {
1938        self.active_connection_ids.insert(socket_id.to_string());
1939    }
1940
1941    fn release_connection(&mut self, socket_id: &str) {
1942        self.active_connection_ids.remove(socket_id);
1943    }
1944}
1945
1946impl ActiveTcpListener {
1947    fn bind(
1948        bind_host: &str,
1949        guest_host: &str,
1950        guest_port: u16,
1951        backlog: Option<u32>,
1952    ) -> Result<Self, SidecarError> {
1953        let bind_addr = resolve_tcp_bind_addr(bind_host, 0)?;
1954        let guest_addr = resolve_tcp_bind_addr(guest_host, guest_port)?;
1955        let listener = TcpListener::bind(bind_addr).map_err(sidecar_net_error)?;
1956        listener.set_nonblocking(true).map_err(sidecar_net_error)?;
1957        let local_addr = listener.local_addr().map_err(sidecar_net_error)?;
1958        Ok(Self {
1959            listener: Some(listener),
1960            kernel_socket_id: None,
1961            local_addr: Some(local_addr),
1962            guest_local_addr: guest_addr,
1963            backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
1964                .expect("default backlog fits within usize"),
1965            active_connection_ids: BTreeSet::new(),
1966        })
1967    }
1968
1969    fn bind_kernel(
1970        kernel: &mut SidecarKernel,
1971        kernel_pid: u32,
1972        guest_host: &str,
1973        guest_port: u16,
1974        backlog: Option<u32>,
1975    ) -> Result<Self, SidecarError> {
1976        let guest_addr = resolve_tcp_bind_addr(guest_host, guest_port)?;
1977        let spec = match guest_addr {
1978            SocketAddr::V4(_) => SocketSpec::tcp(),
1979            SocketAddr::V6(_) => SocketSpec::new(SocketDomain::Inet6, SocketType::Stream),
1980        };
1981        let socket_id = kernel
1982            .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
1983            .map_err(kernel_error)?;
1984        kernel
1985            .socket_bind_inet(
1986                EXECUTION_DRIVER_NAME,
1987                kernel_pid,
1988                socket_id,
1989                InetSocketAddress::new(guest_addr.ip().to_string(), guest_addr.port()),
1990            )
1991            .map_err(kernel_error)?;
1992        kernel
1993            .socket_listen(
1994                EXECUTION_DRIVER_NAME,
1995                kernel_pid,
1996                socket_id,
1997                usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
1998                    .expect("default backlog fits within usize"),
1999            )
2000            .map_err(kernel_error)?;
2001        Ok(Self {
2002            listener: None,
2003            kernel_socket_id: Some(socket_id),
2004            local_addr: Some(guest_addr),
2005            guest_local_addr: guest_addr,
2006            backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
2007                .expect("default backlog fits within usize"),
2008            active_connection_ids: BTreeSet::new(),
2009        })
2010    }
2011
2012    pub(crate) fn local_addr(&self) -> SocketAddr {
2013        self.local_addr.unwrap_or(self.guest_local_addr)
2014    }
2015
2016    fn guest_local_addr(&self) -> SocketAddr {
2017        self.guest_local_addr
2018    }
2019
2020    fn poll(
2021        &mut self,
2022        kernel: &mut SidecarKernel,
2023        kernel_pid: u32,
2024        wait: Duration,
2025    ) -> Result<Option<JavascriptTcpListenerEvent>, SidecarError> {
2026        if let Some(socket_id) = self.kernel_socket_id {
2027            let result = kernel
2028                .poll_targets(
2029                    EXECUTION_DRIVER_NAME,
2030                    kernel_pid,
2031                    vec![PollTargetEntry::socket(socket_id, POLLIN)],
2032                    i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
2033                )
2034                .map_err(kernel_error)?;
2035            let revents = result
2036                .targets
2037                .first()
2038                .map(|entry| entry.revents)
2039                .unwrap_or_else(PollEvents::empty);
2040            if revents.is_empty() {
2041                return Ok(None);
2042            }
2043            let accepted_socket_id =
2044                match kernel.socket_accept(EXECUTION_DRIVER_NAME, kernel_pid, socket_id) {
2045                    Ok(accepted_socket_id) => accepted_socket_id,
2046                    Err(error) if error.code() == "EAGAIN" => return Ok(None),
2047                    Err(error) => {
2048                        return Ok(Some(JavascriptTcpListenerEvent::Error {
2049                            code: Some(error.code().to_string()),
2050                            message: error.to_string(),
2051                        }));
2052                    }
2053                };
2054            let accepted = kernel.socket_get(accepted_socket_id).ok_or_else(|| {
2055                SidecarError::InvalidState(format!(
2056                    "accepted kernel TCP socket {accepted_socket_id} is missing"
2057                ))
2058            })?;
2059            let local_addr = accepted.local_address().ok_or_else(|| {
2060                SidecarError::InvalidState(format!(
2061                    "accepted kernel TCP socket {accepted_socket_id} missing local address"
2062                ))
2063            })?;
2064            let remote_addr = accepted.peer_address().ok_or_else(|| {
2065                SidecarError::InvalidState(format!(
2066                    "accepted kernel TCP socket {accepted_socket_id} missing peer address"
2067                ))
2068            })?;
2069            return Ok(Some(JavascriptTcpListenerEvent::Connection(
2070                PendingTcpSocket {
2071                    stream: None,
2072                    kernel_socket_id: Some(accepted_socket_id),
2073                    preallocated: true,
2074                    guest_local_addr: resolve_tcp_bind_addr(local_addr.host(), local_addr.port())?,
2075                    guest_remote_addr: resolve_tcp_bind_addr(
2076                        remote_addr.host(),
2077                        remote_addr.port(),
2078                    )?,
2079                },
2080            )));
2081        }
2082
2083        let deadline = Instant::now() + wait;
2084        loop {
2085            match self
2086                .listener
2087                .as_ref()
2088                .ok_or_else(|| {
2089                    SidecarError::InvalidState(String::from("TCP listener socket missing"))
2090                })?
2091                .accept()
2092            {
2093                Ok((stream, remote_addr)) => {
2094                    if self.active_connection_ids.len() >= self.backlog {
2095                        let _ = stream.shutdown(Shutdown::Both);
2096                        if wait.is_zero() || Instant::now() >= deadline {
2097                            return Ok(None);
2098                        }
2099                        continue;
2100                    }
2101                    return Ok(Some(JavascriptTcpListenerEvent::Connection(
2102                        PendingTcpSocket {
2103                            stream: Some(stream),
2104                            kernel_socket_id: None,
2105                            preallocated: false,
2106                            guest_local_addr: self.guest_local_addr,
2107                            guest_remote_addr: SocketAddr::new(
2108                                remote_addr.ip(),
2109                                remote_addr.port(),
2110                            ),
2111                        },
2112                    )));
2113                }
2114                Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
2115                    if wait.is_zero() || Instant::now() >= deadline {
2116                        return Ok(None);
2117                    }
2118                    thread::sleep(Duration::from_millis(10));
2119                }
2120                Err(error) => {
2121                    return Ok(Some(JavascriptTcpListenerEvent::Error {
2122                        code: io_error_code(&error),
2123                        message: error.to_string(),
2124                    }));
2125                }
2126            }
2127        }
2128    }
2129
2130    fn close(&self, kernel: &mut SidecarKernel, kernel_pid: u32) -> Result<(), SidecarError> {
2131        if let Some(socket_id) = self.kernel_socket_id {
2132            kernel
2133                .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id)
2134                .map_err(kernel_error)?;
2135        }
2136        Ok(())
2137    }
2138
2139    fn active_connection_count(&self) -> usize {
2140        self.active_connection_ids.len()
2141    }
2142
2143    fn register_connection(&mut self, socket_id: &str) {
2144        self.active_connection_ids.insert(socket_id.to_string());
2145    }
2146
2147    fn release_connection(&mut self, socket_id: &str) {
2148        self.active_connection_ids.remove(socket_id);
2149    }
2150}
2151
2152// UDP types moved to crate::state
2153
2154impl ActiveUdpSocket {
2155    fn new(
2156        kernel: &mut SidecarKernel,
2157        kernel_pid: u32,
2158        family: JavascriptUdpFamily,
2159    ) -> Result<Self, SidecarError> {
2160        let spec = match family {
2161            JavascriptUdpFamily::Ipv4 => SocketSpec::udp(),
2162            JavascriptUdpFamily::Ipv6 => SocketSpec::new(SocketDomain::Inet6, SocketType::Datagram),
2163        };
2164        let socket_id = kernel
2165            .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
2166            .map_err(kernel_error)?;
2167        Ok(Self {
2168            family,
2169            socket: None,
2170            kernel_socket_id: Some(socket_id),
2171            guest_local_addr: None,
2172            recv_buffer_size: 0,
2173            send_buffer_size: 0,
2174        })
2175    }
2176
2177    fn local_addr(&self) -> Option<SocketAddr> {
2178        self.guest_local_addr
2179    }
2180
2181    fn socket(&self) -> Result<&UdpSocket, SidecarError> {
2182        self.socket
2183            .as_ref()
2184            .ok_or_else(|| SidecarError::Execution(String::from("EBADF: bad file descriptor")))
2185    }
2186
2187    fn bind(
2188        &mut self,
2189        kernel: &mut SidecarKernel,
2190        kernel_pid: u32,
2191        host: Option<&str>,
2192        port: u16,
2193        context: &JavascriptSocketPathContext,
2194    ) -> Result<SocketAddr, SidecarError> {
2195        if self.socket.is_some() || self.guest_local_addr.is_some() {
2196            return Err(SidecarError::Execution(String::from(
2197                "EINVAL: secure-exec dgram socket is already bound",
2198            )));
2199        }
2200
2201        let (bind_host, guest_host, guest_family) = normalize_udp_bind_host(host, self.family)?;
2202        let guest_port = allocate_guest_listen_port(
2203            port,
2204            guest_family,
2205            &context.used_udp_guest_ports,
2206            context.listen_policy,
2207        )?;
2208        let local_addr = resolve_udp_bind_addr(guest_host, guest_port, self.family)?;
2209        if let Some(socket_id) = self.kernel_socket_id {
2210            kernel
2211                .socket_bind_inet(
2212                    EXECUTION_DRIVER_NAME,
2213                    kernel_pid,
2214                    socket_id,
2215                    InetSocketAddress::new(local_addr.ip().to_string(), local_addr.port()),
2216                )
2217                .map_err(kernel_error)?;
2218        } else {
2219            let bind_addr = resolve_udp_bind_addr(bind_host, 0, self.family)?;
2220            let socket = UdpSocket::bind(bind_addr).map_err(sidecar_net_error)?;
2221            socket.set_nonblocking(true).map_err(sidecar_net_error)?;
2222            self.socket = Some(socket);
2223        }
2224        self.guest_local_addr = Some(local_addr);
2225        Ok(local_addr)
2226    }
2227
2228    fn ensure_bound_for_send(
2229        &mut self,
2230        kernel: &mut SidecarKernel,
2231        kernel_pid: u32,
2232        context: &JavascriptSocketPathContext,
2233    ) -> Result<SocketAddr, SidecarError> {
2234        if let Some(local_addr) = self.local_addr() {
2235            return Ok(local_addr);
2236        }
2237
2238        self.bind(kernel, kernel_pid, None, 0, context)
2239    }
2240
2241    fn send_to<B>(
2242        &mut self,
2243        request: ActiveUdpSendToRequest<'_, B>,
2244    ) -> Result<(usize, SocketAddr), SidecarError>
2245    where
2246        B: NativeSidecarBridge + Send + 'static,
2247        BridgeError<B>: fmt::Debug + Send + Sync + 'static,
2248    {
2249        let ActiveUdpSendToRequest {
2250            bridge,
2251            kernel,
2252            kernel_pid,
2253            vm_id,
2254            dns,
2255            host,
2256            port,
2257            context,
2258            contents,
2259        } = request;
2260        let remote_addr = resolve_udp_addr(UdpRemoteAddrRequest {
2261            bridge,
2262            kernel,
2263            vm_id,
2264            dns,
2265            host,
2266            port,
2267            family: self.family,
2268            context,
2269        })?;
2270        let local_addr = self.ensure_bound_for_send(kernel, kernel_pid, context)?;
2271        let written = if let Some(socket_id) = self.kernel_socket_id {
2272            if is_loopback_ip(remote_addr.ip()) && remote_addr.port() == port {
2273                kernel
2274                    .socket_send_to_inet_loopback(
2275                        EXECUTION_DRIVER_NAME,
2276                        kernel_pid,
2277                        socket_id,
2278                        InetSocketAddress::new(remote_addr.ip().to_string(), remote_addr.port()),
2279                        contents,
2280                    )
2281                    .map_err(kernel_error)?
2282            } else {
2283                return Err(SidecarError::Execution(String::from(
2284                    "ERR_NOT_IMPLEMENTED: external UDP datagrams are not yet supported by the kernel-backed V8 bridge",
2285                )));
2286            }
2287        } else {
2288            let socket = self.socket.as_ref().ok_or_else(|| {
2289                SidecarError::InvalidState(String::from("UDP socket is not initialized"))
2290            })?;
2291            socket
2292                .send_to(contents, remote_addr)
2293                .map_err(sidecar_net_error)?
2294        };
2295        Ok((written, local_addr))
2296    }
2297
2298    fn poll(
2299        &self,
2300        kernel: &mut SidecarKernel,
2301        kernel_pid: u32,
2302        wait: Duration,
2303    ) -> Result<Option<JavascriptUdpSocketEvent>, SidecarError> {
2304        if let Some(socket_id) = self.kernel_socket_id {
2305            let result = kernel
2306                .poll_targets(
2307                    EXECUTION_DRIVER_NAME,
2308                    kernel_pid,
2309                    vec![PollTargetEntry::socket(socket_id, POLLIN)],
2310                    i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
2311                )
2312                .map_err(kernel_error)?;
2313            let revents = result
2314                .targets
2315                .first()
2316                .map(|entry| entry.revents)
2317                .unwrap_or_else(PollEvents::empty);
2318            if revents.is_empty() {
2319                return Ok(None);
2320            }
2321            return match kernel.socket_recv_datagram(
2322                EXECUTION_DRIVER_NAME,
2323                kernel_pid,
2324                socket_id,
2325                64 * 1024,
2326            ) {
2327                Ok(Some(datagram)) => {
2328                    let (source_address, payload) = datagram.into_parts();
2329                    let remote_addr = source_address
2330                        .map(|source| {
2331                            resolve_udp_bind_addr(source.host(), source.port(), self.family)
2332                        })
2333                        .transpose()?
2334                        .unwrap_or_else(|| match self.family {
2335                            JavascriptUdpFamily::Ipv4 => {
2336                                SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)
2337                            }
2338                            JavascriptUdpFamily::Ipv6 => {
2339                                SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0)
2340                            }
2341                        });
2342                    Ok(Some(JavascriptUdpSocketEvent::Message {
2343                        data: payload,
2344                        remote_addr,
2345                    }))
2346                }
2347                Ok(None) => Ok(None),
2348                Err(error) if error.code() == "EAGAIN" => Ok(None),
2349                Err(error) => Ok(Some(JavascriptUdpSocketEvent::Error {
2350                    code: Some(error.code().to_string()),
2351                    message: error.to_string(),
2352                })),
2353            };
2354        }
2355        let socket = self.socket()?;
2356        let deadline = Instant::now() + wait;
2357        let mut buffer = vec![0_u8; 64 * 1024];
2358
2359        loop {
2360            match socket.recv_from(&mut buffer) {
2361                Ok((bytes_read, remote_addr)) => {
2362                    return Ok(Some(JavascriptUdpSocketEvent::Message {
2363                        data: buffer[..bytes_read].to_vec(),
2364                        remote_addr,
2365                    }));
2366                }
2367                Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
2368                    if wait.is_zero() || Instant::now() >= deadline {
2369                        return Ok(None);
2370                    }
2371                    thread::sleep(Duration::from_millis(10));
2372                }
2373                Err(error) => {
2374                    return Ok(Some(JavascriptUdpSocketEvent::Error {
2375                        code: io_error_code(&error),
2376                        message: error.to_string(),
2377                    }));
2378                }
2379            }
2380        }
2381    }
2382
2383    fn close(&mut self, kernel: &mut SidecarKernel, kernel_pid: u32) {
2384        if let Some(socket_id) = self.kernel_socket_id {
2385            let _ = kernel.socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id);
2386        }
2387        self.socket.take();
2388        self.guest_local_addr = None;
2389    }
2390
2391    fn set_buffer_size(&mut self, which: &str, size: usize) -> Result<(), SidecarError> {
2392        match which {
2393            "recv" => self.recv_buffer_size = size,
2394            "send" => self.send_buffer_size = size,
2395            other => {
2396                return Err(SidecarError::InvalidState(format!(
2397                    "unsupported UDP buffer size kind {other}"
2398                )));
2399            }
2400        }
2401        if self.kernel_socket_id.is_some() {
2402            return Ok(());
2403        }
2404        let socket = self.socket()?;
2405        let socket = SockRef::from(socket);
2406        match which {
2407            "recv" => socket.set_recv_buffer_size(size).map_err(sidecar_net_error),
2408            "send" => socket.set_send_buffer_size(size).map_err(sidecar_net_error),
2409            other => Err(SidecarError::InvalidState(format!(
2410                "unsupported UDP buffer size kind {other}"
2411            ))),
2412        }
2413    }
2414
2415    fn get_buffer_size(&self, which: &str) -> Result<usize, SidecarError> {
2416        if self.kernel_socket_id.is_some() {
2417            return Ok(match which {
2418                "recv" => self.recv_buffer_size,
2419                "send" => self.send_buffer_size,
2420                other => {
2421                    return Err(SidecarError::InvalidState(format!(
2422                        "unsupported UDP buffer size kind {other}"
2423                    )));
2424                }
2425            });
2426        }
2427        let socket = self.socket()?;
2428        let socket = SockRef::from(socket);
2429        match which {
2430            "recv" => socket.recv_buffer_size().map_err(sidecar_net_error),
2431            "send" => socket.send_buffer_size().map_err(sidecar_net_error),
2432            other => Err(SidecarError::InvalidState(format!(
2433                "unsupported UDP buffer size kind {other}"
2434            ))),
2435        }
2436    }
2437}
2438
2439// ActiveExecution, ActiveExecutionEvent, SocketQueryKind moved to crate::state
2440
2441impl ActiveExecution {
2442    pub(crate) fn uses_shared_v8_runtime(&self) -> bool {
2443        match self {
2444            Self::Javascript(execution) => execution.uses_shared_v8_runtime(),
2445            Self::Python(execution) => execution.uses_shared_v8_runtime(),
2446            Self::Wasm(execution) => execution.uses_shared_v8_runtime(),
2447            Self::Tool(_) => false,
2448        }
2449    }
2450
2451    pub(crate) fn child_pid(&self) -> u32 {
2452        match self {
2453            Self::Javascript(execution) => execution.child_pid(),
2454            Self::Python(execution) => execution.child_pid(),
2455            Self::Wasm(execution) => execution.child_pid(),
2456            Self::Tool(_) => 0,
2457        }
2458    }
2459
2460    pub(crate) fn write_stdin(&mut self, chunk: &[u8]) -> Result<(), SidecarError> {
2461        match self {
2462            Self::Javascript(execution) => execution
2463                .write_stdin(chunk)
2464                .map_err(|error| SidecarError::Execution(error.to_string())),
2465            Self::Python(execution) => execution
2466                .write_stdin(chunk)
2467                .map_err(|error| SidecarError::Execution(error.to_string())),
2468            Self::Wasm(execution) => execution
2469                .write_stdin(chunk)
2470                .map_err(|error| SidecarError::Execution(error.to_string())),
2471            Self::Tool(_) => Ok(()),
2472        }
2473    }
2474
2475    pub(crate) fn close_stdin(&mut self) -> Result<(), SidecarError> {
2476        match self {
2477            Self::Javascript(execution) => execution
2478                .close_stdin()
2479                .map_err(|error| SidecarError::Execution(error.to_string())),
2480            Self::Python(execution) => execution
2481                .close_stdin()
2482                .map_err(|error| SidecarError::Execution(error.to_string())),
2483            Self::Wasm(execution) => execution
2484                .close_stdin()
2485                .map_err(|error| SidecarError::Execution(error.to_string())),
2486            Self::Tool(_) => Ok(()),
2487        }
2488    }
2489
2490    pub(crate) fn respond_python_vfs_rpc_success(
2491        &mut self,
2492        id: u64,
2493        payload: PythonVfsRpcResponsePayload,
2494    ) -> Result<(), SidecarError> {
2495        match self {
2496            Self::Python(execution) => execution
2497                .respond_vfs_rpc_success(id, payload)
2498                .map_err(|error| SidecarError::Execution(error.to_string())),
2499            _ => Err(SidecarError::InvalidState(String::from(
2500                "only Python executions can service Python VFS RPC responses",
2501            ))),
2502        }
2503    }
2504
2505    pub(crate) fn respond_python_vfs_rpc_error(
2506        &mut self,
2507        id: u64,
2508        code: impl Into<String>,
2509        message: impl Into<String>,
2510    ) -> Result<(), SidecarError> {
2511        match self {
2512            Self::Python(execution) => execution
2513                .respond_vfs_rpc_error(id, code, message)
2514                .map_err(|error| SidecarError::Execution(error.to_string())),
2515            _ => Err(SidecarError::InvalidState(String::from(
2516                "only Python executions can service Python VFS RPC responses",
2517            ))),
2518        }
2519    }
2520
2521    pub(crate) fn send_javascript_stream_event(
2522        &self,
2523        event_type: &str,
2524        payload: Value,
2525    ) -> Result<(), SidecarError> {
2526        match self {
2527            Self::Javascript(execution) => execution
2528                .send_stream_event(event_type, payload)
2529                .map_err(|error| SidecarError::Execution(error.to_string())),
2530            Self::Wasm(execution) => execution
2531                .send_stream_event(event_type, payload)
2532                .map_err(|error| SidecarError::Execution(error.to_string())),
2533            _ => Err(SidecarError::InvalidState(String::from(
2534                "only embedded V8 executions can receive JavaScript stream events",
2535            ))),
2536        }
2537    }
2538
2539    pub(crate) fn javascript_v8_session_handle(&self) -> Option<V8SessionHandle> {
2540        match self {
2541            Self::Javascript(execution) => Some(execution.v8_session_handle()),
2542            Self::Wasm(execution) => Some(execution.v8_session_handle()),
2543            _ => None,
2544        }
2545    }
2546
2547    pub(crate) fn terminate(&mut self) -> Result<(), SidecarError> {
2548        match self {
2549            Self::Javascript(execution) => execution
2550                .terminate()
2551                .map_err(|error| SidecarError::Execution(error.to_string())),
2552            Self::Python(execution) => execution
2553                .kill()
2554                .map_err(|error| SidecarError::Execution(error.to_string())),
2555            Self::Wasm(execution) => execution
2556                .terminate()
2557                .map_err(|error| SidecarError::Execution(error.to_string())),
2558            Self::Tool(_) => Ok(()),
2559        }
2560    }
2561
2562    pub(crate) fn respond_javascript_sync_rpc_success(
2563        &mut self,
2564        id: u64,
2565        result: Value,
2566    ) -> Result<(), SidecarError> {
2567        match self {
2568            Self::Javascript(execution) => execution
2569                .respond_sync_rpc_success(id, result)
2570                .map_err(|error| SidecarError::Execution(error.to_string())),
2571            Self::Python(execution) => execution
2572                .respond_javascript_sync_rpc_success(id, result)
2573                .map_err(|error| SidecarError::Execution(error.to_string())),
2574            Self::Wasm(execution) => execution
2575                .respond_sync_rpc_success(id, result)
2576                .map_err(|error| SidecarError::Execution(error.to_string())),
2577            _ => Err(SidecarError::InvalidState(String::from(
2578                "only JavaScript, Python, and WebAssembly executions can service JavaScript sync RPC responses",
2579            ))),
2580        }
2581    }
2582
2583    pub(crate) fn respond_javascript_sync_rpc_error(
2584        &mut self,
2585        id: u64,
2586        code: impl Into<String>,
2587        message: impl Into<String>,
2588    ) -> Result<(), SidecarError> {
2589        match self {
2590            Self::Javascript(execution) => execution
2591                .respond_sync_rpc_error(id, code, message)
2592                .map_err(|error| SidecarError::Execution(error.to_string())),
2593            Self::Python(execution) => execution
2594                .respond_javascript_sync_rpc_error(id, code, message)
2595                .map_err(|error| SidecarError::Execution(error.to_string())),
2596            Self::Wasm(execution) => execution
2597                .respond_sync_rpc_error(id, code, message)
2598                .map_err(|error| SidecarError::Execution(error.to_string())),
2599            _ => Err(SidecarError::InvalidState(String::from(
2600                "only JavaScript, Python, and WebAssembly executions can service JavaScript sync RPC responses",
2601            ))),
2602        }
2603    }
2604
2605    pub(crate) async fn poll_event(
2606        &mut self,
2607        timeout: Duration,
2608    ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
2609        match self {
2610            Self::Javascript(execution) => execution
2611                .poll_event(timeout)
2612                .await
2613                .map(|event| {
2614                    event.map(|event| match event {
2615                        JavascriptExecutionEvent::Stdout(chunk) => {
2616                            ActiveExecutionEvent::Stdout(chunk)
2617                        }
2618                        JavascriptExecutionEvent::Stderr(chunk) => {
2619                            ActiveExecutionEvent::Stderr(chunk)
2620                        }
2621                        JavascriptExecutionEvent::SyncRpcRequest(request) => {
2622                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2623                        }
2624                        JavascriptExecutionEvent::SignalState {
2625                            signal,
2626                            registration,
2627                        } => ActiveExecutionEvent::SignalState {
2628                            signal,
2629                            registration: map_node_signal_registration(registration),
2630                        },
2631                        JavascriptExecutionEvent::Exited(code) => {
2632                            ActiveExecutionEvent::Exited(code)
2633                        }
2634                    })
2635                })
2636                .map_err(|error| SidecarError::Execution(error.to_string())),
2637            Self::Python(execution) => execution
2638                .poll_event(timeout)
2639                .await
2640                .map(|event| {
2641                    event.map(|event| match event {
2642                        PythonExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2643                        PythonExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2644                        PythonExecutionEvent::JavascriptSyncRpcRequest(request) => {
2645                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2646                        }
2647                        PythonExecutionEvent::VfsRpcRequest(request) => {
2648                            ActiveExecutionEvent::PythonVfsRpcRequest(request)
2649                        }
2650                        PythonExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2651                    })
2652                })
2653                .map_err(|error| SidecarError::Execution(error.to_string())),
2654            Self::Wasm(execution) => execution
2655                .poll_event(timeout)
2656                .await
2657                .map(|event| {
2658                    event.map(|event| match event {
2659                        WasmExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2660                        WasmExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2661                        WasmExecutionEvent::SyncRpcRequest(request) => {
2662                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2663                        }
2664                        WasmExecutionEvent::SignalState {
2665                            signal,
2666                            registration,
2667                        } => ActiveExecutionEvent::SignalState {
2668                            signal,
2669                            registration: map_wasm_signal_registration(registration),
2670                        },
2671                        WasmExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2672                    })
2673                })
2674                .map_err(|error| SidecarError::Execution(error.to_string())),
2675            Self::Tool(execution) => {
2676                let _ = timeout;
2677                poll_tool_process_event(execution)
2678            }
2679        }
2680    }
2681
2682    pub(crate) fn poll_event_blocking(
2683        &mut self,
2684        timeout: Duration,
2685    ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
2686        match self {
2687            Self::Javascript(execution) => execution
2688                .poll_event_blocking(timeout)
2689                .map(|event| {
2690                    event.map(|event| match event {
2691                        JavascriptExecutionEvent::Stdout(chunk) => {
2692                            ActiveExecutionEvent::Stdout(chunk)
2693                        }
2694                        JavascriptExecutionEvent::Stderr(chunk) => {
2695                            ActiveExecutionEvent::Stderr(chunk)
2696                        }
2697                        JavascriptExecutionEvent::SyncRpcRequest(request) => {
2698                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2699                        }
2700                        JavascriptExecutionEvent::SignalState {
2701                            signal,
2702                            registration,
2703                        } => ActiveExecutionEvent::SignalState {
2704                            signal,
2705                            registration: map_node_signal_registration(registration),
2706                        },
2707                        JavascriptExecutionEvent::Exited(code) => {
2708                            ActiveExecutionEvent::Exited(code)
2709                        }
2710                    })
2711                })
2712                .map_err(|error| SidecarError::Execution(error.to_string())),
2713            Self::Python(execution) => execution
2714                .poll_event_blocking(timeout)
2715                .map(|event| {
2716                    event.map(|event| match event {
2717                        PythonExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2718                        PythonExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2719                        PythonExecutionEvent::JavascriptSyncRpcRequest(request) => {
2720                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2721                        }
2722                        PythonExecutionEvent::VfsRpcRequest(request) => {
2723                            ActiveExecutionEvent::PythonVfsRpcRequest(request)
2724                        }
2725                        PythonExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2726                    })
2727                })
2728                .map_err(|error| SidecarError::Execution(error.to_string())),
2729            Self::Wasm(execution) => execution
2730                .poll_event_blocking(timeout)
2731                .map(|event| {
2732                    event.map(|event| match event {
2733                        WasmExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2734                        WasmExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2735                        WasmExecutionEvent::SyncRpcRequest(request) => {
2736                            ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2737                        }
2738                        WasmExecutionEvent::SignalState {
2739                            signal,
2740                            registration,
2741                        } => ActiveExecutionEvent::SignalState {
2742                            signal,
2743                            registration: map_wasm_signal_registration(registration),
2744                        },
2745                        WasmExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2746                    })
2747                })
2748                .map_err(|error| SidecarError::Execution(error.to_string())),
2749            Self::Tool(execution) => {
2750                let _ = timeout;
2751                poll_tool_process_event(execution)
2752            }
2753        }
2754    }
2755}
2756
2757struct ToolProcessEventRequest {
2758    sidecar_requests: SharedSidecarRequestClient,
2759    connection_id: String,
2760    session_id: String,
2761    vm_id: String,
2762    tool_resolution: ToolCommandResolution,
2763    cancelled: Arc<AtomicBool>,
2764    pending_events: Arc<Mutex<VecDeque<ActiveExecutionEvent>>>,
2765    events_overflowed: Arc<AtomicBool>,
2766}
2767
2768pub(crate) fn send_tool_process_event(
2769    pending_events: &Arc<Mutex<VecDeque<ActiveExecutionEvent>>>,
2770    events_overflowed: &AtomicBool,
2771    event: ActiveExecutionEvent,
2772) -> bool {
2773    let mut pending_events = pending_events
2774        .lock()
2775        .unwrap_or_else(|poisoned| poisoned.into_inner());
2776    if pending_events.len() >= MAX_PROCESS_EVENT_QUEUE {
2777        events_overflowed.store(true, Ordering::Relaxed);
2778        return false;
2779    }
2780    pending_events.push_back(event);
2781    true
2782}
2783
2784fn spawn_tool_process_events(request: ToolProcessEventRequest) {
2785    let ToolProcessEventRequest {
2786        sidecar_requests,
2787        connection_id,
2788        session_id,
2789        vm_id,
2790        tool_resolution,
2791        cancelled,
2792        pending_events,
2793        events_overflowed,
2794    } = request;
2795    std::thread::spawn(move || match tool_resolution {
2796        ToolCommandResolution::Failure(message) => {
2797            if !send_tool_process_event(
2798                &pending_events,
2799                &events_overflowed,
2800                ActiveExecutionEvent::Stderr(format_tool_failure_output(&message)),
2801            ) {
2802                return;
2803            }
2804            let _ = send_tool_process_event(
2805                &pending_events,
2806                &events_overflowed,
2807                ActiveExecutionEvent::Exited(1),
2808            );
2809        }
2810        ToolCommandResolution::Invoke { request, timeout } => {
2811            let response = sidecar_requests.invoke(
2812                OwnershipScope::vm(connection_id.clone(), session_id.clone(), vm_id.clone()),
2813                SidecarRequestPayload::HostCallback(request.clone()),
2814                timeout,
2815            );
2816            if cancelled.load(Ordering::Relaxed) {
2817                return;
2818            }
2819
2820            match response {
2821                Ok(crate::protocol::SidecarResponsePayload::HostCallbackResult(result)) => {
2822                    if let Some(value) = result.result {
2823                        let value: serde_json::Value = serde_json::from_str(&value)
2824                            .unwrap_or(serde_json::Value::String(value));
2825                        let stdout = serde_json::to_vec(&json!({
2826                            "ok": true,
2827                            "result": value,
2828                        }))
2829                        .unwrap_or_else(|error| {
2830                            format_tool_failure_output(&format!(
2831                                "failed to serialize tool result: {error}"
2832                            ))
2833                        });
2834                        if !send_tool_process_event(
2835                            &pending_events,
2836                            &events_overflowed,
2837                            ActiveExecutionEvent::Stdout(stdout),
2838                        ) {
2839                            return;
2840                        }
2841                        let _ = send_tool_process_event(
2842                            &pending_events,
2843                            &events_overflowed,
2844                            ActiveExecutionEvent::Exited(0),
2845                        );
2846                    } else {
2847                        let message = result
2848                            .error
2849                            .unwrap_or_else(|| String::from("tool invocation returned no result"));
2850                        if !send_tool_process_event(
2851                            &pending_events,
2852                            &events_overflowed,
2853                            ActiveExecutionEvent::Stderr(format_tool_failure_output(&message)),
2854                        ) {
2855                            return;
2856                        }
2857                        let _ = send_tool_process_event(
2858                            &pending_events,
2859                            &events_overflowed,
2860                            ActiveExecutionEvent::Exited(1),
2861                        );
2862                    }
2863                }
2864                Ok(_) => {
2865                    if !send_tool_process_event(
2866                        &pending_events,
2867                        &events_overflowed,
2868                        ActiveExecutionEvent::Stderr(format_tool_failure_output(
2869                            "unexpected sidecar tool response",
2870                        )),
2871                    ) {
2872                        return;
2873                    }
2874                    let _ = send_tool_process_event(
2875                        &pending_events,
2876                        &events_overflowed,
2877                        ActiveExecutionEvent::Exited(1),
2878                    );
2879                }
2880                Err(error) => {
2881                    if !send_tool_process_event(
2882                        &pending_events,
2883                        &events_overflowed,
2884                        ActiveExecutionEvent::Stderr(format_tool_failure_output(
2885                            &error.to_string(),
2886                        )),
2887                    ) {
2888                        return;
2889                    }
2890                    let _ = send_tool_process_event(
2891                        &pending_events,
2892                        &events_overflowed,
2893                        ActiveExecutionEvent::Exited(1),
2894                    );
2895                }
2896            }
2897        }
2898    });
2899}
2900
2901impl<B> NativeSidecar<B>
2902where
2903    B: NativeSidecarBridge + Send + 'static,
2904    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
2905{
2906    pub(crate) async fn execute(
2907        &mut self,
2908        request: &RequestFrame,
2909        payload: ExecuteRequest,
2910    ) -> Result<DispatchResult, SidecarError> {
2911        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
2912        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
2913
2914        let vm = self
2915            .vms
2916            .get_mut(&vm_id)
2917            .ok_or_else(|| missing_vm_error(&vm_id))?;
2918        if vm.active_processes.contains_key(&payload.process_id) {
2919            return Err(SidecarError::InvalidState(format!(
2920                "VM {vm_id} already has an active process with id {}",
2921                payload.process_id
2922            )));
2923        }
2924
2925        if let Some(command) = payload.command.as_deref() {
2926            if let Some(tool_resolution) =
2927                resolve_tool_command(vm, command, &payload.args, payload.cwd.as_deref())?
2928            {
2929                let guest_cwd = payload
2930                    .cwd
2931                    .as_deref()
2932                    .map(normalize_path)
2933                    .unwrap_or_else(|| vm.guest_cwd.clone());
2934                let kernel_handle = vm
2935                    .kernel
2936                    .create_virtual_process(
2937                        EXECUTION_DRIVER_NAME,
2938                        TOOL_DRIVER_NAME,
2939                        command,
2940                        std::iter::once(command.to_owned())
2941                            .chain(payload.args.iter().cloned())
2942                            .collect(),
2943                        VirtualProcessOptions {
2944                            env: vm.guest_env.clone(),
2945                            cwd: Some(guest_cwd.clone()),
2946                            ..VirtualProcessOptions::default()
2947                        },
2948                    )
2949                    .map_err(kernel_error)?;
2950                let kernel_pid = kernel_handle.pid();
2951                let tool_execution = ToolExecution::default();
2952                let cancelled = tool_execution.cancelled.clone();
2953                let pending_events = tool_execution.pending_events.clone();
2954                let events_overflowed = tool_execution.events_overflowed.clone();
2955                vm.active_processes.insert(
2956                    payload.process_id.clone(),
2957                    ActiveProcess::new(
2958                        kernel_pid,
2959                        kernel_handle,
2960                        GuestRuntimeKind::JavaScript,
2961                        ActiveExecution::Tool(tool_execution),
2962                    )
2963                    .with_guest_cwd(guest_cwd.clone())
2964                    .with_host_cwd(resolve_vm_guest_path_to_host(vm, &guest_cwd)),
2965                );
2966                self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy)?;
2967                spawn_tool_process_events(ToolProcessEventRequest {
2968                    sidecar_requests: self.sidecar_requests.clone(),
2969                    connection_id: connection_id.clone(),
2970                    session_id: session_id.clone(),
2971                    vm_id: vm_id.clone(),
2972                    tool_resolution,
2973                    cancelled,
2974                    pending_events,
2975                    events_overflowed,
2976                });
2977
2978                return Ok(DispatchResult {
2979                    response: self.respond(
2980                        request,
2981                        ResponsePayload::ProcessStarted(ProcessStartedResponse {
2982                            process_id: payload.process_id,
2983                            pid: Some(kernel_pid),
2984                        }),
2985                    ),
2986                    events: Vec::new(),
2987                });
2988            }
2989        }
2990
2991        let resolved = resolve_execute_request(vm, &payload)?;
2992        let mut env = resolved.env.clone();
2993        let sandbox_root = normalize_host_path(&vm.cwd);
2994        env.insert(
2995            String::from(EXECUTION_SANDBOX_ROOT_ENV),
2996            sandbox_root.to_string_lossy().into_owned(),
2997        );
2998        if resolved.runtime == GuestRuntimeKind::JavaScript {
2999            env.insert(
3000                String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
3001                String::from("1"),
3002            );
3003        } else if resolved.runtime == GuestRuntimeKind::WebAssembly {
3004            env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
3005        }
3006        let argv = std::iter::once(resolved.entrypoint.clone())
3007            .chain(resolved.execution_args.iter().cloned())
3008            .collect::<Vec<_>>();
3009        let kernel_handle = vm
3010            .kernel
3011            .spawn_process(
3012                &resolved.command,
3013                argv,
3014                SpawnOptions {
3015                    requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
3016                    cwd: Some(resolved.guest_cwd.clone()),
3017                    ..SpawnOptions::default()
3018                },
3019            )
3020            .map_err(kernel_error)?;
3021        let kernel_pid = kernel_handle.pid();
3022
3023        let (execution, process_env) = match resolved.runtime {
3024            GuestRuntimeKind::JavaScript => {
3025                let inline_code = load_javascript_entrypoint_source(
3026                    vm,
3027                    &resolved.host_cwd,
3028                    &resolved.entrypoint,
3029                    &env,
3030                );
3031                prepare_javascript_shadow(vm, &resolved)?;
3032
3033                let context =
3034                    self.javascript_engine
3035                        .create_context(CreateJavascriptContextRequest {
3036                            vm_id: vm_id.clone(),
3037                            bootstrap_module: None,
3038                            compile_cache_root: Some(self.cache_root.join("node-compile-cache")),
3039                        });
3040                let module_reader = build_module_reader(vm)
3041                    .map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
3042                let execution = self
3043                    .javascript_engine
3044                    .start_execution_with_module_reader(
3045                        StartJavascriptExecutionRequest {
3046                            vm_id: vm_id.clone(),
3047                            context_id: context.context_id,
3048                            argv: std::iter::once(resolved.entrypoint.clone())
3049                                .chain(resolved.execution_args.iter().cloned())
3050                                .collect(),
3051                            env: env.clone(),
3052                            cwd: resolved.host_cwd.clone(),
3053                            inline_code,
3054                        },
3055                        module_reader,
3056                    )
3057                    .map_err(javascript_error)?;
3058                (ActiveExecution::Javascript(execution), env.clone())
3059            }
3060            GuestRuntimeKind::Python => {
3061                let python_file_path = python_file_entrypoint(&resolved.entrypoint);
3062                let pyodide_dist_path = self
3063                    .python_engine
3064                    .bundled_pyodide_dist_path_for_vm(&vm_id)
3065                    .map_err(python_error)?;
3066                let pyodide_cache_path = pyodide_dist_path
3067                    .parent()
3068                    .and_then(Path::parent)
3069                    .unwrap_or(pyodide_dist_path.as_path())
3070                    .join("pyodide-package-cache");
3071                add_runtime_guest_path_mapping(
3072                    &mut env,
3073                    PYTHON_PYODIDE_GUEST_ROOT,
3074                    &pyodide_dist_path,
3075                );
3076                add_runtime_guest_path_mapping(
3077                    &mut env,
3078                    PYTHON_PYODIDE_CACHE_GUEST_ROOT,
3079                    &pyodide_cache_path,
3080                );
3081                add_runtime_host_access_path(
3082                    &mut env,
3083                    "AGENT_OS_EXTRA_FS_READ_PATHS",
3084                    &pyodide_dist_path,
3085                    true,
3086                );
3087                add_runtime_host_access_path(
3088                    &mut env,
3089                    "AGENT_OS_EXTRA_FS_READ_PATHS",
3090                    &pyodide_cache_path,
3091                    true,
3092                );
3093                add_runtime_host_access_path(
3094                    &mut env,
3095                    "AGENT_OS_EXTRA_FS_WRITE_PATHS",
3096                    &pyodide_cache_path,
3097                    false,
3098                );
3099                let context = self
3100                    .python_engine
3101                    .create_context(CreatePythonContextRequest {
3102                        vm_id: vm_id.clone(),
3103                        pyodide_dist_path,
3104                    });
3105                let execution = self
3106                    .python_engine
3107                    .start_execution(StartPythonExecutionRequest {
3108                        vm_id: vm_id.clone(),
3109                        context_id: context.context_id,
3110                        code: resolved.entrypoint.clone(),
3111                        file_path: python_file_path,
3112                        env: env.clone(),
3113                        cwd: resolved.host_cwd.clone(),
3114                    })
3115                    .map_err(python_error)?;
3116                (ActiveExecution::Python(execution), env.clone())
3117            }
3118            GuestRuntimeKind::WebAssembly => {
3119                env.insert(
3120                    String::from("AGENT_OS_VIRTUAL_PROCESS_PID"),
3121                    kernel_pid.to_string(),
3122                );
3123                env.insert(
3124                    String::from("AGENT_OS_VIRTUAL_PROCESS_PPID"),
3125                    String::from("0"),
3126                );
3127                apply_wasm_limit_env(&mut env, vm.kernel.resource_limits());
3128                let wasm_permission_tier = resolved.wasm_permission_tier.unwrap_or_else(|| {
3129                    resolve_wasm_permission_tier(
3130                        vm,
3131                        Some(&resolved.command),
3132                        None,
3133                        &resolved.entrypoint,
3134                    )
3135                });
3136                let context = self.wasm_engine.create_context(CreateWasmContextRequest {
3137                    vm_id: vm_id.clone(),
3138                    module_path: Some(resolved.entrypoint.clone()),
3139                });
3140                let execution = self
3141                    .wasm_engine
3142                    .start_execution(StartWasmExecutionRequest {
3143                        vm_id: vm_id.clone(),
3144                        context_id: context.context_id,
3145                        argv: resolved.process_args.clone(),
3146                        env: env.clone(),
3147                        cwd: resolved.host_cwd.clone(),
3148                        permission_tier: execution_wasm_permission_tier(wasm_permission_tier),
3149                    })
3150                    .map_err(wasm_error)?;
3151                (ActiveExecution::Wasm(execution), env)
3152            }
3153        };
3154        let child_pid = execution.child_pid();
3155        let kernel_stdin_writer_fd = install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?;
3156        vm.active_processes.insert(
3157            payload.process_id.clone(),
3158            ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
3159                .with_kernel_stdin_writer_fd(kernel_stdin_writer_fd)
3160                .with_guest_cwd(resolved.guest_cwd.clone())
3161                .with_env(process_env)
3162                .with_host_cwd(resolved.host_cwd.clone()),
3163        );
3164        self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy)?;
3165
3166        Ok(DispatchResult {
3167            response: self.respond(
3168                request,
3169                ResponsePayload::ProcessStarted(ProcessStartedResponse {
3170                    process_id: payload.process_id,
3171                    pid: Some(if child_pid == 0 {
3172                        kernel_pid
3173                    } else {
3174                        child_pid
3175                    }),
3176                }),
3177            ),
3178            events: Vec::new(),
3179        })
3180    }
3181
3182    pub(crate) async fn write_stdin(
3183        &mut self,
3184        request: &RequestFrame,
3185        payload: WriteStdinRequest,
3186    ) -> Result<DispatchResult, SidecarError> {
3187        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3188        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3189
3190        let vm = self
3191            .vms
3192            .get_mut(&vm_id)
3193            .ok_or_else(|| missing_vm_error(&vm_id))?;
3194        let process = vm
3195            .active_processes
3196            .get_mut(&payload.process_id)
3197            .ok_or_else(|| {
3198                SidecarError::InvalidState(format!(
3199                    "VM {vm_id} has no active process {}",
3200                    payload.process_id
3201                ))
3202            })?;
3203        process.execution.write_stdin(&payload.chunk)?;
3204        write_kernel_process_stdin(&mut vm.kernel, process, &payload.chunk)?;
3205
3206        Ok(DispatchResult {
3207            response: self.respond(
3208                request,
3209                ResponsePayload::StdinWritten(StdinWrittenResponse {
3210                    process_id: payload.process_id,
3211                    accepted_bytes: payload.chunk.len() as u64,
3212                }),
3213            ),
3214            events: Vec::new(),
3215        })
3216    }
3217
3218    pub(crate) async fn close_stdin(
3219        &mut self,
3220        request: &RequestFrame,
3221        payload: CloseStdinRequest,
3222    ) -> Result<DispatchResult, SidecarError> {
3223        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3224        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3225
3226        let vm = self
3227            .vms
3228            .get_mut(&vm_id)
3229            .ok_or_else(|| missing_vm_error(&vm_id))?;
3230        let process = vm
3231            .active_processes
3232            .get_mut(&payload.process_id)
3233            .ok_or_else(|| {
3234                SidecarError::InvalidState(format!(
3235                    "VM {vm_id} has no active process {}",
3236                    payload.process_id
3237                ))
3238            })?;
3239        process.execution.close_stdin()?;
3240        close_kernel_process_stdin(&mut vm.kernel, process)?;
3241
3242        Ok(DispatchResult {
3243            response: self.respond(
3244                request,
3245                ResponsePayload::StdinClosed(StdinClosedResponse {
3246                    process_id: payload.process_id,
3247                }),
3248            ),
3249            events: Vec::new(),
3250        })
3251    }
3252
3253    pub(crate) async fn kill_process(
3254        &mut self,
3255        request: &RequestFrame,
3256        payload: KillProcessRequest,
3257    ) -> Result<DispatchResult, SidecarError> {
3258        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3259        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3260        self.kill_process_internal(&vm_id, &payload.process_id, &payload.signal)?;
3261
3262        Ok(DispatchResult {
3263            response: self.respond(
3264                request,
3265                ResponsePayload::ProcessKilled(ProcessKilledResponse {
3266                    process_id: payload.process_id,
3267                }),
3268            ),
3269            events: Vec::new(),
3270        })
3271    }
3272
3273    pub(crate) async fn find_listener(
3274        &mut self,
3275        request: &RequestFrame,
3276        payload: FindListenerRequest,
3277    ) -> Result<DispatchResult, SidecarError> {
3278        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3279        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3280        require_vm_inspection_permission(
3281            &self.bridge,
3282            &vm_id,
3283            "network.inspect",
3284            "network",
3285            &socket_query_resource(SocketQueryKind::TcpListener, &payload),
3286        )?;
3287
3288        let listener =
3289            find_socket_state_entry(self.vms.get(&vm_id), SocketQueryKind::TcpListener, &payload)?;
3290
3291        Ok(DispatchResult {
3292            response: self.respond(
3293                request,
3294                ResponsePayload::ListenerSnapshot(ListenerSnapshotResponse { listener }),
3295            ),
3296            events: Vec::new(),
3297        })
3298    }
3299
3300    pub(crate) async fn get_process_snapshot(
3301        &mut self,
3302        request: &RequestFrame,
3303        _payload: GetProcessSnapshotRequest,
3304    ) -> Result<DispatchResult, SidecarError> {
3305        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3306        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3307        require_vm_inspection_permission(
3308            &self.bridge,
3309            &vm_id,
3310            "process.inspect",
3311            "process",
3312            "process://snapshot",
3313        )?;
3314
3315        let processes = self
3316            .vms
3317            .get_mut(&vm_id)
3318            .map(|vm| {
3319                prune_exited_process_snapshots(vm);
3320                snapshot_vm_processes(vm)
3321            })
3322            .unwrap_or_default();
3323
3324        Ok(DispatchResult {
3325            response: self.respond(
3326                request,
3327                ResponsePayload::ProcessSnapshot(ProcessSnapshotResponse { processes }),
3328            ),
3329            events: Vec::new(),
3330        })
3331    }
3332
3333    pub(crate) async fn find_bound_udp(
3334        &mut self,
3335        request: &RequestFrame,
3336        payload: FindBoundUdpRequest,
3337    ) -> Result<DispatchResult, SidecarError> {
3338        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3339        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3340
3341        let lookup_request = FindListenerRequest {
3342            host: payload.host,
3343            port: payload.port,
3344            path: None,
3345        };
3346        require_vm_inspection_permission(
3347            &self.bridge,
3348            &vm_id,
3349            "network.inspect",
3350            "network",
3351            &socket_query_resource(SocketQueryKind::UdpBound, &lookup_request),
3352        )?;
3353        let socket = find_socket_state_entry(
3354            self.vms.get(&vm_id),
3355            SocketQueryKind::UdpBound,
3356            &lookup_request,
3357        )?;
3358
3359        Ok(DispatchResult {
3360            response: self.respond(
3361                request,
3362                ResponsePayload::BoundUdpSnapshot(BoundUdpSnapshotResponse { socket }),
3363            ),
3364            events: Vec::new(),
3365        })
3366    }
3367
3368    pub(crate) async fn vm_fetch(
3369        &mut self,
3370        request: &RequestFrame,
3371        payload: VmFetchRequest,
3372    ) -> Result<DispatchResult, SidecarError> {
3373        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3374        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3375
3376        let vm = self
3377            .vms
3378            .get_mut(&vm_id)
3379            .ok_or_else(|| SidecarError::InvalidState(String::from("unknown sidecar VM")))?;
3380        let target_path = if payload.path.starts_with('/') {
3381            payload.path.clone()
3382        } else {
3383            format!("/{}", payload.path)
3384        };
3385        let request_url = Url::parse(&format!("http://127.0.0.1:{}{target_path}", payload.port))
3386            .map_err(|error| {
3387                SidecarError::InvalidState(format!(
3388                    "invalid vm.fetch target {target_path:?}: {error}"
3389                ))
3390            })?;
3391        let header_values: BTreeMap<String, Value> = serde_json::from_str(&payload.headers_json)
3392            .map_err(|error| {
3393                SidecarError::InvalidState(format!(
3394                    "vm.fetch headers_json must be valid JSON: {error}"
3395                ))
3396            })?;
3397        let options = JavascriptHttpRequestOptions {
3398            method: Some(payload.method),
3399            headers: header_values,
3400            body: payload.body,
3401            reject_unauthorized: None,
3402        };
3403        let headers = parse_http_header_collection(&options.headers, "vm.fetch headers")?;
3404        let Some((target_process_id, server_id)) =
3405            vm.active_processes
3406                .iter()
3407                .find_map(|(process_id, process)| {
3408                    process
3409                        .http_servers
3410                        .iter()
3411                        .find(|(_, server)| server.guest_local_addr.port() == payload.port)
3412                        .map(|(server_id, _)| (process_id.clone(), *server_id))
3413                })
3414        else {
3415            return Err(SidecarError::Execution(format!(
3416                "vm.fetch could not find a guest HTTP listener on port {}",
3417                payload.port
3418            )));
3419        };
3420        let socket_paths = build_javascript_socket_path_context(vm)?;
3421        let resource_limits = vm.kernel.resource_limits().clone();
3422        let process = vm
3423            .active_processes
3424            .get_mut(&target_process_id)
3425            .ok_or_else(|| {
3426                SidecarError::InvalidState(format!(
3427                    "vm.fetch target process disappeared: {target_process_id}"
3428                ))
3429            })?;
3430        let request_json = serialize_http_loopback_request(&request_url, &options, &headers)?;
3431        let request_id = {
3432            let server = process.http_servers.get_mut(&server_id).ok_or_else(|| {
3433                SidecarError::InvalidState(format!(
3434                    "vm.fetch target server disappeared: {server_id}"
3435                ))
3436            })?;
3437            server.next_request_id += 1;
3438            server.next_request_id
3439        };
3440        process
3441            .pending_http_requests
3442            .insert((server_id, request_id), None);
3443        process.execution.send_javascript_stream_event(
3444            "http_request",
3445            json!({
3446                "serverId": server_id,
3447                "requestId": request_id,
3448                "request": request_json,
3449            }),
3450        )?;
3451        let response_json = wait_for_loopback_http_response(LoopbackHttpResponseWaitRequest {
3452            bridge: &self.bridge,
3453            vm_id: &vm_id,
3454            dns: &vm.dns,
3455            socket_paths: &socket_paths,
3456            kernel: &mut vm.kernel,
3457            process,
3458            resource_limits: &resource_limits,
3459            request_key: (server_id, request_id),
3460        })?;
3461
3462        let response = self.respond(
3463            request,
3464            ResponsePayload::VmFetchResult(VmFetchResponse { response_json }),
3465        );
3466        ensure_vm_fetch_response_frame_within_limit(&response, self.config.max_frame_bytes)?;
3467
3468        Ok(DispatchResult {
3469            response,
3470            events: Vec::new(),
3471        })
3472    }
3473
3474    pub(crate) async fn get_signal_state(
3475        &mut self,
3476        request: &RequestFrame,
3477        payload: GetSignalStateRequest,
3478    ) -> Result<DispatchResult, SidecarError> {
3479        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3480        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3481
3482        let handlers = self
3483            .vms
3484            .get(&vm_id)
3485            .and_then(|vm| vm.signal_states.get(&payload.process_id))
3486            .cloned()
3487            .unwrap_or_default();
3488
3489        Ok(DispatchResult {
3490            response: self.respond(
3491                request,
3492                ResponsePayload::SignalState(SignalStateResponse {
3493                    process_id: payload.process_id,
3494                    handlers: handlers.into_iter().collect(),
3495                }),
3496            ),
3497            events: Vec::new(),
3498        })
3499    }
3500
3501    pub(crate) async fn get_zombie_timer_count(
3502        &mut self,
3503        request: &RequestFrame,
3504        _payload: GetZombieTimerCountRequest,
3505    ) -> Result<DispatchResult, SidecarError> {
3506        let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3507        self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3508
3509        let count = self
3510            .vms
3511            .get(&vm_id)
3512            .map(|vm| vm.kernel.zombie_timer_count() as u64)
3513            .unwrap_or_default();
3514
3515        Ok(DispatchResult {
3516            response: self.respond(
3517                request,
3518                ResponsePayload::ZombieTimerCount(ZombieTimerCountResponse { count }),
3519            ),
3520            events: Vec::new(),
3521        })
3522    }
3523
3524    pub(crate) fn kill_process_internal(
3525        &mut self,
3526        vm_id: &str,
3527        process_id: &str,
3528        signal: &str,
3529    ) -> Result<(), SidecarError> {
3530        let signal_name = signal.to_owned();
3531        let signal = parse_signal(signal)?;
3532        let vm = self
3533            .vms
3534            .get_mut(vm_id)
3535            .ok_or_else(|| SidecarError::InvalidState(format!("unknown sidecar VM {vm_id}")))?;
3536        let process = vm.active_processes.get_mut(process_id).ok_or_else(|| {
3537            SidecarError::InvalidState(format!("VM {vm_id} has no active process {process_id}"))
3538        })?;
3539        let kernel_pid = process.kernel_pid;
3540
3541        enum KillBehavior {
3542            Tool,
3543            SharedV8StateOnly,
3544            SharedV8Continue,
3545            SharedV8Terminate,
3546            SharedV8DispatchOrTerminate,
3547            Noop,
3548            HostPid(u32),
3549        }
3550
3551        let behavior = match &process.execution {
3552            ActiveExecution::Tool(_) => KillBehavior::Tool,
3553            ActiveExecution::Javascript(execution)
3554                if execution.uses_shared_v8_runtime() && matches!(signal, 0 | libc::SIGSTOP) =>
3555            {
3556                KillBehavior::SharedV8StateOnly
3557            }
3558            ActiveExecution::Javascript(execution)
3559                if execution.uses_shared_v8_runtime() && signal == libc::SIGCONT =>
3560            {
3561                KillBehavior::SharedV8Continue
3562            }
3563            ActiveExecution::Wasm(execution)
3564                if execution.uses_shared_v8_runtime()
3565                    && matches!(signal, 0 | libc::SIGSTOP | libc::SIGCONT) =>
3566            {
3567                KillBehavior::SharedV8StateOnly
3568            }
3569            ActiveExecution::Python(execution)
3570                if execution.uses_shared_v8_runtime()
3571                    && matches!(signal, 0 | libc::SIGSTOP | libc::SIGCONT) =>
3572            {
3573                KillBehavior::SharedV8StateOnly
3574            }
3575            ActiveExecution::Javascript(execution)
3576                if execution.uses_shared_v8_runtime() && signal == SIGKILL =>
3577            {
3578                KillBehavior::SharedV8Terminate
3579            }
3580            ActiveExecution::Wasm(execution)
3581                if execution.uses_shared_v8_runtime() && signal == SIGKILL =>
3582            {
3583                KillBehavior::SharedV8Terminate
3584            }
3585            ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime() => {
3586                KillBehavior::SharedV8DispatchOrTerminate
3587            }
3588            ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime() => {
3589                KillBehavior::SharedV8Terminate
3590            }
3591            ActiveExecution::Python(execution) if execution.uses_shared_v8_runtime() => {
3592                KillBehavior::SharedV8Terminate
3593            }
3594            ActiveExecution::Javascript(execution) if execution.child_pid() == 0 => {
3595                KillBehavior::Noop
3596            }
3597            _ => KillBehavior::HostPid(process.execution.child_pid()),
3598        };
3599
3600        match behavior {
3601            KillBehavior::Tool => {
3602                let ActiveExecution::Tool(execution) = &process.execution else {
3603                    unreachable!("kill behavior must match tool execution");
3604                };
3605                if signal != 0 {
3606                    execution.cancelled.store(true, Ordering::Relaxed);
3607                    process.queue_pending_execution_event(ActiveExecutionEvent::Exited(
3608                        128 + signal,
3609                    ))?;
3610                }
3611            }
3612            KillBehavior::SharedV8StateOnly => {
3613                if matches!(signal, libc::SIGSTOP | libc::SIGCONT) {
3614                    vm.kernel
3615                        .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal)
3616                        .map_err(kernel_error)?;
3617                }
3618            }
3619            KillBehavior::SharedV8Continue => {
3620                vm.kernel
3621                    .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal)
3622                    .map_err(kernel_error)?;
3623                if signal != 0 && !dispatch_v8_process_signal(process, signal)? {
3624                    process.execution.terminate()?;
3625                }
3626            }
3627            KillBehavior::SharedV8Terminate => {
3628                if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
3629                    close_kernel_process_stdin(&mut vm.kernel, process)?;
3630                }
3631                process.execution.terminate()?;
3632                let needs_synthetic_exit = matches!(process.execution, ActiveExecution::Wasm(_))
3633                    || (signal == SIGKILL
3634                        && matches!(process.execution, ActiveExecution::Javascript(_)));
3635                if signal != 0 && needs_synthetic_exit {
3636                    process.queue_pending_execution_event(ActiveExecutionEvent::Exited(
3637                        128 + signal,
3638                    ))?;
3639                }
3640            }
3641            KillBehavior::SharedV8DispatchOrTerminate => {
3642                if signal != 0 && !dispatch_v8_process_signal(process, signal)? {
3643                    process.execution.terminate()?;
3644                }
3645            }
3646            KillBehavior::Noop => {}
3647            KillBehavior::HostPid(pid) => {
3648                if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
3649                    close_kernel_process_stdin(&mut vm.kernel, process)?;
3650                }
3651                signal_runtime_process(pid, signal)?;
3652            }
3653        }
3654        emit_security_audit_event(
3655            &self.bridge,
3656            vm_id,
3657            "security.process.kill",
3658            audit_fields([
3659                (String::from("source"), String::from("control_plane")),
3660                (String::from("source_pid"), String::from("0")),
3661                (String::from("target_pid"), process.kernel_pid.to_string()),
3662                (String::from("process_id"), process_id.to_owned()),
3663                (String::from("signal"), signal_name),
3664                (
3665                    String::from("host_pid"),
3666                    process.execution.child_pid().to_string(),
3667                ),
3668            ]),
3669        );
3670        Ok(())
3671    }
3672
3673    pub async fn pump_process_events(
3674        &mut self,
3675        ownership: &OwnershipScope,
3676    ) -> Result<bool, SidecarError> {
3677        let mut emitted_any = false;
3678
3679        let mut queued_envelopes = Vec::new();
3680        {
3681            let pending_capacity = self.pending_process_event_capacity();
3682            let receiver = self.process_event_receiver.as_mut().ok_or_else(|| {
3683                SidecarError::InvalidState(String::from("process event receiver unavailable"))
3684            })?;
3685            loop {
3686                if queued_envelopes.len() >= pending_capacity {
3687                    if receiver.is_empty() {
3688                        break;
3689                    }
3690                    return Err(process_event_queue_overflow_error());
3691                }
3692                match receiver.try_recv() {
3693                    Ok(envelope) => {
3694                        queued_envelopes.push(envelope);
3695                        emitted_any = true;
3696                    }
3697                    Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
3698                    Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
3699                }
3700            }
3701        }
3702        for envelope in queued_envelopes {
3703            self.queue_pending_process_event(envelope)?;
3704        }
3705
3706        let vm_ids = self.vm_ids_for_scope(ownership)?;
3707        for vm_id in vm_ids {
3708            while let Some(vm) = self.vms.get(&vm_id) {
3709                let connection_id = vm.connection_id.clone();
3710                let session_id = vm.session_id.clone();
3711                let process_ids = self
3712                    .vms
3713                    .get(&vm_id)
3714                    .map(|vm| vm.active_processes.keys().cloned().collect::<Vec<_>>())
3715                    .unwrap_or_default();
3716                let mut emitted_this_pass = false;
3717
3718                for process_id in process_ids {
3719                    if self
3720                        .vms
3721                        .get(&vm_id)
3722                        .is_some_and(|vm| vm.detached_child_processes.contains(&process_id))
3723                    {
3724                        continue;
3725                    }
3726                    enum ProcessPollResult {
3727                        Event(Box<Option<ActiveExecutionEvent>>),
3728                        RecoverClosedChannel,
3729                    }
3730                    let poll_result = {
3731                        let Some(vm) = self.vms.get_mut(&vm_id) else {
3732                            continue;
3733                        };
3734                        let Some(process) = vm.active_processes.get_mut(&process_id) else {
3735                            continue;
3736                        };
3737                        if let Some(event) = process.pending_execution_events.pop_front() {
3738                            ProcessPollResult::Event(Box::new(Some(event)))
3739                        } else {
3740                            match process.execution.poll_event(Duration::ZERO).await {
3741                                Ok(event) => ProcessPollResult::Event(Box::new(event)),
3742                                Err(SidecarError::Execution(message))
3743                                    if (process.runtime == GuestRuntimeKind::JavaScript
3744                                        && closed_javascript_event_channel(&message))
3745                                        || (process.runtime == GuestRuntimeKind::Python
3746                                            && closed_python_event_channel(&message))
3747                                        || (process.runtime == GuestRuntimeKind::WebAssembly
3748                                            && closed_wasm_event_channel(&message)) =>
3749                                {
3750                                    ProcessPollResult::RecoverClosedChannel
3751                                }
3752                                Err(other) => return Err(other),
3753                            }
3754                        }
3755                    };
3756                    let event = match poll_result {
3757                        ProcessPollResult::Event(event) => *event,
3758                        ProcessPollResult::RecoverClosedChannel => {
3759                            self.recover_closed_root_runtime_process_event(&vm_id, &process_id)?
3760                        }
3761                    };
3762
3763                    let Some(event) = event else {
3764                        continue;
3765                    };
3766
3767                    self.queue_pending_process_event(ProcessEventEnvelope {
3768                        connection_id: connection_id.clone(),
3769                        session_id: session_id.clone(),
3770                        vm_id: vm_id.clone(),
3771                        process_id: process_id.clone(),
3772                        event,
3773                    })?;
3774                    emitted_any = true;
3775                    emitted_this_pass = true;
3776                }
3777
3778                if !emitted_this_pass {
3779                    break;
3780                }
3781            }
3782
3783            if self.pump_detached_child_process_events(&vm_id)? {
3784                emitted_any = true;
3785            }
3786        }
3787
3788        Ok(emitted_any)
3789    }
3790
3791    fn recover_closed_root_runtime_process_event(
3792        &mut self,
3793        vm_id: &str,
3794        process_id: &str,
3795    ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
3796        let Some(vm) = self.vms.get_mut(vm_id) else {
3797            return Ok(None);
3798        };
3799        let Some(process) = vm.active_processes.get(process_id) else {
3800            return Ok(None);
3801        };
3802        if process.execution.uses_shared_v8_runtime() {
3803            return Ok(None);
3804        }
3805        if process.runtime != GuestRuntimeKind::JavaScript
3806            && process.runtime != GuestRuntimeKind::Python
3807            && process.runtime != GuestRuntimeKind::WebAssembly
3808        {
3809            return Ok(None);
3810        }
3811        let runtime_child_pid = process.execution.child_pid();
3812        if runtime_child_pid == 0 {
3813            return Ok(None);
3814        }
3815        if let Some(status) = runtime_child_exit_status(runtime_child_pid)? {
3816            return Ok(Some(ActiveExecutionEvent::Exited(status)));
3817        }
3818        if runtime_child_is_alive(runtime_child_pid)? {
3819            return Ok(None);
3820        }
3821        Ok(Some(ActiveExecutionEvent::Exited(0)))
3822    }
3823
3824    fn active_process_by_path<'a>(
3825        process: &'a ActiveProcess,
3826        child_path: &[&str],
3827    ) -> Option<&'a ActiveProcess> {
3828        let mut current = process;
3829        for child_id in child_path {
3830            current = current.child_processes.get(*child_id)?;
3831        }
3832        Some(current)
3833    }
3834
3835    fn active_process_by_path_mut<'a>(
3836        process: &'a mut ActiveProcess,
3837        child_path: &[&str],
3838    ) -> Option<&'a mut ActiveProcess> {
3839        let mut current = process;
3840        for child_id in child_path {
3841            current = current.child_processes.get_mut(*child_id)?;
3842        }
3843        Some(current)
3844    }
3845
3846    fn active_process_by_owned_path_mut<'a>(
3847        process: &'a mut ActiveProcess,
3848        child_path: &[String],
3849    ) -> Option<&'a mut ActiveProcess> {
3850        let mut current = process;
3851        for child_id in child_path {
3852            current = current.child_processes.get_mut(child_id)?;
3853        }
3854        Some(current)
3855    }
3856
3857    fn active_process_path_by_kernel_pid(
3858        process: &ActiveProcess,
3859        kernel_pid: u32,
3860    ) -> Option<Vec<String>> {
3861        if process.kernel_pid == kernel_pid {
3862            return Some(Vec::new());
3863        }
3864
3865        for (child_id, child) in &process.child_processes {
3866            let Some(mut path) = Self::active_process_path_by_kernel_pid(child, kernel_pid) else {
3867                continue;
3868            };
3869            path.insert(0, child_id.clone());
3870            return Some(path);
3871        }
3872
3873        None
3874    }
3875
3876    fn descendant_parent_process<'a>(
3877        vm: &'a VmState,
3878        process_id: &str,
3879        child_path: &[&str],
3880    ) -> Option<&'a ActiveProcess> {
3881        let root = vm.active_processes.get(process_id)?;
3882        Self::active_process_by_path(root, child_path)
3883    }
3884
3885    fn descendant_parent_process_mut<'a>(
3886        vm: &'a mut VmState,
3887        process_id: &str,
3888        child_path: &[&str],
3889    ) -> Option<&'a mut ActiveProcess> {
3890        let root = vm.active_processes.get_mut(process_id)?;
3891        Self::active_process_by_path_mut(root, child_path)
3892    }
3893
3894    fn child_process_path_label(process_id: &str, child_path: &[&str]) -> String {
3895        if child_path.is_empty() {
3896            process_id.to_owned()
3897        } else {
3898            format!("{process_id}/{}", child_path.join("/"))
3899        }
3900    }
3901
3902    fn adopt_detached_child_processes(
3903        current_process_id: &str,
3904        process: &mut ActiveProcess,
3905    ) -> Vec<(String, ActiveProcess)> {
3906        let mut adopted = Vec::new();
3907        let child_ids = process.child_processes.keys().cloned().collect::<Vec<_>>();
3908        for child_id in child_ids {
3909            let child_process_id = format!("{current_process_id}/{child_id}");
3910            let Some(mut child) = process.child_processes.remove(&child_id) else {
3911                continue;
3912            };
3913            if child.detached {
3914                adopted.push((child_process_id, child));
3915                continue;
3916            }
3917
3918            adopted.extend(Self::adopt_detached_child_processes(
3919                &child_process_id,
3920                &mut child,
3921            ));
3922            process.child_processes.insert(child_id, child);
3923        }
3924        adopted
3925    }
3926
3927    fn child_process_signal_key<'a>(process_id: &'a str, child_path: &[&'a str]) -> &'a str {
3928        child_path.last().copied().unwrap_or(process_id)
3929    }
3930
3931    fn resolve_detached_child_process_path(
3932        vm: &VmState,
3933        detached_process_id: &str,
3934    ) -> Option<(String, Vec<String>)> {
3935        let root_process_id = vm
3936            .active_processes
3937            .keys()
3938            .filter(|candidate| {
3939                detached_process_id == candidate.as_str()
3940                    || detached_process_id
3941                        .strip_prefix(candidate.as_str())
3942                        .is_some_and(|remainder| remainder.starts_with('/'))
3943            })
3944            .max_by_key(|candidate| candidate.len())?
3945            .clone();
3946
3947        let remainder = detached_process_id
3948            .strip_prefix(root_process_id.as_str())
3949            .unwrap_or_default();
3950        if remainder.is_empty() {
3951            return Some((root_process_id, Vec::new()));
3952        }
3953
3954        Some((
3955            root_process_id,
3956            remainder
3957                .trim_start_matches('/')
3958                .split('/')
3959                .map(str::to_owned)
3960                .collect(),
3961        ))
3962    }
3963
3964    fn pump_detached_child_process_events(&mut self, vm_id: &str) -> Result<bool, SidecarError> {
3965        let detached_process_ids = self
3966            .vms
3967            .get(vm_id)
3968            .map(|vm| {
3969                vm.detached_child_processes
3970                    .iter()
3971                    .cloned()
3972                    .collect::<Vec<_>>()
3973            })
3974            .unwrap_or_default();
3975        let mut emitted_any = false;
3976        for detached_process_id in detached_process_ids {
3977            let Some((root_process_id, child_path)) = self
3978                .vms
3979                .get(vm_id)
3980                .and_then(|vm| Self::resolve_detached_child_process_path(vm, &detached_process_id))
3981            else {
3982                if let Some(vm) = self.vms.get_mut(vm_id) {
3983                    vm.detached_child_processes.remove(&detached_process_id);
3984                }
3985                continue;
3986            };
3987            if child_path.is_empty() {
3988                loop {
3989                    enum ProcessPollResult {
3990                        Event(Box<Option<ActiveExecutionEvent>>),
3991                        RecoverClosedChannel,
3992                    }
3993                    let poll_result = {
3994                        let Some(vm) = self.vms.get_mut(vm_id) else {
3995                            break;
3996                        };
3997                        let Some(process) = vm.active_processes.get_mut(&root_process_id) else {
3998                            break;
3999                        };
4000                        if let Some(event) = process.pending_execution_events.pop_front() {
4001                            ProcessPollResult::Event(Box::new(Some(event)))
4002                        } else {
4003                            match process.execution.poll_event_blocking(Duration::ZERO) {
4004                                Ok(event) => ProcessPollResult::Event(Box::new(event)),
4005                                Err(SidecarError::Execution(message))
4006                                    if (process.runtime == GuestRuntimeKind::JavaScript
4007                                        && closed_javascript_event_channel(&message))
4008                                        || (process.runtime == GuestRuntimeKind::Python
4009                                            && closed_python_event_channel(&message))
4010                                        || (process.runtime == GuestRuntimeKind::WebAssembly
4011                                            && closed_wasm_event_channel(&message)) =>
4012                                {
4013                                    ProcessPollResult::RecoverClosedChannel
4014                                }
4015                                Err(error) => return Err(error),
4016                            }
4017                        }
4018                    };
4019                    let event = match poll_result {
4020                        ProcessPollResult::Event(event) => *event,
4021                        ProcessPollResult::RecoverClosedChannel => {
4022                            self.recover_closed_root_runtime_process_event(vm_id, &root_process_id)?
4023                        }
4024                    };
4025                    let Some(event) = event else {
4026                        break;
4027                    };
4028                    let Some((connection_id, session_id)) = self
4029                        .vms
4030                        .get(vm_id)
4031                        .map(|vm| (vm.connection_id.clone(), vm.session_id.clone()))
4032                    else {
4033                        break;
4034                    };
4035                    match event {
4036                        ActiveExecutionEvent::Stdout(chunk) => {
4037                            self.queue_pending_process_event(ProcessEventEnvelope {
4038                                connection_id,
4039                                session_id,
4040                                vm_id: vm_id.to_owned(),
4041                                process_id: detached_process_id.clone(),
4042                                event: ActiveExecutionEvent::Stdout(chunk),
4043                            })?;
4044                            emitted_any = true;
4045                        }
4046                        ActiveExecutionEvent::Stderr(chunk) => {
4047                            self.queue_pending_process_event(ProcessEventEnvelope {
4048                                connection_id,
4049                                session_id,
4050                                vm_id: vm_id.to_owned(),
4051                                process_id: detached_process_id.clone(),
4052                                event: ActiveExecutionEvent::Stderr(chunk),
4053                            })?;
4054                            emitted_any = true;
4055                        }
4056                        ActiveExecutionEvent::Exited(exit_code) => {
4057                            if let Some(vm) = self.vms.get_mut(vm_id) {
4058                                vm.detached_child_processes.remove(&detached_process_id);
4059                            }
4060                            self.queue_pending_process_event(ProcessEventEnvelope {
4061                                connection_id,
4062                                session_id,
4063                                vm_id: vm_id.to_owned(),
4064                                process_id: detached_process_id.clone(),
4065                                event: ActiveExecutionEvent::Exited(exit_code),
4066                            })?;
4067                            emitted_any = true;
4068                            break;
4069                        }
4070                        ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
4071                            self.handle_javascript_sync_rpc_request(
4072                                vm_id,
4073                                &root_process_id,
4074                                request,
4075                            )?;
4076                        }
4077                        ActiveExecutionEvent::PythonVfsRpcRequest(request) => {
4078                            self.handle_python_vfs_rpc_request(vm_id, &root_process_id, *request)?;
4079                        }
4080                        ActiveExecutionEvent::SignalState {
4081                            signal,
4082                            registration,
4083                        } => {
4084                            if let Some(vm) = self.vms.get_mut(vm_id) {
4085                                vm.signal_states
4086                                    .entry(root_process_id.clone())
4087                                    .or_default()
4088                                    .insert(signal, registration);
4089                            }
4090                        }
4091                    }
4092                }
4093                continue;
4094            }
4095
4096            let parent_path = child_path[..child_path.len() - 1]
4097                .iter()
4098                .map(String::as_str)
4099                .collect::<Vec<_>>();
4100            let child_process_id = child_path.last().expect("child path cannot be empty");
4101
4102            loop {
4103                let event = match self.poll_descendant_javascript_child_process(
4104                    vm_id,
4105                    &root_process_id,
4106                    &parent_path,
4107                    child_process_id,
4108                    0,
4109                ) {
4110                    Ok(event) => event,
4111                    Err(SidecarError::InvalidState(message))
4112                        if message.contains("unknown child process")
4113                            || message.contains("unknown child process path") =>
4114                    {
4115                        if let Some(vm) = self.vms.get_mut(vm_id) {
4116                            vm.detached_child_processes.remove(&detached_process_id);
4117                        }
4118                        break;
4119                    }
4120                    Err(error) if is_javascript_child_process_gone_error(&error) => {
4121                        if let Some(vm) = self.vms.get_mut(vm_id) {
4122                            vm.detached_child_processes.remove(&detached_process_id);
4123                        }
4124                        break;
4125                    }
4126                    Err(error) => return Err(error),
4127                };
4128
4129                let Some(event_type) = event.get("type").and_then(Value::as_str) else {
4130                    break;
4131                };
4132                let Some((connection_id, session_id)) = self
4133                    .vms
4134                    .get(vm_id)
4135                    .map(|vm| (vm.connection_id.clone(), vm.session_id.clone()))
4136                else {
4137                    break;
4138                };
4139
4140                let envelope = match event_type {
4141                    "stdout" => Some(ProcessEventEnvelope {
4142                        connection_id: connection_id.clone(),
4143                        session_id: session_id.clone(),
4144                        vm_id: vm_id.to_owned(),
4145                        process_id: detached_process_id.clone(),
4146                        event: ActiveExecutionEvent::Stdout(javascript_sync_rpc_bytes_arg(
4147                            &[event.get("data").cloned().unwrap_or(Value::Null)],
4148                            0,
4149                            "detached child_process stdout",
4150                        )?),
4151                    }),
4152                    "stderr" => Some(ProcessEventEnvelope {
4153                        connection_id: connection_id.clone(),
4154                        session_id: session_id.clone(),
4155                        vm_id: vm_id.to_owned(),
4156                        process_id: detached_process_id.clone(),
4157                        event: ActiveExecutionEvent::Stderr(javascript_sync_rpc_bytes_arg(
4158                            &[event.get("data").cloned().unwrap_or(Value::Null)],
4159                            0,
4160                            "detached child_process stderr",
4161                        )?),
4162                    }),
4163                    "exit" => {
4164                        if let Some(vm) = self.vms.get_mut(vm_id) {
4165                            vm.detached_child_processes.remove(&detached_process_id);
4166                        }
4167                        Some(ProcessEventEnvelope {
4168                            connection_id,
4169                            session_id,
4170                            vm_id: vm_id.to_owned(),
4171                            process_id: detached_process_id.clone(),
4172                            event: ActiveExecutionEvent::Exited(
4173                                event
4174                                    .get("exitCode")
4175                                    .and_then(Value::as_i64)
4176                                    .map(|value| value as i32)
4177                                    .unwrap_or(1),
4178                            ),
4179                        })
4180                    }
4181                    _ => None,
4182                };
4183
4184                let Some(envelope) = envelope else {
4185                    break;
4186                };
4187                self.queue_pending_process_event(envelope)?;
4188                emitted_any = true;
4189
4190                if event_type == "exit" {
4191                    break;
4192                }
4193            }
4194        }
4195
4196        Ok(emitted_any)
4197    }
4198    pub(crate) fn drain_queued_descendant_javascript_child_process_events(
4199        &mut self,
4200        vm_id: &str,
4201        process_id: &str,
4202        child_path: &[&str],
4203    ) -> Result<(), SidecarError> {
4204        if child_path.is_empty() {
4205            return Ok(());
4206        }
4207        let target_process_id = Self::child_process_path_label(process_id, child_path);
4208        let mut child_capacity = self
4209            .vms
4210            .get(vm_id)
4211            .and_then(|vm| vm.active_processes.get(process_id))
4212            .and_then(|root| descendant_pending_execution_event_capacity(root, child_path));
4213
4214        let mut deferred = VecDeque::new();
4215        while let Some(envelope) = self.pending_process_events.pop_front() {
4216            if envelope.vm_id == vm_id && envelope.process_id == target_process_id {
4217                if matches!(child_capacity, Some(0)) {
4218                    self.pending_process_events.push_front(envelope);
4219                    while let Some(deferred_envelope) = deferred.pop_back() {
4220                        self.pending_process_events.push_front(deferred_envelope);
4221                    }
4222                    return Err(process_event_queue_overflow_error());
4223                }
4224                if let Some(vm) = self.vms.get_mut(vm_id) {
4225                    if let Some(root) = vm.active_processes.get_mut(process_id) {
4226                        if let Some(child) = Self::active_process_by_path_mut(root, child_path) {
4227                            child.queue_pending_execution_event(envelope.event)?;
4228                            child_capacity = child_capacity.map(|capacity| capacity - 1);
4229                            continue;
4230                        }
4231                    }
4232                }
4233            }
4234            deferred.push_back(envelope);
4235        }
4236        self.pending_process_events = deferred;
4237
4238        let mut queued = Vec::new();
4239        {
4240            let transfer_capacity = self
4241                .pending_process_event_capacity()
4242                .min(child_capacity.unwrap_or(usize::MAX));
4243            let receiver = self.process_event_receiver.as_mut().ok_or_else(|| {
4244                SidecarError::InvalidState(String::from("process event receiver unavailable"))
4245            })?;
4246            loop {
4247                if queued.len() >= transfer_capacity {
4248                    if receiver.is_empty() {
4249                        break;
4250                    }
4251                    return Err(process_event_queue_overflow_error());
4252                }
4253                match receiver.try_recv() {
4254                    Ok(envelope) => queued.push(envelope),
4255                    Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
4256                    Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
4257                }
4258            }
4259        }
4260        for envelope in queued {
4261            if envelope.vm_id == vm_id && envelope.process_id == target_process_id {
4262                if let Some(vm) = self.vms.get_mut(vm_id) {
4263                    if let Some(root) = vm.active_processes.get_mut(process_id) {
4264                        if let Some(child) = Self::active_process_by_path_mut(root, child_path) {
4265                            child.queue_pending_execution_event(envelope.event)?;
4266                            continue;
4267                        }
4268                    }
4269                }
4270            }
4271            self.queue_pending_process_event(envelope)?;
4272        }
4273
4274        Ok(())
4275    }
4276
4277    pub(crate) fn handle_execution_event(
4278        &mut self,
4279        vm_id: &str,
4280        process_id: &str,
4281        event: ActiveExecutionEvent,
4282    ) -> Result<Option<EventFrame>, SidecarError> {
4283        let Some(vm) = self.vms.get(vm_id) else {
4284            log_stale_process_event(&self.bridge, vm_id, process_id, "execution event dispatch");
4285            return Ok(None);
4286        };
4287        if !vm.active_processes.contains_key(process_id) {
4288            log_stale_process_event(&self.bridge, vm_id, process_id, "execution event dispatch");
4289            return Ok(None);
4290        }
4291        let (connection_id, session_id) = { (vm.connection_id.clone(), vm.session_id.clone()) };
4292        let ownership = OwnershipScope::vm(&connection_id, &session_id, vm_id);
4293
4294        if self.capture_extension_process_output_event(vm_id, process_id, &event) {
4295            return Ok(None);
4296        }
4297
4298        match event {
4299            ActiveExecutionEvent::Stdout(chunk) => Ok(Some(EventFrame::new(
4300                ownership,
4301                EventPayload::ProcessOutput(ProcessOutputEvent {
4302                    process_id: process_id.to_owned(),
4303                    channel: StreamChannel::Stdout,
4304                    chunk,
4305                }),
4306            ))),
4307            ActiveExecutionEvent::Stderr(chunk) => Ok(Some(EventFrame::new(
4308                ownership,
4309                EventPayload::ProcessOutput(ProcessOutputEvent {
4310                    process_id: process_id.to_owned(),
4311                    channel: StreamChannel::Stderr,
4312                    chunk,
4313                }),
4314            ))),
4315            ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
4316                self.handle_javascript_sync_rpc_request(vm_id, process_id, request)?;
4317                Ok(None)
4318            }
4319            ActiveExecutionEvent::PythonVfsRpcRequest(request) => {
4320                self.handle_python_vfs_rpc_request(vm_id, process_id, *request)?;
4321                Ok(None)
4322            }
4323            ActiveExecutionEvent::SignalState {
4324                signal,
4325                registration,
4326            } => {
4327                let Some(vm) = self.vms.get_mut(vm_id) else {
4328                    return Ok(None);
4329                };
4330                if !vm.active_processes.contains_key(process_id) {
4331                    return Ok(None);
4332                }
4333                vm.signal_states
4334                    .entry(process_id.to_owned())
4335                    .or_default()
4336                    .insert(signal, registration);
4337                Ok(None)
4338            }
4339            ActiveExecutionEvent::Exited(exit_code) => {
4340                let became_idle = self
4341                    .finish_active_process_exit(vm_id, process_id, exit_code)?
4342                    .unwrap_or(false);
4343
4344                if became_idle {
4345                    self.bridge.emit_lifecycle(vm_id, LifecycleState::Ready)?;
4346                }
4347
4348                Ok(Some(EventFrame::new(
4349                    ownership,
4350                    EventPayload::ProcessExited(ProcessExitedEvent {
4351                        process_id: process_id.to_owned(),
4352                        exit_code,
4353                    }),
4354                )))
4355            }
4356        }
4357    }
4358
4359    pub(crate) fn finish_active_process_exit(
4360        &mut self,
4361        vm_id: &str,
4362        process_id: &str,
4363        exit_code: i32,
4364    ) -> Result<Option<bool>, SidecarError> {
4365        let Some(vm) = self.vms.get_mut(vm_id) else {
4366            log_stale_process_event(&self.bridge, vm_id, process_id, "process exit cleanup");
4367            return Ok(None);
4368        };
4369        if !vm.active_processes.contains_key(process_id) {
4370            log_stale_process_event(&self.bridge, vm_id, process_id, "process exit cleanup");
4371            return Ok(None);
4372        }
4373
4374        prune_exited_process_snapshots(vm);
4375        let process_table = vm.kernel.list_processes();
4376        let Some(mut process) = vm.active_processes.remove(process_id) else {
4377            return Ok(None);
4378        };
4379        if let Some(info) = process_table.get(&process.kernel_pid) {
4380            vm.exited_process_snapshots
4381                .push_back(ExitedProcessSnapshot {
4382                    captured_at: Instant::now(),
4383                    process: build_process_snapshot_entry(
4384                        process_id,
4385                        &process,
4386                        info,
4387                        Some(exit_code),
4388                    ),
4389                });
4390        }
4391        let detached_children = Self::adopt_detached_child_processes(process_id, &mut process);
4392        sync_process_host_writes_to_kernel(vm, &process)?;
4393        terminate_child_process_tree(&mut vm.kernel, &mut process);
4394        process.kernel_handle.finish(exit_code);
4395        let _ = vm.kernel.wait_and_reap(process.kernel_pid);
4396        vm.signal_states.remove(process_id);
4397        for (detached_process_id, detached_child) in detached_children {
4398            vm.detached_child_processes
4399                .insert(detached_process_id.clone());
4400            vm.active_processes
4401                .insert(detached_process_id, detached_child);
4402        }
4403        let became_idle = vm.active_processes.is_empty();
4404        self.prune_extension_process_resource(process_id);
4405
4406        Ok(Some(became_idle))
4407    }
4408
4409    pub(crate) fn drain_process_events_blocking_with_limit(
4410        &mut self,
4411        vm_id: &str,
4412        process_id: &str,
4413        max_events: usize,
4414    ) -> Result<Vec<ActiveExecutionEvent>, SidecarError> {
4415        let mut events = Vec::new();
4416        if max_events == 0 {
4417            return Ok(events);
4418        }
4419        let mut deadline = Instant::now() + Duration::from_millis(150);
4420
4421        loop {
4422            if events.len() >= max_events {
4423                break;
4424            }
4425            let event = {
4426                let Some(vm) = self.vms.get_mut(vm_id) else {
4427                    break;
4428                };
4429                let Some(process) = vm.active_processes.get_mut(process_id) else {
4430                    break;
4431                };
4432                if let Some(event) = process.pending_execution_events.pop_front() {
4433                    Some(event)
4434                } else {
4435                    match process.execution.poll_event_blocking(Duration::ZERO) {
4436                        Ok(event) => event,
4437                        Err(SidecarError::Execution(_)) => None,
4438                        Err(other) => return Err(other),
4439                    }
4440                }
4441            };
4442
4443            let Some(event) = event else {
4444                if Instant::now() >= deadline {
4445                    break;
4446                }
4447                let blocking_wait = deadline.saturating_duration_since(Instant::now());
4448                if blocking_wait.is_zero() {
4449                    break;
4450                }
4451                if events.len() >= max_events {
4452                    break;
4453                }
4454                let delayed_event = {
4455                    let Some(vm) = self.vms.get_mut(vm_id) else {
4456                        break;
4457                    };
4458                    let Some(process) = vm.active_processes.get_mut(process_id) else {
4459                        break;
4460                    };
4461                    if let Some(event) = process.pending_execution_events.pop_front() {
4462                        Some(event)
4463                    } else {
4464                        match process.execution.poll_event_blocking(blocking_wait) {
4465                            Ok(event) => event,
4466                            Err(SidecarError::Execution(_)) => None,
4467                            Err(other) => return Err(other),
4468                        }
4469                    }
4470                };
4471                let Some(event) = delayed_event else {
4472                    break;
4473                };
4474                events.push(event);
4475                deadline = Instant::now() + Duration::from_millis(150);
4476                continue;
4477            };
4478            events.push(event);
4479            deadline = Instant::now() + Duration::from_millis(150);
4480        }
4481
4482        Ok(events)
4483    }
4484
4485    pub(crate) fn handle_python_vfs_rpc_request(
4486        &mut self,
4487        vm_id: &str,
4488        process_id: &str,
4489        request: PythonVfsRpcRequest,
4490    ) -> Result<(), SidecarError> {
4491        match request.method {
4492            PythonVfsRpcMethod::Read
4493            | PythonVfsRpcMethod::Write
4494            | PythonVfsRpcMethod::Stat
4495            | PythonVfsRpcMethod::ReadDir
4496            | PythonVfsRpcMethod::Mkdir => {
4497                filesystem_handle_python_vfs_rpc_request(self, vm_id, process_id, request)
4498            }
4499            PythonVfsRpcMethod::HttpRequest => {
4500                self.handle_python_http_rpc_request(vm_id, process_id, request)
4501            }
4502            PythonVfsRpcMethod::DnsLookup => {
4503                self.handle_python_dns_rpc_request(vm_id, process_id, request)
4504            }
4505            PythonVfsRpcMethod::SubprocessRun => {
4506                self.handle_python_subprocess_rpc_request(vm_id, process_id, request)
4507            }
4508        }
4509    }
4510
4511    fn handle_python_http_rpc_request(
4512        &mut self,
4513        vm_id: &str,
4514        process_id: &str,
4515        request: PythonVfsRpcRequest,
4516    ) -> Result<(), SidecarError> {
4517        let Some(vm) = self.vms.get(vm_id) else {
4518            return Ok(());
4519        };
4520        if !vm.active_processes.contains_key(process_id) {
4521            return Ok(());
4522        }
4523        let response = (|| {
4524            let url_text = request.url.as_deref().ok_or_else(|| {
4525                SidecarError::InvalidState(String::from("python httpRequest requires a url"))
4526            })?;
4527            let url = Url::parse(url_text)
4528                .map_err(|error| SidecarError::Execution(format!("ERR_INVALID_URL: {error}")))?;
4529            let host = url.host_str().ok_or_else(|| {
4530                SidecarError::Execution(String::from("ERR_INVALID_URL: missing host"))
4531            })?;
4532            let port = url.port_or_known_default().ok_or_else(|| {
4533                SidecarError::Execution(String::from("ERR_INVALID_URL: missing port"))
4534            })?;
4535            self.bridge.require_network_access(
4536                vm_id,
4537                NetworkOperation::Http,
4538                format_tcp_resource(host, port),
4539            )?;
4540            // Pin the outbound connection to the IP addresses that pass the
4541            // egress range guard at resolution time. A literal IP is validated
4542            // directly; a hostname is resolved once here and the resulting
4543            // address set is pinned into the HTTP client's resolver below so a
4544            // rebinding DNS server cannot make the second (TLS/TCP) lookup land
4545            // on a private/link-local/metadata IP that this check rejected.
4546            let pinned_addresses = if let Ok(literal_ip) = host.parse::<IpAddr>() {
4547                filter_dns_safe_ip_addrs(vec![literal_ip], host)?
4548            } else {
4549                filter_dns_safe_ip_addrs(
4550                    resolve_dns_ip_addrs(
4551                        &self.bridge,
4552                        &vm.kernel,
4553                        vm_id,
4554                        &vm.dns,
4555                        host,
4556                        DnsLookupPolicy::SkipPermissions,
4557                    )?,
4558                    host,
4559                )?
4560            };
4561            let mut headers = BTreeMap::new();
4562            for (name, value) in &request.headers {
4563                headers.insert(name.clone(), Value::String(value.clone()));
4564            }
4565            let options = JavascriptHttpRequestOptions {
4566                method: Some(
4567                    request
4568                        .http_method
4569                        .clone()
4570                        .unwrap_or_else(|| String::from("GET")),
4571                ),
4572                headers,
4573                body: request.body_base64.as_deref().map(|body| {
4574                    String::from_utf8(
4575                        base64::engine::general_purpose::STANDARD
4576                            .decode(body)
4577                            .unwrap_or_default(),
4578                    )
4579                    .unwrap_or_default()
4580                }),
4581                reject_unauthorized: None,
4582            };
4583            let headers =
4584                parse_http_header_collection(&options.headers, "python httpRequest headers")?;
4585            let response =
4586                issue_outbound_http_request(&url, &options, &headers, &pinned_addresses)?;
4587            let payload_json = response.as_str().ok_or_else(|| {
4588                SidecarError::Execution(String::from(
4589                    "python httpRequest returned a non-string response payload",
4590                ))
4591            })?;
4592            let payload: Value = serde_json::from_str(payload_json).map_err(|error| {
4593                SidecarError::Execution(format!(
4594                    "python httpRequest response must be valid JSON: {error}"
4595                ))
4596            })?;
4597            let header_map = payload
4598                .get("headers")
4599                .and_then(Value::as_array)
4600                .map(|entries| {
4601                    let mut normalized = BTreeMap::<String, Vec<String>>::new();
4602                    for entry in entries {
4603                        let Some(pair) = entry.as_array() else {
4604                            continue;
4605                        };
4606                        let Some(name) = pair.first().and_then(Value::as_str) else {
4607                            continue;
4608                        };
4609                        let Some(value) = pair.get(1).and_then(Value::as_str) else {
4610                            continue;
4611                        };
4612                        normalized
4613                            .entry(name.to_owned())
4614                            .or_default()
4615                            .push(value.to_owned());
4616                    }
4617                    normalized
4618                })
4619                .unwrap_or_default();
4620            Ok(PythonVfsRpcResponsePayload::Http {
4621                status: payload
4622                    .get("status")
4623                    .and_then(Value::as_u64)
4624                    .map(|value| value as u16)
4625                    .unwrap_or_default(),
4626                reason: payload
4627                    .get("statusText")
4628                    .and_then(Value::as_str)
4629                    .unwrap_or_default()
4630                    .to_owned(),
4631                url: payload
4632                    .get("url")
4633                    .and_then(Value::as_str)
4634                    .unwrap_or(url_text)
4635                    .to_owned(),
4636                headers: header_map,
4637                body_base64: payload
4638                    .get("body")
4639                    .and_then(Value::as_str)
4640                    .unwrap_or_default()
4641                    .to_owned(),
4642            })
4643        })();
4644
4645        self.respond_python_rpc(vm_id, process_id, request.id, response)
4646    }
4647
4648    fn handle_python_dns_rpc_request(
4649        &mut self,
4650        vm_id: &str,
4651        process_id: &str,
4652        request: PythonVfsRpcRequest,
4653    ) -> Result<(), SidecarError> {
4654        let Some(vm) = self.vms.get(vm_id) else {
4655            return Ok(());
4656        };
4657        if !vm.active_processes.contains_key(process_id) {
4658            return Ok(());
4659        }
4660        let response = (|| {
4661            let hostname = request.hostname.as_deref().ok_or_else(|| {
4662                SidecarError::InvalidState(String::from("python dnsLookup requires a hostname"))
4663            })?;
4664            let mut addresses = filter_dns_safe_ip_addrs(
4665                resolve_dns_ip_addrs(
4666                    &self.bridge,
4667                    &vm.kernel,
4668                    vm_id,
4669                    &vm.dns,
4670                    hostname,
4671                    DnsLookupPolicy::CheckPermissions,
4672                )?,
4673                hostname,
4674            )?;
4675            if let Some(family) = request.family {
4676                addresses.retain(|address| {
4677                    matches!((family, address), (4, IpAddr::V4(_)) | (6, IpAddr::V6(_)))
4678                });
4679            }
4680            Ok(PythonVfsRpcResponsePayload::DnsLookup {
4681                addresses: addresses
4682                    .into_iter()
4683                    .map(|address| address.to_string())
4684                    .collect(),
4685            })
4686        })();
4687
4688        self.respond_python_rpc(vm_id, process_id, request.id, response)
4689    }
4690
4691    fn handle_python_subprocess_rpc_request(
4692        &mut self,
4693        vm_id: &str,
4694        process_id: &str,
4695        request: PythonVfsRpcRequest,
4696    ) -> Result<(), SidecarError> {
4697        let command = request.command.clone().ok_or_else(|| {
4698            SidecarError::InvalidState(String::from("python subprocessRun requires a command"))
4699        })?;
4700        let (internal_bootstrap_env, cwd) = {
4701            let Some(vm) = self.vms.get(vm_id) else {
4702                return Ok(());
4703            };
4704            let Some(process) = vm.active_processes.get(process_id) else {
4705                return Ok(());
4706            };
4707            let cwd = request.cwd.clone().or_else(|| {
4708                guest_runtime_path_for_host_path(
4709                    &vm.guest_env,
4710                    &vm.host_cwd,
4711                    &process.host_cwd.to_string_lossy(),
4712                )
4713            });
4714            (
4715                sanitize_javascript_child_process_internal_bootstrap_env(&vm.guest_env),
4716                cwd,
4717            )
4718        };
4719        let response = self
4720            .spawn_javascript_child_process_sync(
4721                vm_id,
4722                process_id,
4723                JavascriptChildProcessSpawnRequest {
4724                    command,
4725                    args: request.args.clone(),
4726                    options: JavascriptChildProcessSpawnOptions {
4727                        cwd,
4728                        env: request.env.clone(),
4729                        input: None,
4730                        internal_bootstrap_env,
4731                        shell: request.shell,
4732                        detached: false,
4733                        stdio: vec![
4734                            String::from("pipe"),
4735                            String::from("pipe"),
4736                            String::from("pipe"),
4737                        ],
4738                        timeout: None,
4739                        kill_signal: None,
4740                    },
4741                },
4742                request.max_buffer,
4743            )
4744            .map(|payload| PythonVfsRpcResponsePayload::SubprocessRun {
4745                exit_code: payload
4746                    .get("code")
4747                    .and_then(Value::as_i64)
4748                    .map(|value| value as i32)
4749                    .unwrap_or(1),
4750                stdout: payload
4751                    .get("stdout")
4752                    .and_then(Value::as_str)
4753                    .unwrap_or_default()
4754                    .to_owned(),
4755                stderr: payload
4756                    .get("stderr")
4757                    .and_then(Value::as_str)
4758                    .unwrap_or_default()
4759                    .to_owned(),
4760                max_buffer_exceeded: payload
4761                    .get("maxBufferExceeded")
4762                    .and_then(Value::as_bool)
4763                    .unwrap_or(false),
4764            });
4765
4766        self.respond_python_rpc(vm_id, process_id, request.id, response)
4767    }
4768
4769    fn respond_python_rpc(
4770        &mut self,
4771        vm_id: &str,
4772        process_id: &str,
4773        request_id: u64,
4774        response: Result<PythonVfsRpcResponsePayload, SidecarError>,
4775    ) -> Result<(), SidecarError> {
4776        let Some(vm) = self.vms.get_mut(vm_id) else {
4777            return Ok(());
4778        };
4779        let Some(process) = vm.active_processes.get_mut(process_id) else {
4780            return Ok(());
4781        };
4782        let result = match response {
4783            Ok(payload) => process
4784                .execution
4785                .respond_python_vfs_rpc_success(request_id, payload),
4786            Err(error) => process.execution.respond_python_vfs_rpc_error(
4787                request_id,
4788                "ERR_AGENT_OS_PYTHON_VFS_RPC",
4789                error.to_string(),
4790            ),
4791        };
4792        match result {
4793            Ok(()) => Ok(()),
4794            Err(error) if is_broken_pipe_error(&error) => Ok(()),
4795            Err(error) => Err(error),
4796        }
4797    }
4798
4799    pub(crate) fn resolve_javascript_child_process_execution(
4800        &self,
4801        vm: &VmState,
4802        parent_env: &BTreeMap<String, String>,
4803        parent_guest_cwd: &str,
4804        parent_host_cwd: &Path,
4805        request: &JavascriptChildProcessSpawnRequest,
4806    ) -> Result<ResolvedChildProcessExecution, SidecarError> {
4807        let mut runtime_env = parent_env.clone();
4808        runtime_env.extend(request.options.internal_bootstrap_env.clone());
4809        let (guest_cwd, host_cwd_override) = request
4810            .options
4811            .cwd
4812            .as_deref()
4813            .map(|cwd| {
4814                let normalized_parent_host_cwd = normalize_host_path(parent_host_cwd);
4815                let requested_host_cwd = normalize_host_path(Path::new(cwd));
4816                if path_is_within_root(&requested_host_cwd, &normalized_parent_host_cwd) {
4817                    let relative = requested_host_cwd
4818                        .strip_prefix(&normalized_parent_host_cwd)
4819                        .unwrap_or_else(|_| Path::new(""));
4820                    let relative = relative.to_string_lossy().replace('\\', "/");
4821                    let guest_cwd = if relative.is_empty() {
4822                        parent_guest_cwd.to_owned()
4823                    } else {
4824                        normalize_path(&format!("{parent_guest_cwd}/{relative}"))
4825                    };
4826                    (guest_cwd, Some(requested_host_cwd))
4827                } else if Path::new(cwd).is_relative() {
4828                    (
4829                        normalize_path(&format!("{parent_guest_cwd}/{cwd}")),
4830                        Some(normalize_host_path(&parent_host_cwd.join(cwd))),
4831                    )
4832                } else {
4833                    (normalize_path(cwd), None)
4834                }
4835            })
4836            .unwrap_or_else(|| (parent_guest_cwd.to_owned(), None));
4837        let inherited_host_cwd = (host_cwd_override.is_none() && guest_cwd == parent_guest_cwd)
4838            .then(|| normalize_host_path(parent_host_cwd));
4839        let host_cwd = host_cwd_override
4840            .or(inherited_host_cwd)
4841            .or_else(|| {
4842                host_runtime_path_for_guest_path_with_env(
4843                    vm,
4844                    &runtime_env,
4845                    &guest_cwd,
4846                    parent_host_cwd,
4847                )
4848            })
4849            .unwrap_or_else(|| {
4850                let candidate = PathBuf::from(&guest_cwd);
4851                if guest_cwd == parent_guest_cwd {
4852                    normalize_host_path(parent_host_cwd)
4853                } else if candidate.is_absolute() {
4854                    shadow_path_for_guest(vm, &guest_cwd)
4855                } else {
4856                    vm.host_cwd.clone()
4857                }
4858            });
4859        let mut env = parent_env.clone();
4860        env.extend(request.options.env.clone());
4861        // Child JavaScript executions must resolve their own entrypoint/eval state.
4862        // Reusing the parent's values makes the sidecar load the wrong source file.
4863        env.remove("AGENT_OS_GUEST_ENTRYPOINT");
4864        env.remove("AGENT_OS_NODE_EVAL");
4865
4866        let (command, process_args) = if request.options.shell {
4867            let tokens = tokenize_shell_free_command(&request.command);
4868            let requires_shell = command_requires_shell(&request.command)
4869                || tokens.first().is_some_and(|command| {
4870                    is_posix_shell_builtin(command) || shell_first_token_requires_shell(command)
4871                });
4872            if requires_shell {
4873                if !vm.command_guest_paths.contains_key("sh") {
4874                    return Err(SidecarError::InvalidState(format!(
4875                        "shell-mode child_process command requires /bin/sh, which is not \
4876                         installed in this VM (install a software package that provides sh, \
4877                         for example @secure-exec/coreutils): {}",
4878                        request.command
4879                    )));
4880                }
4881                (
4882                    String::from("sh"),
4883                    vec![String::from("-c"), request.command.clone()],
4884                )
4885            } else {
4886                let Some((command, args)) = tokens.split_first() else {
4887                    return Err(SidecarError::InvalidState(String::from(
4888                        "child_process shell command must not be empty",
4889                    )));
4890                };
4891                (command.clone(), args.to_vec())
4892            }
4893        } else {
4894            (request.command.clone(), request.args.clone())
4895        };
4896        let process_args = apply_shell_cwd_prefix(&command, process_args, &guest_cwd);
4897        if is_tool_command(vm, &command) {
4898            let command = normalized_tool_command_name(&command).unwrap_or(command);
4899            return Ok(ResolvedChildProcessExecution {
4900                command: command.clone(),
4901                process_args: std::iter::once(command.clone())
4902                    .chain(process_args.iter().cloned())
4903                    .collect(),
4904                runtime: GuestRuntimeKind::JavaScript,
4905                entrypoint: command,
4906                execution_args: process_args,
4907                env,
4908                guest_cwd,
4909                host_cwd,
4910                wasm_permission_tier: None,
4911                tool_command: true,
4912            });
4913        }
4914
4915        if is_path_like_specifier(&command)
4916            && matches!(
4917                Path::new(&command).extension().and_then(|ext| ext.to_str()),
4918                Some("js" | "mjs" | "cjs" | "ts" | "mts" | "cts")
4919            )
4920        {
4921            let guest_entrypoint = if command.starts_with('/') {
4922                normalize_path(&command)
4923            } else if command.starts_with("file:") {
4924                normalize_path(command.trim_start_matches("file:"))
4925            } else {
4926                normalize_path(&format!("{guest_cwd}/{command}"))
4927            };
4928            let host_entrypoint = if command.starts_with("./") || command.starts_with("../") {
4929                host_cwd.join(&command)
4930            } else {
4931                host_runtime_path_for_guest_path_with_env(
4932                    vm,
4933                    &runtime_env,
4934                    &guest_entrypoint,
4935                    parent_host_cwd,
4936                )
4937                .unwrap_or_else(|| {
4938                    let candidate = PathBuf::from(&guest_entrypoint);
4939                    if candidate.is_absolute() {
4940                        candidate
4941                    } else {
4942                        host_cwd.join(&guest_entrypoint)
4943                    }
4944                })
4945            };
4946            env.insert(String::from("AGENT_OS_GUEST_ENTRYPOINT"), guest_entrypoint);
4947            let guest_entrypoint = env.get("AGENT_OS_GUEST_ENTRYPOINT").cloned();
4948            prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
4949
4950            return Ok(ResolvedChildProcessExecution {
4951                command: command.clone(),
4952                process_args: std::iter::once(command)
4953                    .chain(process_args.iter().cloned())
4954                    .collect(),
4955                runtime: GuestRuntimeKind::JavaScript,
4956                entrypoint: host_entrypoint.to_string_lossy().into_owned(),
4957                execution_args: process_args,
4958                env,
4959                guest_cwd,
4960                host_cwd,
4961                wasm_permission_tier: None,
4962                tool_command: false,
4963            });
4964        }
4965
4966        if is_node_runtime_command(&command) {
4967            if let Some(cli) = resolve_host_node_cli_entrypoint(&command) {
4968                env.insert(
4969                    String::from("AGENT_OS_NODE_EVAL"),
4970                    build_host_node_cli_eval(&cli),
4971                );
4972                prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
4973                add_runtime_guest_path_mapping(&mut env, &cli.guest_root, &cli.package_root);
4974                add_runtime_host_access_path(
4975                    &mut env,
4976                    "AGENT_OS_EXTRA_FS_READ_PATHS",
4977                    &cli.package_root,
4978                    true,
4979                );
4980
4981                return Ok(ResolvedChildProcessExecution {
4982                    command: command.clone(),
4983                    process_args: std::iter::once(command.clone())
4984                        .chain(process_args.iter().cloned())
4985                        .collect(),
4986                    runtime: GuestRuntimeKind::JavaScript,
4987                    entrypoint: String::from("-e"),
4988                    execution_args: std::iter::once(cli.guest_entrypoint.clone())
4989                        .chain(process_args.iter().cloned())
4990                        .collect(),
4991                    env,
4992                    guest_cwd,
4993                    host_cwd,
4994                    wasm_permission_tier: None,
4995                    tool_command: false,
4996                });
4997            }
4998
4999            if process_args.is_empty() {
5000                env.insert(String::from("AGENT_OS_NODE_EVAL"), String::new());
5001                prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
5002
5003                return Ok(ResolvedChildProcessExecution {
5004                    command: command.clone(),
5005                    process_args: vec![command.clone()],
5006                    runtime: GuestRuntimeKind::JavaScript,
5007                    entrypoint: String::from("-e"),
5008                    execution_args: Vec::new(),
5009                    env,
5010                    guest_cwd,
5011                    host_cwd,
5012                    wasm_permission_tier: None,
5013                    tool_command: false,
5014                });
5015            }
5016
5017            if let Some((entrypoint, execution_args)) =
5018                resolve_special_node_cli_invocation(&process_args, &mut env)
5019            {
5020                prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
5021
5022                return Ok(ResolvedChildProcessExecution {
5023                    command: command.clone(),
5024                    process_args: std::iter::once(command.clone())
5025                        .chain(process_args.iter().cloned())
5026                        .collect(),
5027                    runtime: GuestRuntimeKind::JavaScript,
5028                    entrypoint,
5029                    execution_args,
5030                    env,
5031                    guest_cwd,
5032                    host_cwd,
5033                    wasm_permission_tier: None,
5034                    tool_command: false,
5035                });
5036            }
5037
5038            let Some(entrypoint_specifier) = process_args.first() else {
5039                return Err(SidecarError::InvalidState(format!(
5040                    "{command} child_process spawn requires an entrypoint"
5041                )));
5042            };
5043
5044            let (entrypoint, execution_args) = if is_path_like_specifier(entrypoint_specifier) {
5045                let guest_entrypoint = if entrypoint_specifier.starts_with('/') {
5046                    normalize_path(entrypoint_specifier)
5047                } else if entrypoint_specifier.starts_with("file:") {
5048                    normalize_path(entrypoint_specifier.trim_start_matches("file:"))
5049                } else {
5050                    normalize_path(&format!("{guest_cwd}/{entrypoint_specifier}"))
5051                };
5052                let host_entrypoint = if entrypoint_specifier.starts_with("./")
5053                    || entrypoint_specifier.starts_with("../")
5054                {
5055                    host_cwd.join(entrypoint_specifier)
5056                } else {
5057                    host_runtime_path_for_guest_path_with_env(
5058                        vm,
5059                        &runtime_env,
5060                        &guest_entrypoint,
5061                        parent_host_cwd,
5062                    )
5063                    .unwrap_or_else(|| {
5064                        let candidate = PathBuf::from(&guest_entrypoint);
5065                        if candidate.is_absolute() {
5066                            candidate
5067                        } else {
5068                            host_cwd.join(&guest_entrypoint)
5069                        }
5070                    })
5071                };
5072                env.insert(String::from("AGENT_OS_GUEST_ENTRYPOINT"), guest_entrypoint);
5073                (
5074                    host_entrypoint.to_string_lossy().into_owned(),
5075                    process_args.iter().skip(1).cloned().collect(),
5076                )
5077            } else {
5078                (
5079                    entrypoint_specifier.clone(),
5080                    process_args.iter().skip(1).cloned().collect(),
5081                )
5082            };
5083            let guest_entrypoint = env.get("AGENT_OS_GUEST_ENTRYPOINT").cloned();
5084            prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
5085
5086            return Ok(ResolvedChildProcessExecution {
5087                command: command.clone(),
5088                process_args: std::iter::once(command)
5089                    .chain(process_args.iter().cloned())
5090                    .collect(),
5091                runtime: GuestRuntimeKind::JavaScript,
5092                entrypoint,
5093                execution_args,
5094                env,
5095                guest_cwd,
5096                host_cwd,
5097                wasm_permission_tier: None,
5098                tool_command: false,
5099            });
5100        }
5101
5102        if command == PYTHON_COMMAND {
5103            return Err(SidecarError::InvalidState(String::from(
5104                "nested python child_process execution is not supported yet",
5105            )));
5106        }
5107
5108        let guest_entrypoint = resolve_guest_command_entrypoint(
5109            vm,
5110            &guest_cwd,
5111            &command,
5112            env.get("PATH").map(String::as_str),
5113        )
5114        .ok_or_else(|| SidecarError::InvalidState(format!("command not found: {command}")))?;
5115        let host_entrypoint = resolve_vm_guest_path_to_host(vm, &guest_entrypoint);
5116        let wasm_permission_tier = vm.command_permissions.get(&command).copied().or_else(|| {
5117            Path::new(&guest_entrypoint)
5118                .file_name()
5119                .and_then(|name| name.to_str())
5120                .and_then(|name| vm.command_permissions.get(name).copied())
5121        });
5122        if let Some((javascript_guest_entrypoint, javascript_host_entrypoint)) =
5123            resolve_javascript_command_entrypoint(vm, &guest_entrypoint, &host_entrypoint)
5124        {
5125            prepare_guest_runtime_env(
5126                vm,
5127                &mut env,
5128                &guest_cwd,
5129                &host_cwd,
5130                Some(javascript_guest_entrypoint),
5131            )?;
5132
5133            return Ok(ResolvedChildProcessExecution {
5134                command: command.clone(),
5135                process_args: std::iter::once(command)
5136                    .chain(process_args.iter().cloned())
5137                    .collect(),
5138                runtime: GuestRuntimeKind::JavaScript,
5139                entrypoint: javascript_host_entrypoint.to_string_lossy().into_owned(),
5140                execution_args: process_args,
5141                env,
5142                guest_cwd,
5143                host_cwd,
5144                wasm_permission_tier: None,
5145                tool_command: false,
5146            });
5147        }
5148        prepare_guest_runtime_env(
5149            vm,
5150            &mut env,
5151            &guest_cwd,
5152            &host_cwd,
5153            Some(guest_entrypoint.clone()),
5154        )?;
5155
5156        Ok(ResolvedChildProcessExecution {
5157            command: command.clone(),
5158            process_args: std::iter::once(command)
5159                .chain(process_args.iter().cloned())
5160                .collect(),
5161            runtime: GuestRuntimeKind::WebAssembly,
5162            entrypoint: host_entrypoint.to_string_lossy().into_owned(),
5163            execution_args: process_args,
5164            env,
5165            guest_cwd,
5166            host_cwd,
5167            wasm_permission_tier,
5168            tool_command: false,
5169        })
5170    }
5171
5172    pub(crate) fn spawn_javascript_child_process(
5173        &mut self,
5174        vm_id: &str,
5175        process_id: &str,
5176        request: JavascriptChildProcessSpawnRequest,
5177    ) -> Result<Value, SidecarError> {
5178        let resolved = {
5179            let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?;
5180            let parent = vm
5181                .active_processes
5182                .get(process_id)
5183                .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5184            self.resolve_javascript_child_process_execution(
5185                vm,
5186                &parent.env,
5187                &parent.guest_cwd,
5188                &parent.host_cwd,
5189                &request,
5190            )?
5191        };
5192        let (parent_kernel_pid, child_process_id) = {
5193            let vm = self
5194                .vms
5195                .get_mut(vm_id)
5196                .ok_or_else(|| missing_vm_error(vm_id))?;
5197            let process = vm
5198                .active_processes
5199                .get_mut(process_id)
5200                .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5201            (process.kernel_pid, process.allocate_child_process_id())
5202        };
5203        let sidecar_requests = self.sidecar_requests.clone();
5204        let vm = self
5205            .vms
5206            .get_mut(vm_id)
5207            .ok_or_else(|| missing_vm_error(vm_id))?;
5208        let (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd) = if resolved
5209            .tool_command
5210        {
5211            let tool_resolution = resolve_tool_command(
5212                vm,
5213                &resolved.command,
5214                &resolved.execution_args,
5215                Some(&resolved.guest_cwd),
5216            )?
5217            .ok_or_else(|| {
5218                SidecarError::InvalidState(format!(
5219                    "tool command no longer resolves: {}",
5220                    resolved.command
5221                ))
5222            })?;
5223            let kernel_handle = vm
5224                .kernel
5225                .create_virtual_process(
5226                    EXECUTION_DRIVER_NAME,
5227                    TOOL_DRIVER_NAME,
5228                    &resolved.command,
5229                    resolved.process_args.clone(),
5230                    VirtualProcessOptions {
5231                        parent_pid: Some(parent_kernel_pid),
5232                        env: resolved.env.clone(),
5233                        cwd: Some(resolved.guest_cwd.clone()),
5234                    },
5235                )
5236                .map_err(kernel_error)?;
5237            let kernel_pid = kernel_handle.pid();
5238            let tool_execution = ToolExecution::default();
5239            let cancelled = tool_execution.cancelled.clone();
5240            let pending_events = tool_execution.pending_events.clone();
5241            let events_overflowed = tool_execution.events_overflowed.clone();
5242            spawn_tool_process_events(ToolProcessEventRequest {
5243                sidecar_requests: sidecar_requests.clone(),
5244                connection_id: vm.connection_id.clone(),
5245                session_id: vm.session_id.clone(),
5246                vm_id: vm_id.to_owned(),
5247                tool_resolution,
5248                cancelled,
5249                pending_events,
5250                events_overflowed,
5251            });
5252            (
5253                kernel_pid,
5254                kernel_handle,
5255                ActiveExecution::Tool(tool_execution),
5256                None,
5257            )
5258        } else {
5259            let kernel_command = match resolved.runtime {
5260                GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND,
5261                GuestRuntimeKind::WebAssembly => WASM_COMMAND,
5262                GuestRuntimeKind::Python => {
5263                    unreachable!("python child_process execution is rejected")
5264                }
5265            };
5266            let kernel_handle = vm
5267                .kernel
5268                .spawn_process(
5269                    kernel_command,
5270                    resolved.process_args.clone(),
5271                    SpawnOptions {
5272                        requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
5273                        parent_pid: Some(parent_kernel_pid),
5274                        env: resolved.env.clone(),
5275                        cwd: Some(resolved.guest_cwd.clone()),
5276                    },
5277                )
5278                .map_err(kernel_error)?;
5279            let kernel_pid = kernel_handle.pid();
5280            if request.options.detached {
5281                vm.kernel
5282                    .setsid(EXECUTION_DRIVER_NAME, kernel_pid)
5283                    .map_err(kernel_error)?;
5284            }
5285            let mut execution_env = resolved.env.clone();
5286            execution_env.insert(
5287                String::from(EXECUTION_SANDBOX_ROOT_ENV),
5288                normalize_host_path(&vm.cwd).to_string_lossy().into_owned(),
5289            );
5290
5291            let execution = match resolved.runtime {
5292                GuestRuntimeKind::JavaScript => {
5293                    execution_env.extend(sanitize_javascript_child_process_internal_bootstrap_env(
5294                        &request.options.internal_bootstrap_env,
5295                    ));
5296                    execution_env.insert(
5297                        String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
5298                        String::from("1"),
5299                    );
5300                    execution_env.insert(
5301                        String::from("AGENT_OS_VIRTUAL_PROCESS_PID"),
5302                        kernel_pid.to_string(),
5303                    );
5304                    execution_env.insert(
5305                        String::from("AGENT_OS_VIRTUAL_PROCESS_PPID"),
5306                        parent_kernel_pid.to_string(),
5307                    );
5308                    let context =
5309                        self.javascript_engine
5310                            .create_context(CreateJavascriptContextRequest {
5311                                vm_id: vm_id.to_owned(),
5312                                bootstrap_module: None,
5313                                compile_cache_root: Some(
5314                                    self.cache_root.join("node-compile-cache"),
5315                                ),
5316                            });
5317                    let inline_code = load_javascript_entrypoint_source(
5318                        vm,
5319                        &resolved.host_cwd,
5320                        &resolved.entrypoint,
5321                        &execution_env,
5322                    );
5323
5324                    let module_reader = build_module_reader(vm)
5325                        .map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
5326                    let execution = self
5327                        .javascript_engine
5328                        .start_execution_with_module_reader(
5329                            StartJavascriptExecutionRequest {
5330                                vm_id: vm_id.to_owned(),
5331                                context_id: context.context_id,
5332                                argv: std::iter::once(resolved.entrypoint.clone())
5333                                    .chain(resolved.execution_args.clone())
5334                                    .collect(),
5335                                env: execution_env,
5336                                cwd: resolved.host_cwd.clone(),
5337                                inline_code,
5338                            },
5339                            module_reader,
5340                        )
5341                        .map_err(javascript_error)?;
5342                    ActiveExecution::Javascript(execution)
5343                }
5344                GuestRuntimeKind::WebAssembly => {
5345                    execution_env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
5346                    execution_env.insert(
5347                        String::from("AGENT_OS_VIRTUAL_PROCESS_PID"),
5348                        kernel_pid.to_string(),
5349                    );
5350                    execution_env.insert(
5351                        String::from("AGENT_OS_VIRTUAL_PROCESS_PPID"),
5352                        parent_kernel_pid.to_string(),
5353                    );
5354                    apply_wasm_limit_env(&mut execution_env, vm.kernel.resource_limits());
5355                    let context = self.wasm_engine.create_context(CreateWasmContextRequest {
5356                        vm_id: vm_id.to_owned(),
5357                        module_path: Some(resolved.entrypoint.clone()),
5358                    });
5359                    let execution = self
5360                        .wasm_engine
5361                        .start_execution(StartWasmExecutionRequest {
5362                            vm_id: vm_id.to_owned(),
5363                            context_id: context.context_id,
5364                            argv: resolved.process_args.clone(),
5365                            env: execution_env,
5366                            cwd: resolved.host_cwd.clone(),
5367                            permission_tier: execution_wasm_permission_tier(
5368                                resolved
5369                                    .wasm_permission_tier
5370                                    .unwrap_or(WasmPermissionTier::Full),
5371                            ),
5372                        })
5373                        .map_err(wasm_error)?;
5374                    ActiveExecution::Wasm(execution)
5375                }
5376                GuestRuntimeKind::Python => {
5377                    unreachable!("python child_process execution is rejected")
5378                }
5379            };
5380            let kernel_stdin_writer_fd = match javascript_child_process_stdin_mode(&request) {
5381                "pipe" => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5382                "ignore" => {
5383                    vm.kernel
5384                        .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, 0)
5385                        .map_err(kernel_error)?;
5386                    None
5387                }
5388                "inherit" => None,
5389                _ => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5390            };
5391            (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd)
5392        };
5393
5394        let process = vm
5395            .active_processes
5396            .get_mut(process_id)
5397            .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5398        process.child_processes.insert(
5399            child_process_id.clone(),
5400            ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
5401                .with_detached(request.options.detached)
5402                .with_guest_cwd(resolved.guest_cwd.clone())
5403                .with_env(resolved.env.clone())
5404                .with_host_cwd(resolved.host_cwd.clone()),
5405        );
5406        if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd {
5407            process
5408                .child_processes
5409                .get_mut(&child_process_id)
5410                .ok_or_else(|| {
5411                    SidecarError::InvalidState(format!(
5412                        "child process {child_process_id} disappeared during spawn"
5413                    ))
5414                })?
5415                .kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd);
5416        }
5417        Ok(json!({
5418            "childId": child_process_id,
5419            "pid": kernel_pid,
5420            "command": resolved.command,
5421            "args": resolved.process_args,
5422        }))
5423    }
5424
5425    pub(crate) fn spawn_javascript_child_process_sync(
5426        &mut self,
5427        vm_id: &str,
5428        process_id: &str,
5429        request: JavascriptChildProcessSpawnRequest,
5430        max_buffer: Option<usize>,
5431    ) -> Result<Value, SidecarError> {
5432        let sync_input = javascript_child_process_sync_input_bytes(request.options.input.as_ref())?;
5433        let timeout_deadline = request
5434            .options
5435            .timeout
5436            .map(|timeout_ms| Instant::now() + Duration::from_millis(timeout_ms));
5437        let timeout_signal = request
5438            .options
5439            .kill_signal
5440            .clone()
5441            .unwrap_or_else(|| String::from("SIGTERM"));
5442        let spawned = self.spawn_javascript_child_process(vm_id, process_id, request)?;
5443        let child_process_id = spawned
5444            .get("childId")
5445            .and_then(Value::as_str)
5446            .ok_or_else(|| {
5447                SidecarError::InvalidState(String::from(
5448                    "child_process.spawn_sync response is missing childId",
5449                ))
5450            })?
5451            .to_owned();
5452
5453        if let Some(input) = sync_input.as_deref() {
5454            self.write_javascript_child_process_stdin(vm_id, process_id, &child_process_id, input)?;
5455        }
5456        self.close_javascript_child_process_stdin(vm_id, process_id, &child_process_id)?;
5457
5458        let max_buffer = max_buffer.unwrap_or(1024 * 1024);
5459        let mut stdout = Vec::new();
5460        let mut stderr = Vec::new();
5461        let mut max_buffer_exceeded = false;
5462        let mut kill_sent = false;
5463        let mut timed_out = false;
5464
5465        let exit_code = loop {
5466            let wait_ms = if let Some(deadline) = timeout_deadline {
5467                let now = Instant::now();
5468                if now >= deadline {
5469                    if !kill_sent {
5470                        timed_out = true;
5471                        self.kill_javascript_child_process(
5472                            vm_id,
5473                            process_id,
5474                            &child_process_id,
5475                            &timeout_signal,
5476                        )?;
5477                        kill_sent = true;
5478                    }
5479                    0
5480                } else {
5481                    u64::try_from(deadline.saturating_duration_since(now).as_millis().min(50))
5482                        .unwrap_or(50)
5483                }
5484            } else {
5485                50
5486            };
5487            let event =
5488                self.poll_javascript_child_process(vm_id, process_id, &child_process_id, wait_ms)?;
5489            if event.is_null() {
5490                continue;
5491            }
5492
5493            match event.get("type").and_then(Value::as_str) {
5494                Some("stdout") => {
5495                    let chunk = javascript_sync_rpc_bytes_arg(
5496                        &[event.get("data").cloned().unwrap_or(Value::Null)],
5497                        0,
5498                        "child_process.spawn_sync stdout",
5499                    )?;
5500                    stdout.extend_from_slice(&chunk);
5501                    if stdout.len() > max_buffer && !kill_sent {
5502                        max_buffer_exceeded = true;
5503                        self.kill_javascript_child_process(
5504                            vm_id,
5505                            process_id,
5506                            &child_process_id,
5507                            "SIGTERM",
5508                        )?;
5509                        kill_sent = true;
5510                    }
5511                }
5512                Some("stderr") => {
5513                    let chunk = javascript_sync_rpc_bytes_arg(
5514                        &[event.get("data").cloned().unwrap_or(Value::Null)],
5515                        0,
5516                        "child_process.spawn_sync stderr",
5517                    )?;
5518                    stderr.extend_from_slice(&chunk);
5519                    if stderr.len() > max_buffer && !kill_sent {
5520                        max_buffer_exceeded = true;
5521                        self.kill_javascript_child_process(
5522                            vm_id,
5523                            process_id,
5524                            &child_process_id,
5525                            "SIGTERM",
5526                        )?;
5527                        kill_sent = true;
5528                    }
5529                }
5530                Some("exit") => {
5531                    break event
5532                        .get("exitCode")
5533                        .and_then(Value::as_i64)
5534                        .map(|value| value as i32)
5535                        .unwrap_or(1);
5536                }
5537                _ => {}
5538            }
5539        };
5540
5541        Ok(json!({
5542            "stdout": String::from_utf8_lossy(&stdout),
5543            "stderr": String::from_utf8_lossy(&stderr),
5544            "code": exit_code,
5545            "signal": if timed_out { Value::String(timeout_signal) } else { Value::Null },
5546            "timedOut": timed_out,
5547            "maxBufferExceeded": max_buffer_exceeded,
5548        }))
5549    }
5550
5551    fn spawn_descendant_javascript_child_process(
5552        &mut self,
5553        vm_id: &str,
5554        process_id: &str,
5555        current_process_path: &[&str],
5556        request: JavascriptChildProcessSpawnRequest,
5557    ) -> Result<Value, SidecarError> {
5558        let current_process_label =
5559            Self::child_process_path_label(process_id, current_process_path);
5560        let (resolved, parent_kernel_pid) = {
5561            let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?;
5562            let root = vm
5563                .active_processes
5564                .get(process_id)
5565                .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5566            let parent =
5567                Self::active_process_by_path(root, current_process_path).ok_or_else(|| {
5568                    SidecarError::InvalidState(format!(
5569                        "unknown child process path {current_process_label} during nested spawn"
5570                    ))
5571                })?;
5572            (
5573                self.resolve_javascript_child_process_execution(
5574                    vm,
5575                    &parent.env,
5576                    &parent.guest_cwd,
5577                    &parent.host_cwd,
5578                    &request,
5579                )?,
5580                parent.kernel_pid,
5581            )
5582        };
5583
5584        let sidecar_requests = self.sidecar_requests.clone();
5585        let vm = self
5586            .vms
5587            .get_mut(vm_id)
5588            .ok_or_else(|| missing_vm_error(vm_id))?;
5589        let child_process_id = {
5590            let root = vm
5591                .active_processes
5592                .get_mut(process_id)
5593                .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5594            let parent =
5595                Self::active_process_by_path_mut(root, current_process_path).ok_or_else(|| {
5596                    SidecarError::InvalidState(format!(
5597                        "unknown child process path {current_process_label} during nested spawn"
5598                    ))
5599                })?;
5600            parent.allocate_child_process_id()
5601        };
5602        let mut child_path = current_process_path.to_vec();
5603        child_path.push(child_process_id.as_str());
5604        let (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd) = if resolved
5605            .tool_command
5606        {
5607            let tool_resolution = resolve_tool_command(
5608                vm,
5609                &resolved.command,
5610                &resolved.execution_args,
5611                Some(&resolved.guest_cwd),
5612            )?
5613            .ok_or_else(|| {
5614                SidecarError::InvalidState(format!(
5615                    "tool command no longer resolves: {}",
5616                    resolved.command
5617                ))
5618            })?;
5619            let kernel_handle = vm
5620                .kernel
5621                .create_virtual_process(
5622                    EXECUTION_DRIVER_NAME,
5623                    TOOL_DRIVER_NAME,
5624                    &resolved.command,
5625                    resolved.process_args.clone(),
5626                    VirtualProcessOptions {
5627                        parent_pid: Some(parent_kernel_pid),
5628                        env: resolved.env.clone(),
5629                        cwd: Some(resolved.guest_cwd.clone()),
5630                    },
5631                )
5632                .map_err(kernel_error)?;
5633            let kernel_pid = kernel_handle.pid();
5634            let tool_execution = ToolExecution::default();
5635            let cancelled = tool_execution.cancelled.clone();
5636            let pending_events = tool_execution.pending_events.clone();
5637            let events_overflowed = tool_execution.events_overflowed.clone();
5638            spawn_tool_process_events(ToolProcessEventRequest {
5639                sidecar_requests: sidecar_requests.clone(),
5640                connection_id: vm.connection_id.clone(),
5641                session_id: vm.session_id.clone(),
5642                vm_id: vm_id.to_owned(),
5643                tool_resolution,
5644                cancelled,
5645                pending_events,
5646                events_overflowed,
5647            });
5648            (
5649                kernel_pid,
5650                kernel_handle,
5651                ActiveExecution::Tool(tool_execution),
5652                None,
5653            )
5654        } else {
5655            let kernel_command = match resolved.runtime {
5656                GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND,
5657                GuestRuntimeKind::WebAssembly => WASM_COMMAND,
5658                GuestRuntimeKind::Python => {
5659                    unreachable!("python child_process execution is rejected")
5660                }
5661            };
5662            let kernel_handle = vm
5663                .kernel
5664                .spawn_process(
5665                    kernel_command,
5666                    resolved.process_args.clone(),
5667                    SpawnOptions {
5668                        requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
5669                        parent_pid: Some(parent_kernel_pid),
5670                        env: resolved.env.clone(),
5671                        cwd: Some(resolved.guest_cwd.clone()),
5672                    },
5673                )
5674                .map_err(kernel_error)?;
5675            let kernel_pid = kernel_handle.pid();
5676            if request.options.detached {
5677                vm.kernel
5678                    .setsid(EXECUTION_DRIVER_NAME, kernel_pid)
5679                    .map_err(kernel_error)?;
5680            }
5681            let mut execution_env = resolved.env.clone();
5682            execution_env.insert(
5683                String::from(EXECUTION_SANDBOX_ROOT_ENV),
5684                normalize_host_path(&vm.cwd).to_string_lossy().into_owned(),
5685            );
5686            let execution = match resolved.runtime {
5687                GuestRuntimeKind::JavaScript => {
5688                    execution_env.extend(sanitize_javascript_child_process_internal_bootstrap_env(
5689                        &request.options.internal_bootstrap_env,
5690                    ));
5691                    execution_env.insert(
5692                        String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
5693                        String::from("1"),
5694                    );
5695                    execution_env.insert(
5696                        String::from("AGENT_OS_VIRTUAL_PROCESS_PID"),
5697                        kernel_pid.to_string(),
5698                    );
5699                    execution_env.insert(
5700                        String::from("AGENT_OS_VIRTUAL_PROCESS_PPID"),
5701                        parent_kernel_pid.to_string(),
5702                    );
5703                    let context =
5704                        self.javascript_engine
5705                            .create_context(CreateJavascriptContextRequest {
5706                                vm_id: vm_id.to_owned(),
5707                                bootstrap_module: None,
5708                                compile_cache_root: Some(
5709                                    self.cache_root.join("node-compile-cache"),
5710                                ),
5711                            });
5712                    let inline_code = load_javascript_entrypoint_source(
5713                        vm,
5714                        &resolved.host_cwd,
5715                        &resolved.entrypoint,
5716                        &execution_env,
5717                    );
5718
5719                    let module_reader = build_module_reader(vm)
5720                        .map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
5721                    let execution = self
5722                        .javascript_engine
5723                        .start_execution_with_module_reader(
5724                            StartJavascriptExecutionRequest {
5725                                vm_id: vm_id.to_owned(),
5726                                context_id: context.context_id,
5727                                argv: std::iter::once(resolved.entrypoint.clone())
5728                                    .chain(resolved.execution_args.clone())
5729                                    .collect(),
5730                                env: execution_env,
5731                                cwd: resolved.host_cwd.clone(),
5732                                inline_code,
5733                            },
5734                            module_reader,
5735                        )
5736                        .map_err(javascript_error)?;
5737                    ActiveExecution::Javascript(execution)
5738                }
5739                GuestRuntimeKind::WebAssembly => {
5740                    execution_env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
5741                    execution_env.insert(
5742                        String::from("AGENT_OS_VIRTUAL_PROCESS_PID"),
5743                        kernel_pid.to_string(),
5744                    );
5745                    execution_env.insert(
5746                        String::from("AGENT_OS_VIRTUAL_PROCESS_PPID"),
5747                        parent_kernel_pid.to_string(),
5748                    );
5749                    apply_wasm_limit_env(&mut execution_env, vm.kernel.resource_limits());
5750                    let context = self.wasm_engine.create_context(CreateWasmContextRequest {
5751                        vm_id: vm_id.to_owned(),
5752                        module_path: Some(resolved.entrypoint.clone()),
5753                    });
5754                    let execution = self
5755                        .wasm_engine
5756                        .start_execution(StartWasmExecutionRequest {
5757                            vm_id: vm_id.to_owned(),
5758                            context_id: context.context_id,
5759                            argv: resolved.process_args.clone(),
5760                            env: execution_env,
5761                            cwd: resolved.host_cwd.clone(),
5762                            permission_tier: execution_wasm_permission_tier(
5763                                resolved
5764                                    .wasm_permission_tier
5765                                    .unwrap_or(WasmPermissionTier::Full),
5766                            ),
5767                        })
5768                        .map_err(wasm_error)?;
5769                    ActiveExecution::Wasm(execution)
5770                }
5771                GuestRuntimeKind::Python => {
5772                    unreachable!("python child_process execution is rejected")
5773                }
5774            };
5775            let kernel_stdin_writer_fd = match javascript_child_process_stdin_mode(&request) {
5776                "pipe" => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5777                "ignore" => {
5778                    vm.kernel
5779                        .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, 0)
5780                        .map_err(kernel_error)?;
5781                    None
5782                }
5783                "inherit" => None,
5784                _ => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5785            };
5786            (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd)
5787        };
5788
5789        let root = vm
5790            .active_processes
5791            .get_mut(process_id)
5792            .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5793        let parent =
5794            Self::active_process_by_path_mut(root, current_process_path).ok_or_else(|| {
5795                SidecarError::InvalidState(format!(
5796                    "unknown child process path {current_process_label} during nested spawn"
5797                ))
5798            })?;
5799        parent.child_processes.insert(
5800            child_process_id.clone(),
5801            ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
5802                .with_detached(request.options.detached)
5803                .with_guest_cwd(resolved.guest_cwd.clone())
5804                .with_env(resolved.env.clone())
5805                .with_host_cwd(resolved.host_cwd.clone()),
5806        );
5807        if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd {
5808            parent
5809                .child_processes
5810                .get_mut(&child_process_id)
5811                .ok_or_else(|| {
5812                    SidecarError::InvalidState(format!(
5813                        "child process {child_process_id} disappeared during nested spawn"
5814                    ))
5815                })?
5816                .kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd);
5817        }
5818        Ok(json!({
5819            "childId": child_process_id,
5820            "pid": kernel_pid,
5821            "command": resolved.command,
5822            "args": resolved.process_args,
5823        }))
5824    }
5825
5826    fn spawn_descendant_javascript_child_process_sync(
5827        &mut self,
5828        vm_id: &str,
5829        process_id: &str,
5830        current_process_path: &[&str],
5831        request: JavascriptChildProcessSpawnRequest,
5832        max_buffer: Option<usize>,
5833    ) -> Result<Value, SidecarError> {
5834        let sync_input = javascript_child_process_sync_input_bytes(request.options.input.as_ref())?;
5835        let timeout_deadline = request
5836            .options
5837            .timeout
5838            .map(|timeout_ms| Instant::now() + Duration::from_millis(timeout_ms));
5839        let timeout_signal = request
5840            .options
5841            .kill_signal
5842            .clone()
5843            .unwrap_or_else(|| String::from("SIGTERM"));
5844        let spawned = self.spawn_descendant_javascript_child_process(
5845            vm_id,
5846            process_id,
5847            current_process_path,
5848            request,
5849        )?;
5850        let child_process_id = spawned
5851            .get("childId")
5852            .and_then(Value::as_str)
5853            .ok_or_else(|| {
5854                SidecarError::InvalidState(String::from(
5855                    "child_process.spawn_sync response is missing childId",
5856                ))
5857            })?
5858            .to_owned();
5859
5860        if let Some(input) = sync_input.as_deref() {
5861            self.write_descendant_javascript_child_process_stdin(
5862                vm_id,
5863                process_id,
5864                current_process_path,
5865                &child_process_id,
5866                input,
5867            )?;
5868        }
5869        self.close_descendant_javascript_child_process_stdin(
5870            vm_id,
5871            process_id,
5872            current_process_path,
5873            &child_process_id,
5874        )?;
5875
5876        let max_buffer = max_buffer.unwrap_or(1024 * 1024);
5877        let mut stdout = Vec::new();
5878        let mut stderr = Vec::new();
5879        let mut max_buffer_exceeded = false;
5880        let mut kill_sent = false;
5881        let mut timed_out = false;
5882
5883        let exit_code = loop {
5884            let wait_ms = if let Some(deadline) = timeout_deadline {
5885                let now = Instant::now();
5886                if now >= deadline {
5887                    if !kill_sent {
5888                        timed_out = true;
5889                        self.kill_descendant_javascript_child_process(
5890                            vm_id,
5891                            process_id,
5892                            current_process_path,
5893                            &child_process_id,
5894                            &timeout_signal,
5895                        )?;
5896                        kill_sent = true;
5897                    }
5898                    0
5899                } else {
5900                    u64::try_from(deadline.saturating_duration_since(now).as_millis().min(50))
5901                        .unwrap_or(50)
5902                }
5903            } else {
5904                50
5905            };
5906            let event = self.poll_descendant_javascript_child_process(
5907                vm_id,
5908                process_id,
5909                current_process_path,
5910                &child_process_id,
5911                wait_ms,
5912            )?;
5913            if event.is_null() {
5914                continue;
5915            }
5916
5917            match event.get("type").and_then(Value::as_str) {
5918                Some("stdout") => {
5919                    let chunk = javascript_sync_rpc_bytes_arg(
5920                        &[event.get("data").cloned().unwrap_or(Value::Null)],
5921                        0,
5922                        "child_process.spawn_sync stdout",
5923                    )?;
5924                    stdout.extend_from_slice(&chunk);
5925                    if stdout.len() > max_buffer && !kill_sent {
5926                        max_buffer_exceeded = true;
5927                        self.kill_descendant_javascript_child_process(
5928                            vm_id,
5929                            process_id,
5930                            current_process_path,
5931                            &child_process_id,
5932                            "SIGTERM",
5933                        )?;
5934                        kill_sent = true;
5935                    }
5936                }
5937                Some("stderr") => {
5938                    let chunk = javascript_sync_rpc_bytes_arg(
5939                        &[event.get("data").cloned().unwrap_or(Value::Null)],
5940                        0,
5941                        "child_process.spawn_sync stderr",
5942                    )?;
5943                    stderr.extend_from_slice(&chunk);
5944                    if stderr.len() > max_buffer && !kill_sent {
5945                        max_buffer_exceeded = true;
5946                        self.kill_descendant_javascript_child_process(
5947                            vm_id,
5948                            process_id,
5949                            current_process_path,
5950                            &child_process_id,
5951                            "SIGTERM",
5952                        )?;
5953                        kill_sent = true;
5954                    }
5955                }
5956                Some("exit") => {
5957                    break event
5958                        .get("exitCode")
5959                        .and_then(Value::as_i64)
5960                        .map(|value| value as i32)
5961                        .unwrap_or(1);
5962                }
5963                _ => {}
5964            }
5965        };
5966
5967        Ok(json!({
5968            "stdout": String::from_utf8_lossy(&stdout),
5969            "stderr": String::from_utf8_lossy(&stderr),
5970            "code": exit_code,
5971            "signal": if timed_out { Value::String(timeout_signal) } else { Value::Null },
5972            "timedOut": timed_out,
5973            "maxBufferExceeded": max_buffer_exceeded,
5974        }))
5975    }
5976
5977    fn handle_descendant_javascript_child_process_rpc(
5978        &mut self,
5979        vm_id: &str,
5980        process_id: &str,
5981        current_process_path: &[&str],
5982        request: &JavascriptSyncRpcRequest,
5983    ) -> Result<Value, SidecarError> {
5984        match request.method.as_str() {
5985            "child_process.spawn" => {
5986                let Some(vm) = self.vms.get(vm_id) else {
5987                    return Ok(Value::Null);
5988                };
5989                let (payload, _) = parse_javascript_child_process_spawn_request(vm, &request.args)?;
5990                self.spawn_descendant_javascript_child_process(
5991                    vm_id,
5992                    process_id,
5993                    current_process_path,
5994                    payload,
5995                )
5996            }
5997            "child_process.spawn_sync" => {
5998                let Some(vm) = self.vms.get(vm_id) else {
5999                    return Ok(Value::Null);
6000                };
6001                let (payload, max_buffer) =
6002                    parse_javascript_child_process_spawn_request(vm, &request.args)?;
6003                self.spawn_descendant_javascript_child_process_sync(
6004                    vm_id,
6005                    process_id,
6006                    current_process_path,
6007                    payload,
6008                    max_buffer,
6009                )
6010            }
6011            "child_process.poll" => {
6012                let child_process_id =
6013                    javascript_sync_rpc_arg_str(&request.args, 0, "child_process.poll child id")?;
6014                let wait_ms = javascript_sync_rpc_arg_u64_optional(
6015                    &request.args,
6016                    1,
6017                    "child_process.poll wait ms",
6018                )?
6019                .unwrap_or_default();
6020                self.poll_descendant_javascript_child_process(
6021                    vm_id,
6022                    process_id,
6023                    current_process_path,
6024                    child_process_id,
6025                    wait_ms,
6026                )
6027            }
6028            "child_process.write_stdin" => {
6029                let child_process_id = javascript_sync_rpc_arg_str(
6030                    &request.args,
6031                    0,
6032                    "child_process.write_stdin child id",
6033                )?;
6034                let chunk = javascript_sync_rpc_bytes_arg(
6035                    &request.args,
6036                    1,
6037                    "child_process.write_stdin chunk",
6038                )?;
6039                self.write_descendant_javascript_child_process_stdin(
6040                    vm_id,
6041                    process_id,
6042                    current_process_path,
6043                    child_process_id,
6044                    &chunk,
6045                )?;
6046                Ok(Value::Null)
6047            }
6048            "child_process.close_stdin" => {
6049                let child_process_id = javascript_sync_rpc_arg_str(
6050                    &request.args,
6051                    0,
6052                    "child_process.close_stdin child id",
6053                )?;
6054                self.close_descendant_javascript_child_process_stdin(
6055                    vm_id,
6056                    process_id,
6057                    current_process_path,
6058                    child_process_id,
6059                )?;
6060                Ok(Value::Null)
6061            }
6062            "child_process.kill" => {
6063                let child_process_id =
6064                    javascript_sync_rpc_arg_str(&request.args, 0, "child_process.kill child id")?;
6065                let signal =
6066                    javascript_sync_rpc_arg_str(&request.args, 1, "child_process.kill signal")?;
6067                self.kill_descendant_javascript_child_process(
6068                    vm_id,
6069                    process_id,
6070                    current_process_path,
6071                    child_process_id,
6072                    signal,
6073                )?;
6074                Ok(Value::Null)
6075            }
6076            _ => Err(SidecarError::InvalidState(format!(
6077                "unsupported nested child process RPC method {}",
6078                request.method
6079            ))),
6080        }
6081    }
6082
6083    fn poll_descendant_javascript_child_process(
6084        &mut self,
6085        vm_id: &str,
6086        process_id: &str,
6087        current_process_path: &[&str],
6088        child_process_id: &str,
6089        wait_ms: u64,
6090    ) -> Result<Value, SidecarError> {
6091        let mut child_path = current_process_path.to_vec();
6092        child_path.push(child_process_id);
6093        let child_gone_error = || javascript_child_process_gone_error(process_id, &child_path);
6094        let deadline = Instant::now() + Duration::from_millis(wait_ms);
6095        let mut polled_once = false;
6096
6097        loop {
6098            self.drain_queued_descendant_javascript_child_process_events(
6099                vm_id,
6100                process_id,
6101                &child_path,
6102            )?;
6103            enum ChildPollResult {
6104                Event(Box<Option<ActiveExecutionEvent>>),
6105                RecoverRuntimeExit,
6106                Timeout,
6107            }
6108            let wait = if wait_ms == 0 {
6109                Duration::ZERO
6110            } else {
6111                deadline.saturating_duration_since(Instant::now())
6112            };
6113            let poll_result = {
6114                let Some(vm) = self.vms.get_mut(vm_id) else {
6115                    return Ok(Value::Null);
6116                };
6117                let Some(parent) =
6118                    Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6119                else {
6120                    return Err(child_gone_error());
6121                };
6122                let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6123                    return Err(child_gone_error());
6124                };
6125                if let Some(event) = child.pending_execution_events.pop_front() {
6126                    ChildPollResult::Event(Box::new(Some(event)))
6127                } else if polled_once && wait.is_zero() {
6128                    ChildPollResult::Timeout
6129                } else {
6130                    polled_once = true;
6131                    match child.execution.poll_event_blocking(wait) {
6132                        Ok(Some(event)) => ChildPollResult::Event(Box::new(Some(event))),
6133                        Ok(None) => ChildPollResult::RecoverRuntimeExit,
6134                        Err(SidecarError::Execution(message))
6135                            if (child.runtime == GuestRuntimeKind::JavaScript
6136                                && closed_javascript_event_channel(&message))
6137                                || (child.runtime == GuestRuntimeKind::Python
6138                                    && closed_python_event_channel(&message))
6139                                || (child.runtime == GuestRuntimeKind::WebAssembly
6140                                    && closed_wasm_event_channel(&message)) =>
6141                        {
6142                            ChildPollResult::RecoverRuntimeExit
6143                        }
6144                        Err(error) => return Err(error),
6145                    }
6146                }
6147            };
6148            let event = match poll_result {
6149                ChildPollResult::Event(event) => *event,
6150                ChildPollResult::Timeout => return Ok(Value::Null),
6151                ChildPollResult::RecoverRuntimeExit => self
6152                    .recover_descendant_runtime_child_process_event(
6153                        vm_id,
6154                        process_id,
6155                        current_process_path,
6156                        child_process_id,
6157                        wait.as_millis().try_into().unwrap_or(u64::MAX),
6158                    )?,
6159            };
6160
6161            let Some(event) = event else {
6162                return Ok(Value::Null);
6163            };
6164
6165            match event {
6166                ActiveExecutionEvent::Stdout(chunk) => {
6167                    return Ok(json!({
6168                        "type": "stdout",
6169                        "data": javascript_sync_rpc_bytes_value(&chunk),
6170                    }));
6171                }
6172                ActiveExecutionEvent::Stderr(chunk) => {
6173                    return Ok(json!({
6174                        "type": "stderr",
6175                        "data": javascript_sync_rpc_bytes_value(&chunk),
6176                    }));
6177                }
6178                ActiveExecutionEvent::Exited(exit_code) => {
6179                    let had_trailing_events = {
6180                        let Some(vm) = self.vms.get_mut(vm_id) else {
6181                            return Ok(Value::Null);
6182                        };
6183                        let Some(parent) = Self::descendant_parent_process_mut(
6184                            vm,
6185                            process_id,
6186                            current_process_path,
6187                        ) else {
6188                            return Ok(Value::Null);
6189                        };
6190                        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6191                            return Ok(Value::Null);
6192                        };
6193                        let deadline = Instant::now() + Duration::from_millis(150);
6194                        loop {
6195                            let wait = deadline.saturating_duration_since(Instant::now());
6196                            let next = poll_child_execution_after_exit(child, wait)?;
6197                            let Some(next) = next else {
6198                                break;
6199                            };
6200                            if matches!(next, ActiveExecutionEvent::Exited(_)) {
6201                                continue;
6202                            }
6203                            child.queue_pending_execution_event(next)?;
6204                            if Instant::now() >= deadline {
6205                                break;
6206                            }
6207                        }
6208                        if !child.pending_execution_events.is_empty() {
6209                            child.queue_pending_execution_event(ActiveExecutionEvent::Exited(
6210                                exit_code,
6211                            ))?;
6212                            true
6213                        } else {
6214                            false
6215                        }
6216                    };
6217                    if had_trailing_events {
6218                        continue;
6219                    }
6220
6221                    let parent_signal_key =
6222                        Self::child_process_signal_key(process_id, current_process_path);
6223                    let Some(vm) = self.vms.get_mut(vm_id) else {
6224                        return Ok(Value::Null);
6225                    };
6226                    let signal_name = {
6227                        let Some(parent) = Self::descendant_parent_process_mut(
6228                            vm,
6229                            process_id,
6230                            current_process_path,
6231                        ) else {
6232                            return Ok(Value::Null);
6233                        };
6234                        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6235                            return Ok(Value::Null);
6236                        };
6237                        child.pending_self_signal_exit.take().and_then(|signal| {
6238                            if exit_code == 128 + signal {
6239                                canonical_signal_name(signal).map(str::to_owned)
6240                            } else {
6241                                None
6242                            }
6243                        })
6244                    };
6245                    let (parent_runtime_pid, parent_v8_signal_session, should_signal_parent) = {
6246                        let Some(parent) =
6247                            Self::descendant_parent_process(vm, process_id, current_process_path)
6248                        else {
6249                            return Ok(Value::Null);
6250                        };
6251                        (
6252                            parent.execution.child_pid(),
6253                            parent.execution.javascript_v8_session_handle().filter(|_| {
6254                                matches!(
6255                                    &parent.execution,
6256                                    ActiveExecution::Javascript(execution)
6257                                        if execution.uses_shared_v8_runtime()
6258                                )
6259                            }),
6260                            vm.signal_states
6261                                .get(parent_signal_key)
6262                                .and_then(|handlers| handlers.get(&(libc::SIGCHLD as u32)))
6263                                .is_some_and(|registration| {
6264                                    registration.action != SignalDispositionAction::Default
6265                                }),
6266                        )
6267                    };
6268                    let Some(parent) =
6269                        Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6270                    else {
6271                        return Ok(Value::Null);
6272                    };
6273                    let Some(mut child) = parent.child_processes.remove(child_process_id) else {
6274                        return Ok(Value::Null);
6275                    };
6276                    let child_process_label =
6277                        Self::child_process_path_label(process_id, &child_path);
6278                    let detached_children =
6279                        Self::adopt_detached_child_processes(&child_process_label, &mut child);
6280                    sync_process_host_writes_to_kernel(vm, &child)?;
6281                    terminate_child_process_tree(&mut vm.kernel, &mut child);
6282                    child.kernel_handle.finish(exit_code);
6283                    let _ = vm.kernel.wait_and_reap(child.kernel_pid);
6284                    vm.signal_states.remove(child_process_id);
6285                    for (detached_process_id, detached_child) in detached_children {
6286                        vm.detached_child_processes
6287                            .insert(detached_process_id.clone());
6288                        vm.active_processes
6289                            .insert(detached_process_id, detached_child);
6290                    }
6291                    if should_signal_parent {
6292                        if let Some(session) = parent_v8_signal_session {
6293                            dispatch_v8_session_signal_async(session, libc::SIGCHLD);
6294                        } else {
6295                            signal_runtime_process(parent_runtime_pid, libc::SIGCHLD)?;
6296                        }
6297                    }
6298                    let mut payload = Map::new();
6299                    payload.insert(String::from("type"), Value::String(String::from("exit")));
6300                    payload.insert(String::from("exitCode"), Value::from(exit_code));
6301                    if let Some(signal_name) = signal_name {
6302                        payload.insert(String::from("signal"), Value::String(signal_name));
6303                    }
6304                    return Ok(Value::Object(payload));
6305                }
6306                ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
6307                    let mut current_child_path = current_process_path.to_vec();
6308                    current_child_path.push(child_process_id);
6309                    let response = if request.method == "process.signal_state" {
6310                        let (signal, registration) =
6311                            parse_process_signal_state_request(&request.args)?;
6312                        let Some(vm) = self.vms.get_mut(vm_id) else {
6313                            return Ok(Value::Null);
6314                        };
6315                        let signal_key =
6316                            Self::child_process_signal_key(process_id, &current_child_path)
6317                                .to_owned();
6318                        apply_process_signal_state_update(
6319                            &mut vm.signal_states,
6320                            &signal_key,
6321                            signal,
6322                            registration,
6323                        );
6324                        Ok(Value::Null)
6325                    } else if request.method == "process.kill" {
6326                        self.handle_descendant_process_kill_rpc(
6327                            vm_id,
6328                            process_id,
6329                            current_process_path,
6330                            child_process_id,
6331                            &request,
6332                        )
6333                    } else if request.method.starts_with("child_process.") {
6334                        self.handle_descendant_javascript_child_process_rpc(
6335                            vm_id,
6336                            process_id,
6337                            &current_child_path,
6338                            &request,
6339                        )
6340                    } else {
6341                        let Some(vm) = self.vms.get_mut(vm_id) else {
6342                            return Ok(Value::Null);
6343                        };
6344                        let resource_limits = vm.kernel.resource_limits().clone();
6345                        let network_counts = vm_network_resource_counts(vm);
6346                        let socket_paths = build_javascript_socket_path_context(vm)?;
6347                        let Some(root) = vm.active_processes.get_mut(process_id) else {
6348                            return Ok(Value::Null);
6349                        };
6350                        let Some(parent) =
6351                            Self::active_process_by_path_mut(root, current_process_path)
6352                        else {
6353                            return Ok(Value::Null);
6354                        };
6355                        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6356                            return Ok(Value::Null);
6357                        };
6358                        service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
6359                            bridge: &self.bridge,
6360                            vm_id,
6361                            dns: &vm.dns,
6362                            socket_paths: &socket_paths,
6363                            kernel: &mut vm.kernel,
6364                            process: child,
6365                            sync_request: &request,
6366                            resource_limits: &resource_limits,
6367                            network_counts,
6368                        })
6369                    };
6370
6371                    let Some(vm) = self.vms.get_mut(vm_id) else {
6372                        return Ok(Value::Null);
6373                    };
6374                    let Some(parent) =
6375                        Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6376                    else {
6377                        return Ok(Value::Null);
6378                    };
6379                    let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6380                        return Ok(Value::Null);
6381                    };
6382                    let parent_signal_event = response.as_ref().ok().and_then(|result| {
6383                        let target_path_label =
6384                            Self::child_process_path_label(process_id, current_process_path);
6385                        if request.method != "process.kill"
6386                            || result.get("action").and_then(Value::as_str) != Some("user")
6387                            || result.get("targetProcessPath").and_then(Value::as_str)
6388                                != Some(target_path_label.as_str())
6389                        {
6390                            return None;
6391                        }
6392                        Some(json!({
6393                            "type": "signal",
6394                            "signal": result.get("signal").and_then(Value::as_str).unwrap_or_default(),
6395                            "number": result.get("number").and_then(Value::as_i64).unwrap_or_default(),
6396                        }))
6397                    });
6398                    match response {
6399                        Ok(result) => child
6400                            .execution
6401                            .respond_javascript_sync_rpc_success(request.id, result)
6402                            .or_else(ignore_stale_javascript_sync_rpc_response)?,
6403                        Err(error) => child
6404                            .execution
6405                            .respond_javascript_sync_rpc_error(
6406                                request.id,
6407                                javascript_sync_rpc_error_code(&error),
6408                                error.to_string(),
6409                            )
6410                            .or_else(ignore_stale_javascript_sync_rpc_response)?,
6411                    }
6412                    if let Some(event) = parent_signal_event {
6413                        return Ok(event);
6414                    }
6415                }
6416                ActiveExecutionEvent::PythonVfsRpcRequest(_) => {
6417                    return Err(SidecarError::InvalidState(String::from(
6418                        "nested Python child_process execution is not supported yet",
6419                    )));
6420                }
6421                ActiveExecutionEvent::SignalState {
6422                    signal,
6423                    registration,
6424                } => {
6425                    let Some(vm) = self.vms.get_mut(vm_id) else {
6426                        return Ok(Value::Null);
6427                    };
6428                    let signal_key =
6429                        Self::child_process_signal_key(process_id, &child_path).to_owned();
6430                    apply_process_signal_state_update(
6431                        &mut vm.signal_states,
6432                        &signal_key,
6433                        signal,
6434                        registration.clone(),
6435                    );
6436                    return Ok(json!({
6437                        "type": "signal_state",
6438                        "signal": signal,
6439                        "registration": registration,
6440                    }));
6441                }
6442            }
6443        }
6444    }
6445
6446    fn recover_descendant_runtime_child_process_event(
6447        &mut self,
6448        vm_id: &str,
6449        process_id: &str,
6450        current_process_path: &[&str],
6451        child_process_id: &str,
6452        wait_ms: u64,
6453    ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
6454        let (
6455            parent_kernel_pid,
6456            child_kernel_pid,
6457            child_runtime_pid,
6458            child_runtime,
6459            child_shared_runtime,
6460        ) = {
6461            let mut child_path = current_process_path.to_vec();
6462            child_path.push(child_process_id);
6463            let Some(vm) = self.vms.get_mut(vm_id) else {
6464                return Ok(None);
6465            };
6466            let Some(parent) =
6467                Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6468            else {
6469                return Err(javascript_child_process_gone_error(process_id, &child_path));
6470            };
6471            let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6472                return Err(javascript_child_process_gone_error(process_id, &child_path));
6473            };
6474            (
6475                parent.kernel_pid,
6476                child.kernel_pid,
6477                child.execution.child_pid(),
6478                child.runtime.clone(),
6479                child.execution.uses_shared_v8_runtime(),
6480            )
6481        };
6482        if child_runtime != GuestRuntimeKind::JavaScript
6483            && child_runtime != GuestRuntimeKind::Python
6484            && child_runtime != GuestRuntimeKind::WebAssembly
6485        {
6486            return Ok(None);
6487        }
6488        let wait_deadline = Instant::now() + Duration::from_millis(wait_ms.min(25));
6489        loop {
6490            let Some(vm) = self.vms.get_mut(vm_id) else {
6491                return Ok(None);
6492            };
6493            if let Some(process_info) = vm.kernel.list_processes().get(&child_kernel_pid) {
6494                if process_info.status == ProcessStatus::Exited {
6495                    return Ok(Some(ActiveExecutionEvent::Exited(
6496                        process_info.exit_code.unwrap_or(0),
6497                    )));
6498                }
6499            }
6500            if let Some(wait_result) = vm
6501                .kernel
6502                .waitpid_with_options(
6503                    EXECUTION_DRIVER_NAME,
6504                    parent_kernel_pid,
6505                    child_kernel_pid as i32,
6506                    WaitPidFlags::WNOHANG,
6507                )
6508                .map_err(kernel_error)?
6509            {
6510                return Ok(Some(ActiveExecutionEvent::Exited(wait_result.status)));
6511            }
6512
6513            if !child_shared_runtime && child_runtime_pid != 0 {
6514                if let Some(status) = runtime_child_exit_status(child_runtime_pid)? {
6515                    return Ok(Some(ActiveExecutionEvent::Exited(status)));
6516                }
6517                if !runtime_child_is_alive(child_runtime_pid)? {
6518                    return Ok(Some(ActiveExecutionEvent::Exited(0)));
6519                }
6520            }
6521            if Instant::now() >= wait_deadline {
6522                return Ok(None);
6523            }
6524            std::thread::sleep(Duration::from_millis(5));
6525        }
6526    }
6527
6528    fn write_descendant_javascript_child_process_stdin(
6529        &mut self,
6530        vm_id: &str,
6531        process_id: &str,
6532        current_process_path: &[&str],
6533        child_process_id: &str,
6534        chunk: &[u8],
6535    ) -> Result<(), SidecarError> {
6536        let mut child_path = current_process_path.to_vec();
6537        child_path.push(child_process_id);
6538        let Some(vm) = self.vms.get_mut(vm_id) else {
6539            return Err(javascript_child_process_gone_error(process_id, &child_path));
6540        };
6541        let Some(root) = vm.active_processes.get_mut(process_id) else {
6542            return Err(javascript_child_process_gone_error(process_id, &child_path));
6543        };
6544        let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
6545            return Err(javascript_child_process_gone_error(process_id, &child_path));
6546        };
6547        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6548            return Err(javascript_child_process_gone_error(process_id, &child_path));
6549        };
6550        if let Err(error) = child.execution.write_stdin(chunk) {
6551            if is_broken_pipe_error(&error) {
6552                return Ok(());
6553            }
6554            return Err(error);
6555        }
6556        write_kernel_process_stdin(&mut vm.kernel, child, chunk)
6557    }
6558
6559    fn close_descendant_javascript_child_process_stdin(
6560        &mut self,
6561        vm_id: &str,
6562        process_id: &str,
6563        current_process_path: &[&str],
6564        child_process_id: &str,
6565    ) -> Result<(), SidecarError> {
6566        let mut child_path = current_process_path.to_vec();
6567        child_path.push(child_process_id);
6568        let Some(vm) = self.vms.get_mut(vm_id) else {
6569            return Err(javascript_child_process_gone_error(process_id, &child_path));
6570        };
6571        let Some(root) = vm.active_processes.get_mut(process_id) else {
6572            return Err(javascript_child_process_gone_error(process_id, &child_path));
6573        };
6574        let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
6575            return Err(javascript_child_process_gone_error(process_id, &child_path));
6576        };
6577        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6578            return Err(javascript_child_process_gone_error(process_id, &child_path));
6579        };
6580        child.execution.close_stdin()?;
6581        close_kernel_process_stdin(&mut vm.kernel, child)
6582    }
6583
6584    fn kill_descendant_javascript_child_process(
6585        &mut self,
6586        vm_id: &str,
6587        process_id: &str,
6588        current_process_path: &[&str],
6589        child_process_id: &str,
6590        signal: &str,
6591    ) -> Result<(), SidecarError> {
6592        let signal_name = signal.to_owned();
6593        let signal = parse_signal(signal)?;
6594        let Some(vm) = self.vms.get_mut(vm_id) else {
6595            return Ok(());
6596        };
6597        let Some(root) = vm.active_processes.get_mut(process_id) else {
6598            return Ok(());
6599        };
6600        let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
6601            return Ok(());
6602        };
6603        let source_pid = parent.kernel_pid;
6604        let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6605            return Ok(());
6606        };
6607        terminate_tracked_child_process_for_signal(&mut vm.kernel, child, signal)?;
6608        let child_process_label = if current_process_path.is_empty() {
6609            child_process_id.to_owned()
6610        } else {
6611            format!("{}/{}", current_process_path.join("/"), child_process_id)
6612        };
6613        emit_security_audit_event(
6614            &self.bridge,
6615            vm_id,
6616            "security.process.kill",
6617            audit_fields([
6618                (String::from("source"), String::from("guest_child_process")),
6619                (String::from("source_pid"), source_pid.to_string()),
6620                (String::from("target_pid"), child.kernel_pid.to_string()),
6621                (String::from("process_id"), process_id.to_owned()),
6622                (String::from("child_process_id"), child_process_label),
6623                (String::from("signal"), signal_name),
6624            ]),
6625        );
6626        Ok(())
6627    }
6628
6629    fn handle_descendant_process_kill_rpc(
6630        &mut self,
6631        vm_id: &str,
6632        process_id: &str,
6633        current_process_path: &[&str],
6634        child_process_id: &str,
6635        request: &JavascriptSyncRpcRequest,
6636    ) -> Result<Value, SidecarError> {
6637        let target_pid = javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?;
6638        let signal_name = javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?;
6639        let signal = parse_signal(signal_name)?;
6640
6641        let mut source_path = current_process_path.to_vec();
6642        source_path.push(child_process_id);
6643
6644        if signal != 0 && target_pid < 0 {
6645            let pgid = target_pid.unsigned_abs();
6646            let caller_kernel_pid = {
6647                let Some(vm) = self.vms.get(vm_id) else {
6648                    return Err(SidecarError::InvalidState(String::from(
6649                        "ESRCH: unknown VM during process.kill",
6650                    )));
6651                };
6652                let Some(root) = vm.active_processes.get(process_id) else {
6653                    return Err(SidecarError::InvalidState(format!(
6654                        "ESRCH: unknown process {process_id} during process.kill",
6655                    )));
6656                };
6657                let Some(source) = Self::active_process_by_path(root, &source_path) else {
6658                    return Err(SidecarError::InvalidState(format!(
6659                        "ESRCH: unknown child process {child_process_id} during process.kill",
6660                    )));
6661                };
6662                source.kernel_pid
6663            };
6664            let caller_is_member =
6665                self.signal_vm_process_group(vm_id, caller_kernel_pid, pgid, signal_name)?;
6666            if !caller_is_member {
6667                return Ok(Value::Null);
6668            }
6669            let Some(vm) = self.vms.get_mut(vm_id) else {
6670                return Ok(Value::Null);
6671            };
6672            let Some(root) = vm.active_processes.get_mut(process_id) else {
6673                return Ok(Value::Null);
6674            };
6675            let Some(source) = Self::active_process_by_path_mut(root, &source_path) else {
6676                return Ok(Value::Null);
6677            };
6678            source.pending_self_signal_exit = None;
6679            if !matches!(
6680                canonical_signal_name(signal),
6681                Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
6682            ) {
6683                source.pending_self_signal_exit = Some(signal);
6684            }
6685            return Ok(json!({
6686                "self": true,
6687                "action": "default",
6688            }));
6689        }
6690
6691        let Some(vm) = self.vms.get_mut(vm_id) else {
6692            return Err(SidecarError::InvalidState(String::from(
6693                "ESRCH: unknown VM during process.kill",
6694            )));
6695        };
6696
6697        if signal == 0 {
6698            vm.kernel
6699                .signal_process(EXECUTION_DRIVER_NAME, target_pid, signal)
6700                .map_err(kernel_error)?;
6701            return Ok(Value::Null);
6702        }
6703
6704        let target_kernel_pid = u32::try_from(target_pid).map_err(|_| {
6705            SidecarError::InvalidState(format!("EINVAL: invalid process pid {target_pid}"))
6706        })?;
6707        let (source_pid, located_target_path) = {
6708            let Some(root) = vm.active_processes.get(process_id) else {
6709                return Err(SidecarError::InvalidState(format!(
6710                    "ESRCH: unknown process {process_id} during process.kill",
6711                )));
6712            };
6713            let Some(source) = Self::active_process_by_path(root, &source_path) else {
6714                return Err(SidecarError::InvalidState(format!(
6715                    "ESRCH: unknown child process {child_process_id} during process.kill",
6716                )));
6717            };
6718            vm.kernel
6719                .signal_process(EXECUTION_DRIVER_NAME, target_pid, 0)
6720                .map_err(kernel_error)?;
6721            (
6722                source.kernel_pid,
6723                Self::active_process_path_by_kernel_pid(root, target_kernel_pid),
6724            )
6725        };
6726        let Some(target_path) = located_target_path else {
6727            // The target is alive but not part of this root's process tree.
6728            // Resolve it VM-wide so cross-tree pids and untracked kernel
6729            // processes still receive the signal.
6730            self.signal_vm_kernel_pid(vm_id, target_kernel_pid, signal_name)?;
6731            return Ok(Value::Null);
6732        };
6733        let Some(vm) = self.vms.get_mut(vm_id) else {
6734            return Err(SidecarError::InvalidState(String::from(
6735                "ESRCH: unknown VM during process.kill",
6736            )));
6737        };
6738
6739        if source_pid == target_kernel_pid {
6740            let Some(root) = vm.active_processes.get_mut(process_id) else {
6741                return Ok(Value::Null);
6742            };
6743            let Some(source) = Self::active_process_by_path_mut(root, &source_path) else {
6744                return Ok(Value::Null);
6745            };
6746            source.pending_self_signal_exit = None;
6747            if !matches!(
6748                canonical_signal_name(signal),
6749                Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
6750            ) {
6751                source.pending_self_signal_exit = Some(signal);
6752            }
6753            return Ok(json!({
6754                "self": true,
6755                "action": "default",
6756            }));
6757        }
6758
6759        let signal_key = target_path.last().map(String::as_str).unwrap_or(process_id);
6760        let registration = vm
6761            .signal_states
6762            .get(signal_key)
6763            .and_then(|handlers| handlers.get(&(signal as u32)))
6764            .cloned();
6765
6766        let action = match registration
6767            .as_ref()
6768            .map(|registration| &registration.action)
6769        {
6770            Some(SignalDispositionAction::Ignore) => "ignore",
6771            Some(SignalDispositionAction::User) => {
6772                let Some(root) = vm.active_processes.get_mut(process_id) else {
6773                    return Ok(Value::Null);
6774                };
6775                let Some(target) = Self::active_process_by_owned_path_mut(root, &target_path)
6776                else {
6777                    return Err(SidecarError::InvalidState(format!(
6778                        "ESRCH: unknown process pid {target_pid}"
6779                    )));
6780                };
6781                if let Some(session) = target.execution.javascript_v8_session_handle().filter(
6782                    |_| matches!(&target.execution, ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime())
6783                        || matches!(&target.execution, ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime()),
6784                ) {
6785                    dispatch_v8_session_signal_async(session, signal);
6786                } else if !dispatch_v8_process_signal(target, signal)? {
6787                    return Err(SidecarError::InvalidState(format!(
6788                        "unsupported guest signal delivery for pid {target_pid}"
6789                    )));
6790                }
6791                "user"
6792            }
6793            Some(SignalDispositionAction::Default) | None
6794                if matches!(
6795                    canonical_signal_name(signal),
6796                    Some("SIGWINCH" | "SIGCHLD" | "SIGURG")
6797                ) =>
6798            {
6799                "ignore"
6800            }
6801            Some(SignalDispositionAction::Default) | None => {
6802                let Some(root) = vm.active_processes.get_mut(process_id) else {
6803                    return Ok(Value::Null);
6804                };
6805                let Some(target) = Self::active_process_by_owned_path_mut(root, &target_path)
6806                else {
6807                    return Err(SidecarError::InvalidState(format!(
6808                        "ESRCH: unknown process pid {target_pid}"
6809                    )));
6810                };
6811                apply_active_process_default_signal(&mut vm.kernel, target, signal)?;
6812                "default"
6813            }
6814        };
6815
6816        let target_path_label = Self::child_process_path_label(
6817            process_id,
6818            &target_path.iter().map(String::as_str).collect::<Vec<_>>(),
6819        );
6820        emit_security_audit_event(
6821            &self.bridge,
6822            vm_id,
6823            "security.process.kill",
6824            audit_fields([
6825                (String::from("source"), String::from("guest_process")),
6826                (String::from("source_pid"), source_pid.to_string()),
6827                (String::from("target_pid"), target_pid.to_string()),
6828                (String::from("process_id"), process_id.to_owned()),
6829                (
6830                    String::from("target_process_path"),
6831                    target_path_label.clone(),
6832                ),
6833                (String::from("signal"), signal_name.to_owned()),
6834            ]),
6835        );
6836
6837        Ok(json!({
6838            "self": false,
6839            "action": action,
6840            "signal": signal_name,
6841            "number": signal,
6842            "targetProcessPath": target_path_label,
6843        }))
6844    }
6845
6846    pub(crate) fn poll_javascript_child_process(
6847        &mut self,
6848        vm_id: &str,
6849        process_id: &str,
6850        child_process_id: &str,
6851        wait_ms: u64,
6852    ) -> Result<Value, SidecarError> {
6853        self.poll_descendant_javascript_child_process(
6854            vm_id,
6855            process_id,
6856            &[],
6857            child_process_id,
6858            wait_ms,
6859        )
6860    }
6861
6862    pub(crate) fn write_javascript_child_process_stdin(
6863        &mut self,
6864        vm_id: &str,
6865        process_id: &str,
6866        child_process_id: &str,
6867        chunk: &[u8],
6868    ) -> Result<(), SidecarError> {
6869        let Some(vm) = self.vms.get_mut(vm_id) else {
6870            return Err(javascript_child_process_gone_error(
6871                process_id,
6872                &[child_process_id],
6873            ));
6874        };
6875        let Some(child) = vm
6876            .active_processes
6877            .get_mut(process_id)
6878            .ok_or_else(|| missing_process_error(vm_id, process_id))?
6879            .child_processes
6880            .get_mut(child_process_id)
6881        else {
6882            return Err(javascript_child_process_gone_error(
6883                process_id,
6884                &[child_process_id],
6885            ));
6886        };
6887        if let Err(error) = child.execution.write_stdin(chunk) {
6888            if is_broken_pipe_error(&error) {
6889                return Ok(());
6890            }
6891            return Err(error);
6892        }
6893        write_kernel_process_stdin(&mut vm.kernel, child, chunk)
6894    }
6895
6896    pub(crate) fn close_javascript_child_process_stdin(
6897        &mut self,
6898        vm_id: &str,
6899        process_id: &str,
6900        child_process_id: &str,
6901    ) -> Result<(), SidecarError> {
6902        let Some(vm) = self.vms.get_mut(vm_id) else {
6903            return Err(javascript_child_process_gone_error(
6904                process_id,
6905                &[child_process_id],
6906            ));
6907        };
6908        let Some(child) = vm
6909            .active_processes
6910            .get_mut(process_id)
6911            .ok_or_else(|| missing_process_error(vm_id, process_id))?
6912            .child_processes
6913            .get_mut(child_process_id)
6914        else {
6915            return Err(javascript_child_process_gone_error(
6916                process_id,
6917                &[child_process_id],
6918            ));
6919        };
6920        child.execution.close_stdin()?;
6921        close_kernel_process_stdin(&mut vm.kernel, child)
6922    }
6923
6924    pub(crate) fn kill_javascript_child_process(
6925        &mut self,
6926        vm_id: &str,
6927        process_id: &str,
6928        child_process_id: &str,
6929        signal: &str,
6930    ) -> Result<(), SidecarError> {
6931        let signal_name = signal.to_owned();
6932        let signal = parse_signal(signal)?;
6933        let Some(vm) = self.vms.get_mut(vm_id) else {
6934            return Ok(());
6935        };
6936        let process = vm
6937            .active_processes
6938            .get_mut(process_id)
6939            .ok_or_else(|| missing_process_error(vm_id, process_id))?;
6940        let source_pid = process.kernel_pid;
6941        let child = process
6942            .child_processes
6943            .get_mut(child_process_id)
6944            .ok_or_else(|| {
6945                SidecarError::InvalidState(format!(
6946                    "unknown child process {child_process_id} during kill"
6947                ))
6948            })?;
6949        terminate_tracked_child_process_for_signal(&mut vm.kernel, child, signal)?;
6950        emit_security_audit_event(
6951            &self.bridge,
6952            vm_id,
6953            "security.process.kill",
6954            audit_fields([
6955                (String::from("source"), String::from("guest_child_process")),
6956                (String::from("source_pid"), source_pid.to_string()),
6957                (String::from("target_pid"), child.kernel_pid.to_string()),
6958                (String::from("process_id"), process_id.to_owned()),
6959                (
6960                    String::from("child_process_id"),
6961                    child_process_id.to_owned(),
6962                ),
6963                (String::from("signal"), signal_name),
6964            ]),
6965        );
6966        Ok(())
6967    }
6968
6969    /// Delivers a signal to one kernel pid inside a VM, resolving the target
6970    /// through the active-process tree first so tracked sidecar executions get
6971    /// the same termination handling as a direct `child_process.kill`.
6972    /// Untracked kernel processes (for example WASM subprocess trees) receive
6973    /// the signal through the kernel process table directly.
6974    pub(crate) fn signal_vm_kernel_pid(
6975        &mut self,
6976        vm_id: &str,
6977        target_kernel_pid: u32,
6978        signal_name: &str,
6979    ) -> Result<(), SidecarError> {
6980        let signal = parse_signal(signal_name)?;
6981        let located = {
6982            let Some(vm) = self.vms.get(vm_id) else {
6983                return Err(SidecarError::InvalidState(String::from(
6984                    "ESRCH: unknown VM during process.kill",
6985                )));
6986            };
6987            let alive = vm
6988                .kernel
6989                .list_processes()
6990                .get(&target_kernel_pid)
6991                .is_some_and(|info| info.status != ProcessStatus::Exited);
6992            if !alive {
6993                return Err(SidecarError::InvalidState(format!(
6994                    "ESRCH: no such process {target_kernel_pid}"
6995                )));
6996            }
6997            vm.active_processes.iter().find_map(|(process_id, root)| {
6998                Self::active_process_path_by_kernel_pid(root, target_kernel_pid)
6999                    .map(|path| (process_id.clone(), path))
7000            })
7001        };
7002
7003        match located {
7004            Some((process_id, path)) if path.is_empty() => {
7005                self.kill_process_internal(vm_id, &process_id, signal_name)
7006            }
7007            Some((process_id, path)) => {
7008                let Some(vm) = self.vms.get_mut(vm_id) else {
7009                    return Ok(());
7010                };
7011                let Some(root) = vm.active_processes.get_mut(&process_id) else {
7012                    return Ok(());
7013                };
7014                let Some(target) = Self::active_process_by_owned_path_mut(root, &path) else {
7015                    return Err(SidecarError::InvalidState(format!(
7016                        "ESRCH: no such process {target_kernel_pid}"
7017                    )));
7018                };
7019                terminate_tracked_child_process_for_signal(&mut vm.kernel, target, signal)?;
7020                emit_security_audit_event(
7021                    &self.bridge,
7022                    vm_id,
7023                    "security.process.kill",
7024                    audit_fields([
7025                        (String::from("source"), String::from("guest_process")),
7026                        (String::from("target_pid"), target_kernel_pid.to_string()),
7027                        (String::from("process_id"), process_id),
7028                        (String::from("signal"), signal_name.to_owned()),
7029                    ]),
7030                );
7031                Ok(())
7032            }
7033            None => {
7034                let Some(vm) = self.vms.get_mut(vm_id) else {
7035                    return Ok(());
7036                };
7037                let target_pid = i32::try_from(target_kernel_pid).map_err(|_| {
7038                    SidecarError::InvalidState(format!(
7039                        "EINVAL: invalid process pid {target_kernel_pid}"
7040                    ))
7041                })?;
7042                vm.kernel
7043                    .signal_process(EXECUTION_DRIVER_NAME, target_pid, signal)
7044                    .map_err(kernel_error)?;
7045                emit_security_audit_event(
7046                    &self.bridge,
7047                    vm_id,
7048                    "security.process.kill",
7049                    audit_fields([
7050                        (String::from("source"), String::from("guest_process")),
7051                        (String::from("target_pid"), target_kernel_pid.to_string()),
7052                        (String::from("signal"), signal_name.to_owned()),
7053                    ]),
7054                );
7055                Ok(())
7056            }
7057        }
7058    }
7059
7060    /// Delivers a signal to every live member of a VM process group, matching
7061    /// Linux `kill(-pgid, sig)` semantics. Returns whether the caller itself
7062    /// is a member of the group so entry points can apply self-signal
7063    /// delivery; the caller is intentionally skipped here.
7064    pub(crate) fn signal_vm_process_group(
7065        &mut self,
7066        vm_id: &str,
7067        caller_kernel_pid: u32,
7068        pgid: u32,
7069        signal_name: &str,
7070    ) -> Result<bool, SidecarError> {
7071        parse_signal(signal_name)?;
7072        let members = {
7073            let Some(vm) = self.vms.get(vm_id) else {
7074                return Err(SidecarError::InvalidState(String::from(
7075                    "ESRCH: unknown VM during process.kill",
7076                )));
7077            };
7078            vm.kernel
7079                .list_processes()
7080                .into_iter()
7081                .filter(|(_, info)| info.pgid == pgid && info.status != ProcessStatus::Exited)
7082                .map(|(pid, _)| pid)
7083                .collect::<Vec<_>>()
7084        };
7085        if members.is_empty() {
7086            return Err(SidecarError::InvalidState(format!(
7087                "ESRCH: no such process group {pgid}"
7088            )));
7089        }
7090
7091        let mut caller_is_member = false;
7092        for member_pid in members {
7093            if member_pid == caller_kernel_pid {
7094                caller_is_member = true;
7095                continue;
7096            }
7097            match self.signal_vm_kernel_pid(vm_id, member_pid, signal_name) {
7098                Ok(()) => {}
7099                // Group members can exit while the group is being signaled. A
7100                // vanished member is not an error for the group kill overall.
7101                Err(error) if sidecar_error_is_esrch(&error) => {}
7102                Err(error) => return Err(error),
7103            }
7104        }
7105        Ok(caller_is_member)
7106    }
7107}
7108
7109/// Applies a kill signal to a tracked child execution. Shared-runtime
7110/// executions for lethal signals are terminated directly with a synthetic
7111/// signal exit so child polls observe a prompt close; everything else routes
7112/// through the kernel process table.
7113fn terminate_tracked_child_process_for_signal(
7114    kernel: &mut SidecarKernel,
7115    child: &mut ActiveProcess,
7116    signal: i32,
7117) -> Result<(), SidecarError> {
7118    let should_terminate_shared_runtime = child.execution.uses_shared_v8_runtime()
7119        && signal != 0
7120        && !matches!(
7121            signal,
7122            libc::SIGHUP
7123                | libc::SIGINT
7124                | libc::SIGTERM
7125                | libc::SIGCHLD
7126                | libc::SIGWINCH
7127                | libc::SIGSTOP
7128                | libc::SIGCONT
7129        );
7130    if should_terminate_shared_runtime {
7131        child.execution.terminate()?;
7132        child.pending_self_signal_exit = Some(signal);
7133        child.queue_pending_execution_event(ActiveExecutionEvent::Exited(128 + signal))?;
7134    } else {
7135        kernel
7136            .kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, signal)
7137            .map_err(kernel_error)?;
7138    }
7139    Ok(())
7140}
7141
7142fn sidecar_error_is_esrch(error: &SidecarError) -> bool {
7143    error.to_string().contains("ESRCH")
7144}
7145
7146fn apply_active_process_default_signal(
7147    kernel: &mut SidecarKernel,
7148    process: &mut ActiveProcess,
7149    signal: i32,
7150) -> Result<(), SidecarError> {
7151    if matches!(signal, libc::SIGSTOP | libc::SIGCONT) {
7152        return kernel
7153            .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, signal)
7154            .map_err(kernel_error);
7155    }
7156
7157    if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
7158        close_kernel_process_stdin(kernel, process)?;
7159    }
7160
7161    if process.execution.uses_shared_v8_runtime() {
7162        process.execution.terminate()?;
7163        if signal != 0 && matches!(process.execution, ActiveExecution::Wasm(_)) {
7164            process.queue_pending_execution_event(ActiveExecutionEvent::Exited(128 + signal))?;
7165        }
7166        return Ok(());
7167    }
7168
7169    kernel
7170        .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, signal)
7171        .map_err(kernel_error)
7172}
7173
7174fn map_wasm_signal_registration(
7175    registration: secure_exec_execution::wasm::WasmSignalHandlerRegistration,
7176) -> SignalHandlerRegistration {
7177    SignalHandlerRegistration {
7178        action: match registration.action {
7179            secure_exec_execution::wasm::WasmSignalDispositionAction::Default => {
7180                crate::protocol::SignalDispositionAction::Default
7181            }
7182            secure_exec_execution::wasm::WasmSignalDispositionAction::Ignore => {
7183                crate::protocol::SignalDispositionAction::Ignore
7184            }
7185            secure_exec_execution::wasm::WasmSignalDispositionAction::User => {
7186                crate::protocol::SignalDispositionAction::User
7187            }
7188        },
7189        mask: registration.mask,
7190        flags: registration.flags,
7191    }
7192}
7193
7194fn parse_process_signal_state_request(
7195    args: &[Value],
7196) -> Result<(u32, SignalHandlerRegistration), SidecarError> {
7197    let signal = javascript_sync_rpc_arg_u32(args, 0, "process.signal_state signal")?;
7198    let action = javascript_sync_rpc_arg_str(args, 1, "process.signal_state action")?;
7199    let mask_json = javascript_sync_rpc_arg_str(args, 2, "process.signal_state mask")?;
7200    let flags = javascript_sync_rpc_arg_u32(args, 3, "process.signal_state flags")?;
7201    let mask: Vec<u32> = serde_json::from_str(mask_json).map_err(|error| {
7202        SidecarError::InvalidState(format!(
7203            "process.signal_state mask must be valid JSON: {error}"
7204        ))
7205    })?;
7206    let action = match action.trim().to_ascii_lowercase().as_str() {
7207        "default" => SignalDispositionAction::Default,
7208        "ignore" => SignalDispositionAction::Ignore,
7209        "user" => SignalDispositionAction::User,
7210        other => {
7211            return Err(SidecarError::InvalidState(format!(
7212                "unsupported process.signal_state action {other}"
7213            )));
7214        }
7215    };
7216
7217    Ok((
7218        signal,
7219        SignalHandlerRegistration {
7220            action,
7221            mask,
7222            flags,
7223        },
7224    ))
7225}
7226
7227fn apply_process_signal_state_update(
7228    signal_states: &mut BTreeMap<String, BTreeMap<u32, SignalHandlerRegistration>>,
7229    process_id: &str,
7230    signal: u32,
7231    registration: SignalHandlerRegistration,
7232) {
7233    if registration.action == SignalDispositionAction::Default
7234        && registration.mask.is_empty()
7235        && registration.flags == 0
7236    {
7237        let remove_process_entry = signal_states
7238            .get_mut(process_id)
7239            .map(|handlers| {
7240                handlers.remove(&signal);
7241                handlers.is_empty()
7242            })
7243            .unwrap_or(false);
7244        if remove_process_entry {
7245            signal_states.remove(process_id);
7246        }
7247        return;
7248    }
7249
7250    signal_states
7251        .entry(process_id.to_owned())
7252        .or_default()
7253        .insert(signal, registration);
7254}
7255
7256fn map_node_signal_registration(
7257    registration: NodeSignalHandlerRegistration,
7258) -> SignalHandlerRegistration {
7259    SignalHandlerRegistration {
7260        action: match registration.action {
7261            NodeSignalDispositionAction::Default => SignalDispositionAction::Default,
7262            NodeSignalDispositionAction::Ignore => SignalDispositionAction::Ignore,
7263            NodeSignalDispositionAction::User => SignalDispositionAction::User,
7264        },
7265        mask: registration.mask,
7266        flags: registration.flags,
7267    }
7268}
7269
7270fn javascript_child_process_sync_input_bytes(
7271    value: Option<&Value>,
7272) -> Result<Option<Vec<u8>>, SidecarError> {
7273    let Some(value) = value else {
7274        return Ok(None);
7275    };
7276
7277    match value {
7278        Value::Null => Ok(None),
7279        Value::String(text) => Ok(Some(text.as_bytes().to_vec())),
7280        other => javascript_sync_rpc_bytes_arg(
7281            std::slice::from_ref(other),
7282            0,
7283            "child_process.spawn_sync input",
7284        )
7285        .map(Some),
7286    }
7287}
7288
7289// bridge_permissions moved to crate::bridge
7290
7291// reconcile_mounts, resolve_cwd moved to crate::vm
7292
7293fn resolve_execute_request(
7294    vm: &VmState,
7295    payload: &ExecuteRequest,
7296) -> Result<ResolvedChildProcessExecution, SidecarError> {
7297    let payload_env: BTreeMap<String, String> = payload
7298        .env
7299        .iter()
7300        .map(|(k, v)| (k.clone(), v.clone()))
7301        .collect();
7302    if let Some(command) = payload.command.as_deref() {
7303        return resolve_command_execution(
7304            vm,
7305            command,
7306            &payload.args,
7307            &payload_env,
7308            payload.cwd.as_deref(),
7309            payload.wasm_permission_tier,
7310        );
7311    }
7312
7313    let runtime = payload.runtime.clone().ok_or_else(|| {
7314        SidecarError::InvalidState(String::from("execute requires either command or runtime"))
7315    })?;
7316    let entrypoint = payload.entrypoint.clone().ok_or_else(|| {
7317        SidecarError::InvalidState(String::from(
7318            "execute requires either command or entrypoint",
7319        ))
7320    })?;
7321    let (guest_cwd, host_cwd, allow_host_path_overrides) =
7322        resolve_execution_cwds(vm, payload.cwd.as_deref());
7323    let mut env = vm.guest_env.clone();
7324    env.extend(payload_env.clone());
7325
7326    let requested_host_entrypoint = resolve_host_entrypoint_within_vm_host_cwd(vm, &entrypoint);
7327    if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
7328        let requested_cwd = payload.cwd.as_deref().unwrap_or(guest_cwd.as_str());
7329        return Err(SidecarError::InvalidState(format!(
7330            "execution cwd {requested_cwd} is outside sandbox root {}",
7331            vm.host_cwd.to_string_lossy()
7332        )));
7333    }
7334    let host_entrypoint_override = allow_host_path_overrides
7335        .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, &entrypoint))
7336        .flatten();
7337
7338    let guest_entrypoint = host_entrypoint_override
7339        .as_ref()
7340        .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
7341        .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, &entrypoint));
7342    prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
7343
7344    Ok(ResolvedChildProcessExecution {
7345        command: match runtime {
7346            GuestRuntimeKind::JavaScript => String::from(JAVASCRIPT_COMMAND),
7347            GuestRuntimeKind::Python => String::from(PYTHON_COMMAND),
7348            GuestRuntimeKind::WebAssembly => String::from(WASM_COMMAND),
7349        },
7350        process_args: std::iter::once(entrypoint.clone())
7351            .chain(payload.args.iter().cloned())
7352            .collect(),
7353        runtime,
7354        entrypoint: host_entrypoint_override
7355            .map(|(_, host_entrypoint)| host_entrypoint)
7356            .unwrap_or(entrypoint),
7357        execution_args: payload.args.clone(),
7358        env,
7359        guest_cwd,
7360        host_cwd,
7361        wasm_permission_tier: payload.wasm_permission_tier,
7362        tool_command: false,
7363    })
7364}
7365
7366fn resolve_command_execution(
7367    vm: &VmState,
7368    command: &str,
7369    args: &[String],
7370    extra_env: &BTreeMap<String, String>,
7371    cwd: Option<&str>,
7372    explicit_wasm_permission_tier: Option<WasmPermissionTier>,
7373) -> Result<ResolvedChildProcessExecution, SidecarError> {
7374    let (guest_cwd, host_cwd, allow_host_path_overrides) = resolve_execution_cwds(vm, cwd);
7375    let mut env = vm.guest_env.clone();
7376    env.extend(extra_env.clone());
7377    let args = apply_shell_cwd_prefix(command, args.to_vec(), &guest_cwd);
7378
7379    if is_tool_command(vm, command) {
7380        let command = normalized_tool_command_name(command).unwrap_or_else(|| command.to_owned());
7381        return Ok(ResolvedChildProcessExecution {
7382            command: command.clone(),
7383            process_args: std::iter::once(command.clone())
7384                .chain(args.iter().cloned())
7385                .collect(),
7386            runtime: GuestRuntimeKind::JavaScript,
7387            entrypoint: command,
7388            execution_args: args,
7389            env,
7390            guest_cwd,
7391            host_cwd,
7392            wasm_permission_tier: None,
7393            tool_command: true,
7394        });
7395    }
7396
7397    if is_node_runtime_command(command) {
7398        if let Some(cli) = resolve_host_node_cli_entrypoint(command) {
7399            env.insert(
7400                String::from("AGENT_OS_NODE_EVAL"),
7401                build_host_node_cli_eval(&cli),
7402            );
7403            prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
7404            add_runtime_guest_path_mapping(&mut env, &cli.guest_root, &cli.package_root);
7405            add_runtime_host_access_path(
7406                &mut env,
7407                "AGENT_OS_EXTRA_FS_READ_PATHS",
7408                &cli.package_root,
7409                true,
7410            );
7411
7412            return Ok(ResolvedChildProcessExecution {
7413                command: String::from(JAVASCRIPT_COMMAND),
7414                process_args: std::iter::once(command.to_owned())
7415                    .chain(args.iter().cloned())
7416                    .collect(),
7417                runtime: GuestRuntimeKind::JavaScript,
7418                entrypoint: String::from("-e"),
7419                execution_args: std::iter::once(cli.guest_entrypoint.clone())
7420                    .chain(args.iter().cloned())
7421                    .collect(),
7422                env,
7423                guest_cwd,
7424                host_cwd,
7425                wasm_permission_tier: None,
7426                tool_command: false,
7427            });
7428        }
7429
7430        if args.is_empty() {
7431            env.insert(String::from("AGENT_OS_NODE_EVAL"), String::new());
7432            prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
7433
7434            return Ok(ResolvedChildProcessExecution {
7435                command: String::from(JAVASCRIPT_COMMAND),
7436                process_args: vec![command.to_owned()],
7437                runtime: GuestRuntimeKind::JavaScript,
7438                entrypoint: String::from("-e"),
7439                execution_args: Vec::new(),
7440                env,
7441                guest_cwd,
7442                host_cwd,
7443                wasm_permission_tier: None,
7444                tool_command: false,
7445            });
7446        }
7447
7448        if let Some((entrypoint, execution_args)) =
7449            resolve_special_node_cli_invocation(&args, &mut env)
7450        {
7451            prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
7452
7453            return Ok(ResolvedChildProcessExecution {
7454                command: String::from(JAVASCRIPT_COMMAND),
7455                process_args: std::iter::once(command.to_owned())
7456                    .chain(args.iter().cloned())
7457                    .collect(),
7458                runtime: GuestRuntimeKind::JavaScript,
7459                entrypoint,
7460                execution_args,
7461                env,
7462                guest_cwd,
7463                host_cwd,
7464                wasm_permission_tier: None,
7465                tool_command: false,
7466            });
7467        }
7468
7469        let Some(entrypoint_specifier) = args.first() else {
7470            return Err(SidecarError::InvalidState(format!(
7471                "{command} execution requires an entrypoint"
7472            )));
7473        };
7474
7475        let (entrypoint, execution_args, guest_entrypoint) = {
7476            let requested_host_entrypoint =
7477                resolve_host_entrypoint_within_vm_host_cwd(vm, entrypoint_specifier);
7478            if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
7479                let requested_cwd = cwd.unwrap_or(guest_cwd.as_str());
7480                return Err(SidecarError::InvalidState(format!(
7481                    "execution cwd {requested_cwd} is outside sandbox root {}",
7482                    vm.host_cwd.to_string_lossy()
7483                )));
7484            }
7485            let host_entrypoint_override = allow_host_path_overrides
7486                .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, entrypoint_specifier))
7487                .flatten();
7488            let guest_entrypoint = host_entrypoint_override
7489                .as_ref()
7490                .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
7491                .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, entrypoint_specifier));
7492            let entrypoint = host_entrypoint_override.map_or_else(
7493                || {
7494                    guest_entrypoint.as_ref().map_or_else(
7495                        || entrypoint_specifier.clone(),
7496                        |guest_entrypoint| {
7497                            resolve_vm_guest_path_to_host(vm, guest_entrypoint)
7498                                .to_string_lossy()
7499                                .into_owned()
7500                        },
7501                    )
7502                },
7503                |(_, host_entrypoint)| host_entrypoint,
7504            );
7505            (
7506                entrypoint,
7507                args.iter().skip(1).cloned().collect(),
7508                guest_entrypoint,
7509            )
7510        };
7511
7512        prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
7513
7514        return Ok(ResolvedChildProcessExecution {
7515            command: String::from(JAVASCRIPT_COMMAND),
7516            process_args: std::iter::once(command.to_owned())
7517                .chain(args.iter().cloned())
7518                .collect(),
7519            runtime: GuestRuntimeKind::JavaScript,
7520            entrypoint,
7521            execution_args,
7522            env,
7523            guest_cwd,
7524            host_cwd,
7525            wasm_permission_tier: None,
7526            tool_command: false,
7527        });
7528    }
7529
7530    if command.ends_with(".js") || command.ends_with(".mjs") || command.ends_with(".cjs") {
7531        let requested_host_entrypoint = resolve_host_entrypoint_within_vm_host_cwd(vm, command);
7532        if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
7533            let requested_cwd = cwd.unwrap_or(guest_cwd.as_str());
7534            return Err(SidecarError::InvalidState(format!(
7535                "execution cwd {requested_cwd} is outside sandbox root {}",
7536                vm.host_cwd.to_string_lossy()
7537            )));
7538        }
7539        let host_entrypoint_override = allow_host_path_overrides
7540            .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, command))
7541            .flatten();
7542        let guest_entrypoint = host_entrypoint_override
7543            .as_ref()
7544            .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
7545            .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, command));
7546        let entrypoint = host_entrypoint_override.map_or_else(
7547            || {
7548                guest_entrypoint.as_ref().map_or_else(
7549                    || command.to_owned(),
7550                    |guest_entrypoint| {
7551                        resolve_vm_guest_path_to_host(vm, guest_entrypoint)
7552                            .to_string_lossy()
7553                            .into_owned()
7554                    },
7555                )
7556            },
7557            |(_, host_entrypoint)| host_entrypoint,
7558        );
7559        prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
7560
7561        return Ok(ResolvedChildProcessExecution {
7562            command: String::from(JAVASCRIPT_COMMAND),
7563            process_args: std::iter::once(command.to_owned())
7564                .chain(args.iter().cloned())
7565                .collect(),
7566            runtime: GuestRuntimeKind::JavaScript,
7567            entrypoint,
7568            execution_args: args.to_vec(),
7569            env,
7570            guest_cwd,
7571            host_cwd,
7572            wasm_permission_tier: None,
7573            tool_command: false,
7574        });
7575    }
7576
7577    let guest_entrypoint = resolve_guest_command_entrypoint(
7578        vm,
7579        &guest_cwd,
7580        command,
7581        env.get("PATH").map(String::as_str),
7582    )
7583    .ok_or_else(|| {
7584        SidecarError::InvalidState(format!(
7585            "command not found on native sidecar path: {command}"
7586        ))
7587    })?;
7588    let wasm_permission_tier = explicit_wasm_permission_tier
7589        .or_else(|| vm.command_permissions.get(command).copied())
7590        .or_else(|| {
7591            Path::new(&guest_entrypoint)
7592                .file_name()
7593                .and_then(|name| name.to_str())
7594                .and_then(|name| vm.command_permissions.get(name).copied())
7595        });
7596
7597    let host_entrypoint = resolve_vm_guest_path_to_host(vm, &guest_entrypoint);
7598    if let Some((javascript_guest_entrypoint, javascript_host_entrypoint)) =
7599        resolve_javascript_command_entrypoint(vm, &guest_entrypoint, &host_entrypoint)
7600    {
7601        prepare_guest_runtime_env(
7602            vm,
7603            &mut env,
7604            &guest_cwd,
7605            &host_cwd,
7606            Some(javascript_guest_entrypoint),
7607        )?;
7608
7609        return Ok(ResolvedChildProcessExecution {
7610            command: command.to_owned(),
7611            process_args: std::iter::once(command.to_owned())
7612                .chain(args.iter().cloned())
7613                .collect(),
7614            runtime: GuestRuntimeKind::JavaScript,
7615            entrypoint: javascript_host_entrypoint.to_string_lossy().into_owned(),
7616            execution_args: args.to_vec(),
7617            env,
7618            guest_cwd,
7619            host_cwd,
7620            wasm_permission_tier: None,
7621            tool_command: false,
7622        });
7623    }
7624    prepare_guest_runtime_env(
7625        vm,
7626        &mut env,
7627        &guest_cwd,
7628        &host_cwd,
7629        Some(guest_entrypoint.clone()),
7630    )?;
7631
7632    Ok(ResolvedChildProcessExecution {
7633        command: command.to_owned(),
7634        process_args: std::iter::once(command.to_owned())
7635            .chain(args.iter().cloned())
7636            .collect(),
7637        runtime: GuestRuntimeKind::WebAssembly,
7638        entrypoint: host_entrypoint.to_string_lossy().into_owned(),
7639        execution_args: args.to_vec(),
7640        env,
7641        guest_cwd,
7642        host_cwd,
7643        wasm_permission_tier,
7644        tool_command: false,
7645    })
7646}
7647
7648const MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH: usize = 4;
7649
7650fn resolve_javascript_command_entrypoint(
7651    vm: &VmState,
7652    guest_entrypoint: &str,
7653    host_entrypoint: &Path,
7654) -> Option<(String, PathBuf)> {
7655    resolve_javascript_command_entrypoint_inner(
7656        vm,
7657        guest_entrypoint,
7658        host_entrypoint,
7659        MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH,
7660    )
7661}
7662
7663fn resolve_javascript_command_entrypoint_inner(
7664    vm: &VmState,
7665    guest_entrypoint: &str,
7666    host_entrypoint: &Path,
7667    redirects_remaining: usize,
7668) -> Option<(String, PathBuf)> {
7669    if redirects_remaining > 0 {
7670        let symlink_target = fs::symlink_metadata(host_entrypoint)
7671            .ok()
7672            .filter(|metadata| metadata.file_type().is_symlink())
7673            .and_then(|_| fs::read_link(host_entrypoint).ok());
7674        if let Some(symlink_target) = symlink_target {
7675            let guest_parent = Path::new(guest_entrypoint)
7676                .parent()
7677                .and_then(|path| path.to_str())
7678                .unwrap_or("/");
7679            let symlink_guest_entrypoint = if symlink_target.is_absolute() {
7680                normalize_path(&symlink_target.to_string_lossy())
7681            } else {
7682                normalize_path(&format!(
7683                    "{guest_parent}/{}",
7684                    symlink_target.to_string_lossy().replace('\\', "/")
7685                ))
7686            };
7687            let symlink_host_entrypoint =
7688                resolve_vm_guest_path_to_host(vm, &symlink_guest_entrypoint);
7689            return resolve_javascript_command_entrypoint_inner(
7690                vm,
7691                &symlink_guest_entrypoint,
7692                &symlink_host_entrypoint,
7693                redirects_remaining - 1,
7694            );
7695        }
7696    }
7697
7698    let script = load_executable_script_preview(host_entrypoint)?;
7699    let interpreter = parse_script_interpreter_name(&script);
7700
7701    if interpreter.is_none() && is_probable_javascript_entrypoint(host_entrypoint, &script) {
7702        return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf()));
7703    }
7704
7705    let interpreter = interpreter?;
7706    if interpreter == "node" {
7707        return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf()));
7708    }
7709
7710    if redirects_remaining == 0 || !matches!(interpreter.as_str(), "sh" | "bash" | "dash") {
7711        return None;
7712    }
7713
7714    let shim_target = parse_node_shell_shim_target(&script)?;
7715    let guest_parent = Path::new(guest_entrypoint)
7716        .parent()
7717        .and_then(|path| path.to_str())
7718        .unwrap_or("/");
7719    let shim_guest_entrypoint = normalize_path(&format!("{guest_parent}/{shim_target}"));
7720    let shim_host_entrypoint = resolve_vm_guest_path_to_host(vm, &shim_guest_entrypoint);
7721    resolve_javascript_command_entrypoint_inner(
7722        vm,
7723        &shim_guest_entrypoint,
7724        &shim_host_entrypoint,
7725        redirects_remaining - 1,
7726    )
7727}
7728
7729fn load_executable_script_preview(path: &Path) -> Option<String> {
7730    let bytes = fs::read(path).ok()?;
7731    let preview_len = bytes.len().min(16 * 1024);
7732    Some(String::from_utf8_lossy(&bytes[..preview_len]).into_owned())
7733}
7734
7735fn parse_script_interpreter_name(script: &str) -> Option<String> {
7736    let shebang = script.lines().next()?.strip_prefix("#!")?.trim();
7737    let mut tokens = shebang.split_whitespace();
7738    let command = tokens.next()?;
7739    let command_name = Path::new(command).file_name()?.to_str()?;
7740    if command_name == "env" {
7741        for token in tokens {
7742            if token.starts_with('-') {
7743                continue;
7744            }
7745            return Path::new(token)
7746                .file_name()
7747                .and_then(|name| name.to_str())
7748                .map(ToOwned::to_owned);
7749        }
7750        return None;
7751    }
7752
7753    Some(command_name.to_owned())
7754}
7755
7756fn parse_node_shell_shim_target(script: &str) -> Option<String> {
7757    for line in script.lines() {
7758        let trimmed = line.trim();
7759        if !trimmed.starts_with("exec ") {
7760            continue;
7761        }
7762
7763        let mut remaining = trimmed;
7764        while let Some(start) = remaining.find("\"$basedir/") {
7765            let after_prefix = &remaining[start + "\"$basedir/".len()..];
7766            let end = after_prefix.find('"')?;
7767            let candidate = &after_prefix[..end];
7768            remaining = &after_prefix[end + 1..];
7769
7770            if candidate.is_empty() || candidate == "node" || candidate.ends_with("/node") {
7771                continue;
7772            }
7773
7774            return Some(candidate.to_owned());
7775        }
7776    }
7777
7778    None
7779}
7780
7781fn is_probable_javascript_entrypoint(path: &Path, script: &str) -> bool {
7782    let extension = path
7783        .extension()
7784        .and_then(|value| value.to_str())
7785        .unwrap_or_default();
7786    if matches!(extension, "js" | "cjs" | "mjs") {
7787        return true;
7788    }
7789
7790    if !path
7791        .components()
7792        .any(|component| component.as_os_str() == "node_modules")
7793    {
7794        return false;
7795    }
7796
7797    let preview = script.trim_start_matches('\u{feff}').trim_start();
7798    !preview.is_empty()
7799        && !preview.starts_with("#!")
7800        && (preview.starts_with("\"use strict\"")
7801            || preview.starts_with("'use strict'")
7802            || preview.starts_with("import ")
7803            || preview.starts_with("export ")
7804            || preview.starts_with("const ")
7805            || preview.starts_with("let ")
7806            || preview.starts_with("var ")
7807            || preview.starts_with("Object.defineProperty(exports")
7808            || preview.starts_with("module.exports")
7809            || preview.starts_with("require("))
7810}
7811
7812fn resolve_guest_execution_cwd(vm: &VmState, value: Option<&str>) -> String {
7813    value
7814        .map(normalize_path)
7815        .unwrap_or_else(|| vm.guest_cwd.clone())
7816}
7817
7818fn resolve_execution_cwds(vm: &VmState, value: Option<&str>) -> (String, PathBuf, bool) {
7819    if let Some(raw_cwd) = value {
7820        let normalized_vm_host_cwd = normalize_host_path(&vm.host_cwd);
7821        let requested_host_cwd = normalize_host_path(Path::new(raw_cwd));
7822        if path_is_within_root(&requested_host_cwd, &normalized_vm_host_cwd) {
7823            let relative = requested_host_cwd
7824                .strip_prefix(&normalized_vm_host_cwd)
7825                .unwrap_or_else(|_| Path::new(""));
7826            let relative = relative.to_string_lossy().replace('\\', "/");
7827            let guest_cwd = if relative.is_empty() {
7828                String::from("/")
7829            } else {
7830                normalize_path(&format!("/{relative}"))
7831            };
7832            return (guest_cwd, requested_host_cwd, true);
7833        }
7834    }
7835
7836    let guest_cwd = resolve_guest_execution_cwd(vm, value);
7837    let host_cwd = if value.is_none() {
7838        vm.host_cwd.clone()
7839    } else {
7840        resolve_vm_guest_path_to_host(vm, &guest_cwd)
7841    };
7842    (guest_cwd, host_cwd, value.is_none())
7843}
7844
7845fn resolve_vm_guest_path_to_host(vm: &VmState, guest_path: &str) -> PathBuf {
7846    host_mount_path_for_guest_path(vm, guest_path)
7847        .unwrap_or_else(|| shadow_path_for_guest(vm, guest_path))
7848}
7849
7850fn shadow_path_for_guest(vm: &VmState, guest_path: &str) -> PathBuf {
7851    let normalized = normalize_path(guest_path);
7852    let relative = normalized.trim_start_matches('/');
7853    if relative.is_empty() {
7854        return vm.cwd.clone();
7855    }
7856    vm.cwd.join(relative)
7857}
7858
7859fn apply_shell_cwd_prefix(command: &str, mut args: Vec<String>, guest_cwd: &str) -> Vec<String> {
7860    if guest_cwd == "/" || !is_shell_command(command) {
7861        return args;
7862    }
7863
7864    let Some(flag) = args.first() else {
7865        return args;
7866    };
7867    if !matches!(flag.as_str(), "-c" | "-lc") || args.len() < 2 {
7868        return args;
7869    }
7870
7871    let command_text = args[1].clone();
7872    let quoted_cwd = shell_single_quote(guest_cwd);
7873    args[1] = format!("cd {quoted_cwd} && {command_text}");
7874    args
7875}
7876
7877fn is_shell_command(command: &str) -> bool {
7878    Path::new(command)
7879        .file_name()
7880        .and_then(|name| name.to_str())
7881        .unwrap_or(command)
7882        .trim_end_matches(".exe")
7883        .eq("sh")
7884        || Path::new(command)
7885            .file_name()
7886            .and_then(|name| name.to_str())
7887            .unwrap_or(command)
7888            .trim_end_matches(".exe")
7889            .eq("bash")
7890}
7891
7892fn shell_single_quote(value: &str) -> String {
7893    if value.is_empty() {
7894        return String::from("''");
7895    }
7896    format!("'{}'", value.replace('\'', "'\"'\"'"))
7897}
7898
7899pub(crate) fn sync_active_process_host_writes_to_kernel(
7900    vm: &mut VmState,
7901) -> Result<(), SidecarError> {
7902    if vm.root_filesystem_mode != RootFilesystemMode::ReadOnly {
7903        let shadow_root = vm.cwd.clone();
7904        sync_host_directory_tree_to_kernel(vm, &shadow_root, "/")?;
7905    }
7906
7907    let normalized_vm_root = normalize_host_path(&vm.cwd);
7908    let extra_roots = collect_active_process_host_sync_roots(vm, &normalized_vm_root);
7909    for (host_cwd, guest_cwd) in extra_roots {
7910        sync_host_directory_tree_to_kernel(vm, &host_cwd, &guest_cwd)?;
7911    }
7912
7913    Ok(())
7914}
7915
7916fn collect_active_process_host_sync_roots(
7917    vm: &VmState,
7918    normalized_vm_root: &Path,
7919) -> Vec<(PathBuf, String)> {
7920    let mut roots = Vec::new();
7921    let mut seen = BTreeSet::new();
7922
7923    for process in vm.active_processes.values() {
7924        collect_process_host_sync_roots(process, normalized_vm_root, &mut seen, &mut roots);
7925    }
7926
7927    roots
7928}
7929
7930fn collect_process_host_sync_roots(
7931    process: &ActiveProcess,
7932    normalized_vm_root: &Path,
7933    seen: &mut BTreeSet<(PathBuf, String)>,
7934    roots: &mut Vec<(PathBuf, String)>,
7935) {
7936    let normalized_host_cwd = normalize_host_path(&process.host_cwd);
7937    if !path_is_within_root(&normalized_host_cwd, normalized_vm_root) {
7938        let guest_cwd = normalize_path(&process.guest_cwd);
7939        if seen.insert((normalized_host_cwd.clone(), guest_cwd.clone())) {
7940            roots.push((normalized_host_cwd, guest_cwd));
7941        }
7942    }
7943
7944    for child in process.child_processes.values() {
7945        collect_process_host_sync_roots(child, normalized_vm_root, seen, roots);
7946    }
7947}
7948
7949fn sync_process_host_writes_to_kernel(
7950    vm: &mut VmState,
7951    process: &ActiveProcess,
7952) -> Result<(), SidecarError> {
7953    if vm.root_filesystem_mode != RootFilesystemMode::ReadOnly {
7954        let shadow_root = vm.cwd.clone();
7955        sync_host_directory_tree_to_kernel(vm, &shadow_root, "/")?;
7956    }
7957
7958    if !path_is_within_root(
7959        &normalize_host_path(&process.host_cwd),
7960        &normalize_host_path(&vm.cwd),
7961    ) {
7962        sync_host_directory_tree_to_kernel(vm, &process.host_cwd, &process.guest_cwd)?;
7963    }
7964
7965    Ok(())
7966}
7967
7968fn sync_host_directory_tree_to_kernel(
7969    vm: &mut VmState,
7970    host_root: &Path,
7971    guest_root: &str,
7972) -> Result<(), SidecarError> {
7973    let normalized_host_root = normalize_host_path(host_root);
7974    let normalized_guest_root = normalize_path(guest_root);
7975    let mut synced_file_times = BTreeMap::new();
7976    sync_host_directory_tree_to_kernel_inner(
7977        vm,
7978        &normalized_host_root,
7979        &normalized_host_root,
7980        &normalized_guest_root,
7981        &mut synced_file_times,
7982    )
7983}
7984
7985fn sync_host_directory_tree_to_kernel_inner(
7986    vm: &mut VmState,
7987    host_root: &Path,
7988    current_host_dir: &Path,
7989    guest_root: &str,
7990    synced_file_times: &mut BTreeMap<(u64, u64), (u64, u64)>,
7991) -> Result<(), SidecarError> {
7992    let entries = match fs::read_dir(current_host_dir) {
7993        Ok(entries) => entries,
7994        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
7995        Err(error) => {
7996            return Err(SidecarError::Io(format!(
7997                "failed to read host shadow directory {}: {error}",
7998                current_host_dir.display()
7999            )));
8000        }
8001    };
8002
8003    for entry in entries {
8004        let entry = entry.map_err(|error| {
8005            SidecarError::Io(format!(
8006                "failed to read host shadow entry in {}: {error}",
8007                current_host_dir.display()
8008            ))
8009        })?;
8010        let host_path = entry.path();
8011        let file_type = entry.file_type().map_err(|error| {
8012            SidecarError::Io(format!(
8013                "failed to stat host shadow entry {}: {error}",
8014                host_path.display()
8015            ))
8016        })?;
8017        let relative_path = host_path
8018            .strip_prefix(host_root)
8019            .map_err(|error| {
8020                SidecarError::InvalidState(format!(
8021                    "failed to relativize host shadow path {} against {}: {error}",
8022                    host_path.display(),
8023                    host_root.display()
8024                ))
8025            })?
8026            .to_string_lossy()
8027            .replace('\\', "/");
8028        let guest_path = if guest_root == "/" {
8029            normalize_path(&format!("/{relative_path}"))
8030        } else {
8031            normalize_path(&format!(
8032                "{}/{}",
8033                guest_root.trim_end_matches('/'),
8034                relative_path
8035            ))
8036        };
8037
8038        if should_skip_shadow_sync_path(vm, &guest_path) {
8039            continue;
8040        }
8041
8042        if file_type.is_dir() {
8043            let metadata = entry.metadata().map_err(|error| {
8044                SidecarError::Io(format!(
8045                    "failed to read host shadow metadata {}: {error}",
8046                    host_path.display()
8047                ))
8048            })?;
8049            if !is_shadow_bootstrap_dir(&guest_path)
8050                && !vm.kernel.exists(&guest_path).unwrap_or(false)
8051            {
8052                vm.kernel.mkdir(&guest_path, true).map_err(|error| {
8053                    SidecarError::InvalidState(format!(
8054                        "failed to sync host shadow directory {} to guest {}: {}",
8055                        host_path.display(),
8056                        guest_path,
8057                        kernel_error(error)
8058                    ))
8059                })?;
8060                vm.kernel
8061                    .chmod(&guest_path, host_shadow_mode(&metadata))
8062                    .map_err(|error| {
8063                        SidecarError::InvalidState(format!(
8064                            "failed to sync host shadow directory mode {} to guest {}: {}",
8065                            host_path.display(),
8066                            guest_path,
8067                            kernel_error(error)
8068                        ))
8069                    })?;
8070            }
8071            sync_host_directory_tree_to_kernel_inner(
8072                vm,
8073                host_root,
8074                &host_path,
8075                guest_root,
8076                synced_file_times,
8077            )?;
8078            continue;
8079        }
8080
8081        if file_type.is_file() {
8082            let metadata = entry.metadata().map_err(|error| {
8083                SidecarError::Io(format!(
8084                    "failed to read host shadow metadata {}: {error}",
8085                    host_path.display()
8086                ))
8087            })?;
8088            let timestamp_key = (metadata.dev(), metadata.ino());
8089            let (atime_ms, mtime_ms) =
8090                *synced_file_times.entry(timestamp_key).or_insert_with(|| {
8091                    (
8092                        metadata_time_ms(metadata.atime(), metadata.atime_nsec()),
8093                        metadata_time_ms(metadata.mtime(), metadata.mtime_nsec()),
8094                    )
8095                });
8096            let desired_mode = host_shadow_mode(&metadata);
8097            let bytes = read_host_shadow_file(&host_path, desired_mode).map_err(|error| {
8098                SidecarError::Io(format!(
8099                    "failed to read host shadow file {}: {error}",
8100                    host_path.display()
8101                ))
8102            })?;
8103            vm.kernel.write_file(&guest_path, bytes).map_err(|error| {
8104                SidecarError::InvalidState(format!(
8105                    "failed to sync host shadow file {} to guest {}: {}",
8106                    host_path.display(),
8107                    guest_path,
8108                    kernel_error(error)
8109                ))
8110            })?;
8111            vm.kernel
8112                .chmod(&guest_path, desired_mode)
8113                .map_err(|error| {
8114                    SidecarError::InvalidState(format!(
8115                        "failed to sync host shadow file mode {} to guest {}: {}",
8116                        host_path.display(),
8117                        guest_path,
8118                        kernel_error(error)
8119                    ))
8120                })?;
8121            vm.kernel
8122                .utimes(&guest_path, atime_ms, mtime_ms)
8123                .map_err(|error| {
8124                    SidecarError::InvalidState(format!(
8125                        "failed to sync host shadow file times {} to guest {}: {}",
8126                        host_path.display(),
8127                        guest_path,
8128                        kernel_error(error)
8129                    ))
8130                })?;
8131            continue;
8132        }
8133
8134        if file_type.is_symlink() {
8135            let target = match fs::read_link(&host_path) {
8136                Ok(target) => target,
8137                Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
8138                Err(error) => {
8139                    return Err(SidecarError::Io(format!(
8140                        "failed to read host shadow symlink {}: {error}",
8141                        host_path.display()
8142                    )));
8143                }
8144            };
8145            replace_kernel_symlink(vm, &guest_path, &target.to_string_lossy())?;
8146        }
8147    }
8148
8149    Ok(())
8150}
8151
8152fn replace_kernel_symlink(
8153    vm: &mut VmState,
8154    guest_path: &str,
8155    target: &str,
8156) -> Result<(), SidecarError> {
8157    if vm.kernel.symlink(target, guest_path).is_ok() {
8158        return Ok(());
8159    }
8160
8161    if let Ok(existing_target) = vm.kernel.read_link(guest_path) {
8162        if existing_target == target {
8163            return Ok(());
8164        }
8165    }
8166
8167    let _ = vm.kernel.remove_file(guest_path);
8168    let _ = vm.kernel.remove_dir(guest_path);
8169    vm.kernel
8170        .symlink(target, guest_path)
8171        .map_err(kernel_error)?;
8172    Ok(())
8173}
8174
8175fn host_shadow_mode(metadata: &fs::Metadata) -> u32 {
8176    metadata.permissions().mode() & 0o7777
8177}
8178
8179/// Reads a shadow-root file back into the kernel even when guest-visible mode
8180/// bits make it unreadable for the host user. The sidecar is the kernel for
8181/// this tree, so guest permission bits (for example a 0o200 write-only file
8182/// produced by `chmod` plus a shell append redirect) must not break the
8183/// exit-time shadow sync. The original mode is restored after the read.
8184fn read_host_shadow_file(host_path: &Path, mode: u32) -> std::io::Result<Vec<u8>> {
8185    match fs::read(host_path) {
8186        Ok(bytes) => Ok(bytes),
8187        Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => {
8188            fs::set_permissions(host_path, fs::Permissions::from_mode(mode | 0o400))?;
8189            let result = fs::read(host_path);
8190            fs::set_permissions(host_path, fs::Permissions::from_mode(mode))?;
8191            result
8192        }
8193        Err(error) => Err(error),
8194    }
8195}
8196
8197fn metadata_time_ms(seconds: i64, nanos: i64) -> u64 {
8198    let seconds = seconds.max(0) as u64;
8199    let nanos = nanos.max(0) as u64;
8200    seconds
8201        .saturating_mul(1_000)
8202        .saturating_add(nanos / 1_000_000)
8203}
8204
8205fn is_shadow_bootstrap_dir(path: &str) -> bool {
8206    matches!(
8207        path,
8208        "/dev"
8209            | "/proc"
8210            | "/tmp"
8211            | "/bin"
8212            | "/lib"
8213            | "/sbin"
8214            | "/boot"
8215            | "/etc"
8216            | "/root"
8217            | "/run"
8218            | "/srv"
8219            | "/sys"
8220            | "/opt"
8221            | "/mnt"
8222            | "/media"
8223            | "/home"
8224            | "/home/user"
8225            | "/usr"
8226            | "/usr/bin"
8227            | "/usr/games"
8228            | "/usr/include"
8229            | "/usr/lib"
8230            | "/usr/libexec"
8231            | "/usr/man"
8232            | "/usr/local"
8233            | "/usr/local/bin"
8234            | "/usr/sbin"
8235            | "/usr/share"
8236            | "/usr/share/man"
8237            | "/var"
8238            | "/var/cache"
8239            | "/var/empty"
8240            | "/var/lib"
8241            | "/var/lock"
8242            | "/var/log"
8243            | "/var/run"
8244            | "/var/spool"
8245            | "/var/tmp"
8246            | "/etc/agentos"
8247    )
8248}
8249
8250#[cfg(test)]
8251mod shadow_sync_tests {
8252    use super::{is_protected_agentos_shadow_sync_path, is_shadow_bootstrap_dir};
8253
8254    #[test]
8255    fn shadow_bootstrap_sync_skips_virtual_home_tree() {
8256        assert!(is_shadow_bootstrap_dir("/home"));
8257        assert!(is_shadow_bootstrap_dir("/home/user"));
8258    }
8259
8260    #[test]
8261    fn protected_agentos_paths_are_not_shadow_synced() {
8262        assert!(is_protected_agentos_shadow_sync_path("/etc/agentos"));
8263        assert!(is_protected_agentos_shadow_sync_path(
8264            "/etc/agentos/instructions.md"
8265        ));
8266        assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos-copy"));
8267        assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos.md"));
8268    }
8269}
8270
8271fn is_kernel_owned_shadow_sync_path(path: &str) -> bool {
8272    matches!(path, "/dev" | "/proc" | "/sys")
8273        || path.starts_with("/dev/")
8274        || path.starts_with("/proc/")
8275        || path.starts_with("/sys/")
8276}
8277
8278pub(crate) fn is_protected_agentos_shadow_sync_path(path: &str) -> bool {
8279    path == "/etc/agentos" || path.starts_with("/etc/agentos/")
8280}
8281
8282fn should_skip_shadow_sync_path(vm: &VmState, guest_path: &str) -> bool {
8283    is_kernel_owned_shadow_sync_path(guest_path)
8284        || is_protected_agentos_shadow_sync_path(guest_path)
8285        || host_mount_path_for_guest_path_from_mounts(&vm.configuration.mounts, guest_path)
8286            .is_some()
8287}
8288
8289fn resolve_path_like_guest_specifier(cwd: &str, specifier: &str) -> String {
8290    if specifier.starts_with("file://") {
8291        normalize_path(specifier.trim_start_matches("file://"))
8292    } else if specifier.starts_with("file:") {
8293        normalize_path(specifier.trim_start_matches("file:"))
8294    } else if specifier.starts_with('/') {
8295        normalize_path(specifier)
8296    } else {
8297        normalize_path(&format!("{cwd}/{specifier}"))
8298    }
8299}
8300
8301fn guest_entrypoint_for_specifier(cwd: &str, specifier: &str) -> Option<String> {
8302    is_path_like_specifier(specifier).then(|| resolve_path_like_guest_specifier(cwd, specifier))
8303}
8304
8305fn is_node_runtime_command(command: &str) -> bool {
8306    matches!(command, "node" | "npm" | "npx")
8307        || Path::new(command)
8308            .file_name()
8309            .and_then(|name| name.to_str())
8310            .is_some_and(|name| matches!(name, "node" | "npm" | "npx"))
8311}
8312
8313fn resolve_special_node_cli_invocation(
8314    args: &[String],
8315    env: &mut BTreeMap<String, String>,
8316) -> Option<(String, Vec<String>)> {
8317    let first = args.first()?;
8318    match first.as_str() {
8319        "-e" | "--eval" => {
8320            env.insert(
8321                String::from("AGENT_OS_NODE_EVAL"),
8322                args.get(1).cloned().unwrap_or_default(),
8323            );
8324            Some((first.clone(), args.iter().skip(2).cloned().collect()))
8325        }
8326        "-v" | "--version" => {
8327            env.insert(
8328                String::from("AGENT_OS_NODE_EVAL"),
8329                String::from("console.log(process.version);"),
8330            );
8331            Some((String::from("-e"), args.to_vec()))
8332        }
8333        _ => None,
8334    }
8335}
8336
8337fn node_runtime_command_name(command: &str) -> Option<&str> {
8338    let name = Path::new(command)
8339        .file_name()
8340        .and_then(|name| name.to_str())?;
8341    matches!(name, "node" | "npm" | "npx").then_some(name)
8342}
8343
8344struct ResolvedHostNodeCliEntrypoint {
8345    command_name: String,
8346    guest_root: String,
8347    guest_entrypoint: String,
8348    package_root: PathBuf,
8349}
8350
8351fn resolve_host_node_cli_entrypoint(command: &str) -> Option<ResolvedHostNodeCliEntrypoint> {
8352    let command_name = node_runtime_command_name(command)?;
8353    if !matches!(command_name, "npm" | "npx") {
8354        return None;
8355    }
8356
8357    let path = std::env::var_os("PATH")?;
8358    for root in std::env::split_paths(&path) {
8359        let candidate = root.join(command_name);
8360        if !candidate.is_file() {
8361            continue;
8362        }
8363        let entrypoint = candidate.canonicalize().ok().unwrap_or(candidate);
8364        let package_root = entrypoint.parent()?.parent()?.to_path_buf();
8365        let guest_root = format!("/__secure_exec/node-runtime/{command_name}");
8366        let relative_entrypoint = entrypoint.strip_prefix(&package_root).ok()?;
8367        let guest_entrypoint = normalize_path(&format!(
8368            "{guest_root}/{}",
8369            relative_entrypoint.to_string_lossy().replace('\\', "/")
8370        ));
8371        return Some(ResolvedHostNodeCliEntrypoint {
8372            command_name: command_name.to_owned(),
8373            guest_root,
8374            guest_entrypoint,
8375            package_root,
8376        });
8377    }
8378
8379    None
8380}
8381
8382fn build_host_node_cli_eval(cli: &ResolvedHostNodeCliEntrypoint) -> String {
8383    let guest_npm_main = normalize_path(&format!("{}/lib/npm.js", cli.guest_root));
8384    let guest_npm_cli = normalize_path(&format!("{}/bin/npm-cli.js", cli.guest_root));
8385    let guest_package_json = normalize_path(&format!("{}/package.json", cli.guest_root));
8386    let guest_display_module = normalize_path(&format!("{}/lib/utils/display.js", cli.guest_root));
8387    let guest_log_file_module =
8388        normalize_path(&format!("{}/lib/utils/log-file.js", cli.guest_root));
8389    let debug_preamble = "const __agentOsDebugNpmCli = !!process.env.CODEX_DEBUG_NPM_CLI; const __agentOsDebugLog = (...args) => { if (__agentOsDebugNpmCli) { console.error('[secure-exec npm debug]', ...args); } }; const __agentOsIsProcessExitError = (error) => !!(error && typeof error === 'object' && (error._isProcessExit === true || error.name === 'ProcessExitError')); const __agentOsResolveExitCode = (code) => Number.isFinite(code) ? code : (Number.isFinite(process.exitCode) ? process.exitCode : 0); const __agentOsFinish = (code) => { process.exitCode = __agentOsResolveExitCode(code); }; if (__agentOsDebugNpmCli) { const __agentOsWrapAsyncFsMethod = (__agentOsTarget, __agentOsMethod) => { const __agentOsOriginal = __agentOsTarget[__agentOsMethod]; if (typeof __agentOsOriginal !== 'function' || __agentOsOriginal.__agentOsDebugWrapped) { return; } const __agentOsWrapped = async (...args) => { const target = args.length > 0 ? args[0] : '<none>'; __agentOsDebugLog(`fs.${__agentOsMethod}:start`, String(target)); try { const result = await __agentOsOriginal.apply(__agentOsTarget, args); __agentOsDebugLog(`fs.${__agentOsMethod}:done`, String(target)); return result; } catch (error) { __agentOsDebugLog(`fs.${__agentOsMethod}:error`, String(target), error && error.stack ? error.stack : String(error)); throw error; } }; __agentOsWrapped.__agentOsDebugWrapped = true; __agentOsTarget[__agentOsMethod] = __agentOsWrapped; }; const __agentOsWrapSyncFsMethod = (__agentOsTarget, __agentOsMethod) => { const __agentOsOriginal = __agentOsTarget[__agentOsMethod]; if (typeof __agentOsOriginal !== 'function' || __agentOsOriginal.__agentOsDebugWrapped) { return; } const __agentOsWrapped = (...args) => { const target = args.length > 0 ? args[0] : '<none>'; __agentOsDebugLog(`fs.${__agentOsMethod}:start`, String(target)); try { const result = __agentOsOriginal.apply(__agentOsTarget, args); __agentOsDebugLog(`fs.${__agentOsMethod}:done`, String(target)); return result; } catch (error) { __agentOsDebugLog(`fs.${__agentOsMethod}:error`, String(target), error && error.stack ? error.stack : String(error)); throw error; } }; __agentOsWrapped.__agentOsDebugWrapped = true; __agentOsTarget[__agentOsMethod] = __agentOsWrapped; }; const __agentOsFsPromiseModules = [require('fs/promises'), require('node:fs/promises')]; for (const __agentOsFsPromises of __agentOsFsPromiseModules) { for (const __agentOsMethod of ['access', 'lstat', 'mkdir', 'open', 'readFile', 'readdir', 'readlink', 'realpath', 'rename', 'rm', 'rmdir', 'stat', 'symlink', 'unlink', 'writeFile']) { __agentOsWrapAsyncFsMethod(__agentOsFsPromises, __agentOsMethod); } } const __agentOsFsModules = [require('fs'), require('node:fs')]; for (const __agentOsFs of __agentOsFsModules) { for (const __agentOsMethod of ['accessSync', 'existsSync', 'lstatSync', 'mkdirSync', 'openSync', 'readFileSync', 'readdirSync', 'readlinkSync', 'realpathSync', 'renameSync', 'rmSync', 'rmdirSync', 'statSync', 'symlinkSync', 'unlinkSync', 'writeFileSync']) { __agentOsWrapSyncFsMethod(__agentOsFs, __agentOsMethod); } } }";
8390    let display_stub = format!(
8391        "const __agentOsDisplayModulePath = require.resolve({display_module}); const __agentOsLogFileModulePath = require.resolve({log_file_module}); const __agentOsColorPassthrough = new Proxy((value) => value, {{ get: () => __agentOsColorPassthrough, apply: (_target, _thisArg, args) => args[0] }}); class __AgentOsNpmDisplayStub {{ constructor() {{ this.chalk = {{ noColor: __agentOsColorPassthrough, stdout: __agentOsColorPassthrough, stderr: __agentOsColorPassthrough }}; this._logPaused = true; this._logBuffer = []; this._outputBuffer = []; this._write = (stream, values) => {{ if (!Array.isArray(values) || values.length === 0) {{ return; }} const text = values.map((value) => typeof value === 'string' ? value : String(value)).join(' '); if (text.length === 0) {{ return; }} const normalized = text.replace(/\\r\\n/g, '\\n'); if (/^\\n?> npx\\n> /u.test(normalized)) {{ return; }} stream.write(text.endsWith('\\n') ? text : `${{text}}\\n`); }}; this._inputHandler = (level, ...args) => {{ if (level !== 'read') {{ return; }} const [resolve, reject, callback] = args; Promise.resolve().then(() => callback()).then(resolve, reject); }}; this._logHandler = (level, ...args) => {{ if (level === 'resume') {{ this._logPaused = false; for (const entry of this._logBuffer.splice(0)) {{ this._write(process.stderr, entry); }} return; }} if (level === 'pause') {{ this._logPaused = true; return; }} if (this._logPaused) {{ this._logBuffer.push(args); return; }} this._write(process.stderr, args); }}; this._outputHandler = (level, ...args) => {{ if (level === 'buffer') {{ this._outputBuffer.push(['standard', args]); return; }} if (level === 'flush') {{ for (const [bufferLevel, bufferArgs] of this._outputBuffer.splice(0)) {{ this._write(bufferLevel === 'error' ? process.stderr : process.stdout, bufferArgs); }} return; }} this._write(level === 'error' ? process.stderr : process.stdout, args); }}; process.on('input', this._inputHandler); process.on('log', this._logHandler); process.on('output', this._outputHandler); }} async load() {{ process.emit('log', 'resume'); process.emit('output', 'flush'); }} off() {{ if (this._inputHandler) {{ process.off('input', this._inputHandler); }} if (this._logHandler) {{ process.off('log', this._logHandler); }} if (this._outputHandler) {{ process.off('output', this._outputHandler); }} this._logBuffer.length = 0; this._outputBuffer.length = 0; }} }} class __AgentOsNpmLogFileStub {{ constructor() {{ this.files = []; }} async load() {{ return []; }} off() {{}} }} globalThis._moduleCache[__agentOsDisplayModulePath] = {{ exports: __AgentOsNpmDisplayStub }}; globalThis._moduleCache[__agentOsLogFileModulePath] = {{ exports: __AgentOsNpmLogFileStub }};",
8392        display_module = serde_json::to_string(&guest_display_module)
8393            .unwrap_or_else(|_| format!("\"{guest_display_module}\"")),
8394        log_file_module = serde_json::to_string(&guest_log_file_module)
8395            .unwrap_or_else(|_| format!("\"{guest_log_file_module}\"")),
8396    );
8397    let registry_fetch_stub = "const { createRequire: __agentOsCreateRequire } = require('module'); const __agentOsNpmRequire = __agentOsCreateRequire(require.resolve(__AGENT_OS_NPM_MAIN__)); try { const __agentOsMinipassFetchPath = __agentOsNpmRequire.resolve('minipass-fetch'); const __agentOsMinipassFetch = __agentOsNpmRequire(__agentOsMinipassFetchPath); const { FetchError: __agentOsFetchError, Headers: __agentOsFetchHeaders, Request: __agentOsFetchRequest, Response: __agentOsFetchResponse, AbortError: __agentOsAbortError } = __agentOsMinipassFetch; const { Minipass: __agentOsMinipass } = __agentOsNpmRequire('minipass'); const __agentOsCreateBinaryMinipass = () => new __agentOsMinipass({ objectMode: false, encoding: null }); const __agentOsCloneBuffer = (buffer) => Buffer.isBuffer(buffer) ? Buffer.from(buffer) : Buffer.from(buffer ?? []); const __agentOsBufferToArrayBuffer = (buffer) => { const bytes = __agentOsCloneBuffer(buffer); return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); }; const __agentOsAttachBufferedBodyMethods = (response, responseBuffer) => { const __agentOsReadBuffer = async () => __agentOsCloneBuffer(responseBuffer); response.__agentOsBufferedBody = __agentOsCloneBuffer(responseBuffer); response.buffer = __agentOsReadBuffer; response.text = async () => (await __agentOsReadBuffer()).toString('utf8'); response.json = async () => JSON.parse(await response.text()); response.arrayBuffer = async () => __agentOsBufferToArrayBuffer(await __agentOsReadBuffer()); response.clone = () => { const clonedBody = __agentOsCreateBinaryMinipass(); const clonedBuffer = __agentOsCloneBuffer(responseBuffer); clonedBody.end(clonedBuffer); const clonedResponse = new __agentOsFetchResponse(clonedBody, { url: response.url, status: response.status, statusText: response.statusText, headers: response.headers, size: response.size, timeout: response.timeout, counter: response.counter, trailer: response.trailer }); return __agentOsAttachBufferedBodyMethods(clonedResponse, clonedBuffer); }; return response; }; const __agentOsNormalizeHeaders = (__agentOsHeaders) => { const normalized = {}; __agentOsHeaders.forEach((value, key) => { if (normalized[key] === undefined) { normalized[key] = value; return; } if (Array.isArray(normalized[key])) { normalized[key].push(value); return; } normalized[key] = [normalized[key], value]; }); return normalized; }; const __agentOsPatchedMinipassFetch = async (input, opts = {}) => { const request = input instanceof __agentOsFetchRequest ? input : new __agentOsFetchRequest(input, opts); const __agentOsController = !request.signal && typeof AbortController === 'function' ? new AbortController() : null; const __agentOsSignal = request.signal ?? __agentOsController?.signal; let __agentOsTimer = null; if (__agentOsController && Number.isFinite(request.timeout) && request.timeout > 0) { __agentOsTimer = setTimeout(() => __agentOsController.abort(new Error(`network timeout at: ${request.url}`)), request.timeout); __agentOsTimer.unref?.(); } try { const requestHeaders = {}; request.headers.forEach((value, key) => { requestHeaders[key] = value; }); const response = await fetch(request.url, { method: request.method, headers: requestHeaders, body: request.body ?? undefined, redirect: request.redirect ?? opts.redirect ?? 'follow', signal: __agentOsSignal, ...(request.body ? { duplex: 'half' } : {}) }); const responseBody = __agentOsCreateBinaryMinipass(); const contentType = String(response.headers.get('content-type') || '').toLowerCase(); const responseBuffer = contentType.includes('json') ? Buffer.from(JSON.stringify(await response.json())) : contentType.startsWith('text/') ? Buffer.from(await response.text()) : Buffer.from(await response.arrayBuffer()); responseBody.end(responseBuffer); return __agentOsAttachBufferedBodyMethods(new __agentOsFetchResponse(responseBody, { url: response.url, status: response.status, statusText: response.statusText, headers: __agentOsNormalizeHeaders(response.headers), size: request.size, timeout: request.timeout, counter: request.counter ?? opts.counter ?? 0, trailer: Promise.resolve(new __agentOsFetchHeaders()) }), responseBuffer); } catch (error) { if (error instanceof Error) { throw error; } throw new __agentOsFetchError(String(error), 'system', error); } finally { if (__agentOsTimer) { clearTimeout(__agentOsTimer); } } }; globalThis.__agentOsPatchedMinipassFetch = __agentOsPatchedMinipassFetch; __agentOsPatchedMinipassFetch.isRedirect = typeof __agentOsMinipassFetch.isRedirect === 'function' ? __agentOsMinipassFetch.isRedirect.bind(__agentOsMinipassFetch) : (code) => code === 301 || code === 302 || code === 303 || code === 307 || code === 308; __agentOsPatchedMinipassFetch.FetchError = __agentOsFetchError; __agentOsPatchedMinipassFetch.Headers = __agentOsFetchHeaders; __agentOsPatchedMinipassFetch.Request = __agentOsFetchRequest; __agentOsPatchedMinipassFetch.Response = __agentOsFetchResponse; __agentOsPatchedMinipassFetch.AbortError = __agentOsAbortError; globalThis._moduleCache[__agentOsMinipassFetchPath] = { exports: __agentOsPatchedMinipassFetch }; __agentOsDebugLog('patched-minipass-fetch', __agentOsMinipassFetchPath); const __agentOsCheckResponsePath = __agentOsNpmRequire.resolve('npm-registry-fetch/lib/check-response.js'); const __agentOsCheckResponse = __agentOsNpmRequire(__agentOsCheckResponsePath); const __agentOsEnsureResponseBodyStream = (response) => { if (!response || (response.body && typeof response.body.on === 'function')) { return response; } const body = __agentOsCreateBinaryMinipass(); const finishWithError = (error) => body.emit('error', error instanceof Error ? error : new Error(String(error))); try { if (typeof response.buffer === 'function') { Promise.resolve(response.buffer()).then((buffer) => body.end(buffer), finishWithError); } else if (Buffer.isBuffer(response.body) || typeof response.body === 'string') { body.end(response.body); } else if (response.body && typeof response.body[Symbol.asyncIterator] === 'function') { (async () => { try { for await (const chunk of response.body) { body.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); } body.end(); } catch (error) { finishWithError(error); body.end(); } })(); } else { body.end(); } } catch (error) { finishWithError(error); body.end(); } return new __agentOsFetchResponse(body, response); }; globalThis._moduleCache[__agentOsCheckResponsePath] = { exports: (payload) => { const normalized = { ...payload, res: __agentOsEnsureResponseBodyStream(payload.res) }; __agentOsDebugLog('check-response-body', normalized.res && normalized.res.status, typeof (normalized.res && normalized.res.body), normalized.res && normalized.res.body && typeof normalized.res.body.on, normalized.res && normalized.res.body && normalized.res.body.constructor && normalized.res.body.constructor.name, !!(normalized.res && normalized.res.__agentOsBufferedBody), normalized.res && typeof normalized.res.json); return __agentOsCheckResponse(normalized); } }; __agentOsDebugLog('patched-check-response', __agentOsCheckResponsePath); } catch (error) { __agentOsDebugLog('patch-minipass-fetch-failed', error && error.stack ? error.stack : String(error)); } try { const __agentOsRegistryFetchPath = __agentOsNpmRequire.resolve('npm-registry-fetch'); const __agentOsRegistryFetch = __agentOsNpmRequire(__agentOsRegistryFetchPath); const __agentOsWrapRegistryFetch = (fn) => { const wrapResult = (promise) => Promise.resolve(promise).then((res) => { __agentOsDebugLog('registry-fetch-result', res && res.status, typeof (res && res.body), res && res.body && typeof res.body.on, res && res.body && res.body.constructor && res.body.constructor.name, !!(res && res.__agentOsBufferedBody), res && typeof res.json); return res; }); const wrapped = (uri, opts = {}) => wrapResult(globalThis.__agentOsPatchedMinipassFetch(uri, { method: opts.method, headers: opts.headers, body: opts.body, redirect: opts.redirect, signal: opts.signal, timeout: opts.timeout, size: opts.size, counter: opts.counter })); if (typeof fn.json === 'function') { wrapped.json = (uri, opts = {}) => wrapped(uri, opts).then((res) => res.json()); } if (fn.json && typeof fn.json.stream === 'function') { wrapped.json = wrapped.json || {}; wrapped.json.stream = (uri, path, opts = {}) => fn.json.stream(uri, path, { ...opts, agent: false }); } if (typeof fn.pickRegistry === 'function') { wrapped.pickRegistry = fn.pickRegistry.bind(fn); } if (typeof fn.getAuth === 'function') { wrapped.getAuth = fn.getAuth.bind(fn); } return wrapped; }; globalThis._moduleCache[__agentOsRegistryFetchPath] = { exports: __agentOsWrapRegistryFetch(__agentOsRegistryFetch) }; __agentOsDebugLog('patched-npm-registry-fetch', __agentOsRegistryFetchPath); } catch (error) { __agentOsDebugLog('patch-npm-registry-fetch-failed', error && error.stack ? error.stack : String(error)); }";
8398    match cli.command_name.as_str() {
8399        "npx" => format!(
8400            "{debug_preamble} {display_stub} {registry_fetch_stub} process.argv[1] = require.resolve({npm_cli}); process.argv.splice(2, 0, 'exec'); __agentOsDebugLog('argv', JSON.stringify(process.argv), 'cwd', process.cwd()); (async () => {{ const pkg = require({package_json}); if (process.argv.includes('--version') || process.argv.includes('-v')) {{ __agentOsDebugLog('version-shortcut'); console.log(pkg.version); __agentOsFinish(0); return; }} const Npm = require({npm_main}); const npm = new Npm(); __agentOsDebugLog('before-load'); const loaded = await npm.load(); __agentOsDebugLog('after-load', loaded && loaded.command, JSON.stringify(loaded && loaded.args)); if (!loaded.exec) {{ __agentOsDebugLog('no-exec'); __agentOsFinish(); return; }} if (!loaded.command) {{ __agentOsDebugLog('no-command'); const {{ output }} = require('proc-log'); output.standard(npm.usage); __agentOsFinish(1); return; }} __agentOsDebugLog('before-exec', loaded.command, JSON.stringify(loaded.args)); await npm.exec(loaded.command, loaded.args); __agentOsDebugLog('after-exec', __agentOsResolveExitCode()); __agentOsFinish(); }})().catch((error) => {{ if (__agentOsIsProcessExitError(error)) {{ __agentOsDebugLog('process-exit-error', __agentOsResolveExitCode(error.code)); __agentOsFinish(error.code); return; }} console.error(error && error.stack ? error.stack : String(error)); __agentOsFinish(error && typeof error === 'object' && Number.isFinite(error.exitCode) ? error.exitCode : 1); }});",
8401            debug_preamble = debug_preamble,
8402            display_stub = display_stub,
8403            registry_fetch_stub = registry_fetch_stub.replace(
8404                "__AGENT_OS_NPM_MAIN__",
8405                &serde_json::to_string(&guest_npm_main)
8406                    .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8407            ),
8408            npm_main = serde_json::to_string(&guest_npm_main)
8409                .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8410            npm_cli = serde_json::to_string(&guest_npm_cli)
8411                .unwrap_or_else(|_| format!("\"{guest_npm_cli}\"")),
8412            package_json = serde_json::to_string(&guest_package_json)
8413                .unwrap_or_else(|_| format!("\"{guest_package_json}\"")),
8414        ),
8415        _ => format!(
8416            "{debug_preamble} {display_stub} {registry_fetch_stub} __agentOsDebugLog('argv', JSON.stringify(process.argv), 'cwd', process.cwd()); (async () => {{ const pkg = require({package_json}); if (process.argv.includes('--version') || process.argv.includes('-v')) {{ __agentOsDebugLog('version-shortcut'); console.log(pkg.version); __agentOsFinish(0); return; }} const Npm = require({npm_main}); const npm = new Npm(); __agentOsDebugLog('before-load'); const loaded = await npm.load(); __agentOsDebugLog('after-load', loaded && loaded.command, JSON.stringify(loaded && loaded.args)); if (!loaded.exec) {{ __agentOsDebugLog('no-exec'); __agentOsFinish(); return; }} if (!loaded.command) {{ __agentOsDebugLog('no-command'); const {{ output }} = require('proc-log'); output.standard(npm.usage); __agentOsFinish(1); return; }} __agentOsDebugLog('before-exec', loaded.command, JSON.stringify(loaded.args)); await npm.exec(loaded.command, loaded.args); __agentOsDebugLog('after-exec', __agentOsResolveExitCode()); __agentOsFinish(); }})().catch((error) => {{ if (__agentOsIsProcessExitError(error)) {{ __agentOsDebugLog('process-exit-error', __agentOsResolveExitCode(error.code)); __agentOsFinish(error.code); return; }} console.error(error && error.stack ? error.stack : String(error)); __agentOsFinish(error && typeof error === 'object' && Number.isFinite(error.exitCode) ? error.exitCode : 1); }});",
8417            debug_preamble = debug_preamble,
8418            display_stub = display_stub,
8419            registry_fetch_stub = registry_fetch_stub.replace(
8420                "__AGENT_OS_NPM_MAIN__",
8421                &serde_json::to_string(&guest_npm_main)
8422                    .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8423            ),
8424            npm_main = serde_json::to_string(&guest_npm_main)
8425                .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8426            package_json = serde_json::to_string(&guest_package_json)
8427                .unwrap_or_else(|_| format!("\"{guest_package_json}\"")),
8428        ),
8429    }
8430}
8431
8432fn resolve_guest_command_entrypoint(
8433    vm: &VmState,
8434    guest_cwd: &str,
8435    command: &str,
8436    path_env: Option<&str>,
8437) -> Option<String> {
8438    if !is_path_like_specifier(command) {
8439        if let Some(entrypoint) = vm.command_guest_paths.get(command) {
8440            return Some(entrypoint.clone());
8441        }
8442
8443        for search_dir in guest_command_search_dirs(vm, guest_cwd, path_env) {
8444            let candidate = normalize_path(&format!("{search_dir}/{command}"));
8445            if let Some(entrypoint) = resolve_guest_command_path_candidate(vm, &candidate) {
8446                return Some(entrypoint);
8447            }
8448        }
8449
8450        return None;
8451    }
8452
8453    let normalized = resolve_path_like_guest_specifier(guest_cwd, command);
8454    resolve_guest_command_path_candidate(vm, &normalized).or_else(|| {
8455        // Some guest shells materialize PATH lookups into absolute candidate paths.
8456        // If that path points into a searched directory but does not exist, fall
8457        // back to the command basename so the sidecar can remap VM command packages.
8458        let parent_dir = Path::new(&normalized).parent()?.to_str()?;
8459        if !guest_command_search_dirs(vm, guest_cwd, path_env)
8460            .iter()
8461            .any(|search_dir| normalize_path(search_dir) == normalize_path(parent_dir))
8462        {
8463            return None;
8464        }
8465
8466        let file_name = Path::new(&normalized).file_name()?.to_str()?;
8467        vm.command_guest_paths.get(file_name).cloned()
8468    })
8469}
8470
8471fn guest_command_search_dirs(vm: &VmState, guest_cwd: &str, path_env: Option<&str>) -> Vec<String> {
8472    let mut search_dirs = Vec::new();
8473    let mut seen = BTreeSet::new();
8474
8475    if let Some(path) = path_env.or_else(|| vm.guest_env.get("PATH").map(String::as_str)) {
8476        for segment in path.split(':') {
8477            let trimmed = segment.trim();
8478            if trimmed.is_empty() {
8479                continue;
8480            }
8481            let normalized = if trimmed.starts_with('/') {
8482                normalize_path(trimmed)
8483            } else {
8484                normalize_path(&format!("{guest_cwd}/{trimmed}"))
8485            };
8486            if seen.insert(normalized.clone()) {
8487                search_dirs.push(normalized);
8488            }
8489        }
8490    }
8491
8492    for fallback in ["/bin", "/usr/bin", "/usr/local/bin"] {
8493        let normalized = String::from(fallback);
8494        if seen.insert(normalized.clone()) {
8495            search_dirs.push(normalized);
8496        }
8497    }
8498
8499    search_dirs
8500}
8501
8502fn resolve_guest_command_path_candidate(vm: &VmState, candidate: &str) -> Option<String> {
8503    if candidate.starts_with("/bin/")
8504        || candidate.starts_with("/usr/bin/")
8505        || candidate.starts_with("/usr/local/bin/")
8506        || candidate.starts_with("/__secure_exec/commands/")
8507    {
8508        if let Some(file_name) = Path::new(candidate)
8509            .file_name()
8510            .and_then(|name| name.to_str())
8511        {
8512            if let Some(guest_entrypoint) = vm.command_guest_paths.get(file_name) {
8513                return Some(guest_entrypoint.clone());
8514            }
8515        }
8516    }
8517
8518    if vm
8519        .kernel
8520        .exists(candidate)
8521        .ok()
8522        .is_some_and(|exists| exists)
8523    {
8524        return Some(normalize_path(candidate));
8525    }
8526
8527    resolve_vm_guest_path_to_host(vm, candidate)
8528        .is_file()
8529        .then(|| normalize_path(candidate))
8530}
8531
8532fn resolve_host_entrypoint_within_vm_host_cwd(
8533    vm: &VmState,
8534    specifier: &str,
8535) -> Option<(String, String)> {
8536    let candidate = Path::new(specifier);
8537    if !candidate.is_absolute() {
8538        return None;
8539    }
8540
8541    let normalized_entrypoint = normalize_host_path(candidate);
8542    let normalized_host_cwd = normalize_host_path(&vm.host_cwd);
8543    if !path_is_within_root(&normalized_entrypoint, &normalized_host_cwd) {
8544        return None;
8545    }
8546
8547    let relative = normalized_entrypoint
8548        .strip_prefix(&normalized_host_cwd)
8549        .ok()?
8550        .to_string_lossy()
8551        .replace('\\', "/");
8552    let guest_entrypoint = if relative.is_empty() {
8553        String::from("/")
8554    } else {
8555        normalize_path(&format!("/{relative}"))
8556    };
8557    Some((
8558        guest_entrypoint,
8559        normalized_entrypoint.to_string_lossy().into_owned(),
8560    ))
8561}
8562
8563fn prepare_guest_runtime_env(
8564    vm: &VmState,
8565    env: &mut BTreeMap<String, String>,
8566    guest_cwd: &str,
8567    host_cwd: &Path,
8568    guest_entrypoint: Option<String>,
8569) -> Result<(), SidecarError> {
8570    let user = vm.kernel.user_profile();
8571    let resource_limits = vm.kernel.resource_limits();
8572    let path_mappings = runtime_guest_path_mappings(vm);
8573    let read_paths = expand_host_access_paths(
8574        std::iter::once(vm.cwd.clone())
8575            .chain(
8576                path_mappings
8577                    .iter()
8578                    .map(|mapping| PathBuf::from(&mapping.host_path)),
8579            )
8580            .chain(std::iter::once(host_cwd.to_path_buf()))
8581            .collect::<Vec<_>>()
8582            .as_slice(),
8583    );
8584    let write_paths = dedupe_host_paths(
8585        std::iter::once(vm.cwd.clone())
8586            .chain(std::iter::once(host_cwd.to_path_buf()))
8587            .chain(runtime_guest_writable_host_paths(vm))
8588            .collect::<Vec<_>>()
8589            .as_slice(),
8590    );
8591    let allowed_node_builtins = configured_allowed_node_builtins(vm);
8592    let loopback_exempt_ports = configured_loopback_exempt_ports(vm);
8593
8594    env.insert(
8595        String::from("AGENT_OS_GUEST_PATH_MAPPINGS"),
8596        serde_json::to_string(&path_mappings).map_err(|error| {
8597            SidecarError::InvalidState(format!("failed to encode guest path mappings: {error}"))
8598        })?,
8599    );
8600    env.entry(String::from(EXECUTION_SANDBOX_ROOT_ENV))
8601        .or_insert_with(|| normalize_host_path(&vm.cwd).to_string_lossy().into_owned());
8602    env.insert(
8603        String::from("AGENT_OS_EXTRA_FS_READ_PATHS"),
8604        serde_json::to_string(
8605            &read_paths
8606                .iter()
8607                .map(|path| path.to_string_lossy().into_owned())
8608                .collect::<Vec<_>>(),
8609        )
8610        .map_err(|error| {
8611            SidecarError::InvalidState(format!("failed to encode read paths: {error}"))
8612        })?,
8613    );
8614    env.insert(
8615        String::from("AGENT_OS_EXTRA_FS_WRITE_PATHS"),
8616        serde_json::to_string(
8617            &write_paths
8618                .iter()
8619                .map(|path| path.to_string_lossy().into_owned())
8620                .collect::<Vec<_>>(),
8621        )
8622        .map_err(|error| {
8623            SidecarError::InvalidState(format!("failed to encode write paths: {error}"))
8624        })?,
8625    );
8626    env.insert(
8627        String::from("AGENT_OS_ALLOWED_NODE_BUILTINS"),
8628        serde_json::to_string(&allowed_node_builtins).map_err(|error| {
8629            SidecarError::InvalidState(format!("failed to encode allowed builtins: {error}"))
8630        })?,
8631    );
8632    // The guest JS host platform drives subtractive global scrubbing in the
8633    // per-execution runtime shim (see prepend_v8_runtime_shim).
8634    env.insert(
8635        String::from("AGENT_OS_JS_PLATFORM"),
8636        js_runtime_platform_env(vm).to_owned(),
8637    );
8638    // Module-resolution mode (omitted when full Node resolution / the default).
8639    if let Some(resolution) = js_runtime_module_resolution_env(vm) {
8640        env.insert(
8641            String::from("AGENT_OS_JS_MODULE_RESOLUTION"),
8642            resolution.to_owned(),
8643        );
8644    }
8645    // Builtin allow-list gate for the live resolver. Present only when builtins
8646    // should be restricted (non-node platform => deny all; node + explicit
8647    // allow-list => exactly those). Absent => unrestricted (node default).
8648    if let Some(allowlist) = js_runtime_enforced_builtins(vm) {
8649        env.insert(
8650            String::from("AGENT_OS_JS_BUILTIN_ALLOWLIST"),
8651            serde_json::to_string(&allowlist).map_err(|error| {
8652                SidecarError::InvalidState(format!(
8653                    "failed to encode jsRuntime builtin allow-list: {error}"
8654                ))
8655            })?,
8656        );
8657    }
8658    env.insert(
8659        String::from("AGENT_OS_VIRTUAL_OS_USER"),
8660        user.username.clone(),
8661    );
8662    env.insert(
8663        String::from("AGENT_OS_VIRTUAL_OS_HOMEDIR"),
8664        user.homedir.clone(),
8665    );
8666    env.insert(
8667        String::from("AGENT_OS_VIRTUAL_OS_SHELL"),
8668        user.shell.clone(),
8669    );
8670    env.insert(
8671        String::from("AGENT_OS_VIRTUAL_OS_CPU_COUNT"),
8672        virtual_os_cpu_count(resource_limits).to_string(),
8673    );
8674    env.insert(
8675        String::from("AGENT_OS_VIRTUAL_OS_TOTALMEM"),
8676        virtual_os_totalmem_bytes(resource_limits).to_string(),
8677    );
8678    env.insert(
8679        String::from("AGENT_OS_VIRTUAL_OS_FREEMEM"),
8680        virtual_os_freemem_bytes(resource_limits).to_string(),
8681    );
8682    env.insert(
8683        String::from("AGENT_OS_VIRTUAL_PROCESS_UID"),
8684        user.uid.to_string(),
8685    );
8686    env.insert(
8687        String::from("AGENT_OS_VIRTUAL_PROCESS_GID"),
8688        user.gid.to_string(),
8689    );
8690    env.entry(String::from("HOME"))
8691        .or_insert_with(|| user.homedir.clone());
8692    env.entry(String::from("USER"))
8693        .or_insert_with(|| user.username.clone());
8694    env.entry(String::from("LOGNAME"))
8695        .or_insert_with(|| user.username.clone());
8696    env.entry(String::from("SHELL"))
8697        .or_insert_with(|| user.shell.clone());
8698    env.entry(String::from("PATH")).or_insert_with(|| {
8699        vm.guest_env
8700            .get("PATH")
8701            .cloned()
8702            .unwrap_or_else(|| crate::vm::DEFAULT_GUEST_PATH_ENV.to_owned())
8703    });
8704    env.entry(String::from("TMPDIR"))
8705        .or_insert_with(|| String::from("/tmp"));
8706    env.insert(String::from("PWD"), guest_cwd.to_owned());
8707    if !loopback_exempt_ports.is_empty() {
8708        env.insert(
8709            String::from(LOOPBACK_EXEMPT_PORTS_ENV),
8710            serde_json::to_string(&loopback_exempt_ports).map_err(|error| {
8711                SidecarError::InvalidState(format!("failed to encode loopback exemptions: {error}"))
8712            })?,
8713        );
8714    }
8715    if let Some(guest_entrypoint) = guest_entrypoint {
8716        env.insert(String::from("AGENT_OS_GUEST_ENTRYPOINT"), guest_entrypoint);
8717    }
8718    Ok(())
8719}
8720
8721fn virtual_os_cpu_count(resource_limits: &ResourceLimits) -> usize {
8722    resource_limits.virtual_cpu_count.unwrap_or(1).max(1)
8723}
8724
8725fn virtual_os_totalmem_bytes(resource_limits: &ResourceLimits) -> u64 {
8726    resource_limits
8727        .max_wasm_memory_bytes
8728        .unwrap_or(1024 * 1024 * 1024)
8729}
8730
8731fn virtual_os_freemem_bytes(resource_limits: &ResourceLimits) -> u64 {
8732    resource_limits
8733        .max_wasm_memory_bytes
8734        .unwrap_or(512 * 1024 * 1024)
8735}
8736
8737/// The guest JavaScript host platform configured for this VM, defaulting to
8738/// full Node.js emulation when no `jsRuntime` config was supplied at create.
8739fn js_runtime_platform(vm: &VmState) -> vm_config::JsRuntimePlatform {
8740    vm.configuration
8741        .js_runtime
8742        .as_ref()
8743        .map(|cfg| cfg.platform)
8744        .unwrap_or(vm_config::JsRuntimePlatform::Node)
8745}
8746
8747/// Lowercase wire name for the configured platform, mirroring the serde
8748/// representation of `vm_config::JsRuntimePlatform`.
8749fn js_runtime_platform_env(vm: &VmState) -> &'static str {
8750    match js_runtime_platform(vm) {
8751        vm_config::JsRuntimePlatform::Node => "node",
8752        vm_config::JsRuntimePlatform::Browser => "browser",
8753        vm_config::JsRuntimePlatform::Neutral => "neutral",
8754        vm_config::JsRuntimePlatform::Bare => "bare",
8755    }
8756}
8757
8758/// Wire name for the configured module-resolution mode, or `None` when it is the
8759/// full-Node default (which the live resolver also assumes when the env is unset).
8760fn js_runtime_module_resolution_env(vm: &VmState) -> Option<&'static str> {
8761    let resolution = vm
8762        .configuration
8763        .js_runtime
8764        .as_ref()
8765        .map(|cfg| cfg.module_resolution)
8766        .unwrap_or(vm_config::JsModuleResolution::Node);
8767    match resolution {
8768        vm_config::JsModuleResolution::Node => None,
8769        vm_config::JsModuleResolution::Relative => Some("relative"),
8770        vm_config::JsModuleResolution::None => Some("none"),
8771    }
8772}
8773
8774/// The builtin allow-list the live resolver should enforce, or `None` to leave
8775/// builtins unrestricted (full Node default — preserving today's behavior).
8776/// Non-node platforms enforce an empty list (deny all builtins).
8777fn js_runtime_enforced_builtins(vm: &VmState) -> Option<Vec<String>> {
8778    if js_runtime_platform(vm) != vm_config::JsRuntimePlatform::Node {
8779        return Some(Vec::new());
8780    }
8781    vm.configuration
8782        .js_runtime
8783        .as_ref()
8784        .and_then(|cfg| cfg.allowed_builtins.clone())
8785}
8786
8787fn configured_allowed_node_builtins(vm: &VmState) -> Vec<String> {
8788    // Non-node platforms expose no Node builtin modules at all.
8789    if js_runtime_platform(vm) != vm_config::JsRuntimePlatform::Node {
8790        return Vec::new();
8791    }
8792    // Under the node platform an explicit allow-list wins — including an explicit
8793    // empty list, which means deny all. Absence falls back to the engine default.
8794    let configured = match vm
8795        .configuration
8796        .js_runtime
8797        .as_ref()
8798        .and_then(|cfg| cfg.allowed_builtins.as_ref())
8799    {
8800        Some(list) => list.clone(),
8801        None => DEFAULT_ALLOWED_NODE_BUILTINS
8802            .iter()
8803            .map(|value| (*value).to_owned())
8804            .collect::<Vec<_>>(),
8805    };
8806    dedupe_strings(&configured)
8807}
8808
8809fn configured_loopback_exempt_ports(vm: &VmState) -> Vec<String> {
8810    if !vm.configuration.loopback_exempt_ports.is_empty() {
8811        return vm
8812            .configuration
8813            .loopback_exempt_ports
8814            .iter()
8815            .map(ToString::to_string)
8816            .collect();
8817    }
8818
8819    vm.create_loopback_exempt_ports
8820        .iter()
8821        .map(ToString::to_string)
8822        .collect()
8823}
8824
8825/// Extract the `hostPath` string from a mount plugin's JSON-encoded config.
8826fn mount_config_host_path(config: &str) -> Option<String> {
8827    serde_json::from_str::<Value>(config)
8828        .ok()?
8829        .get("hostPath")
8830        .and_then(Value::as_str)
8831        .map(str::to_owned)
8832}
8833
8834fn runtime_guest_writable_host_paths(vm: &VmState) -> Vec<PathBuf> {
8835    vm.configuration
8836        .mounts
8837        .iter()
8838        .filter(|mount| !mount.read_only)
8839        .filter_map(|mount| {
8840            ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
8841                .then(|| mount_config_host_path(&mount.plugin.config))
8842                .flatten()
8843                .map(PathBuf::from)
8844        })
8845        .collect()
8846}
8847
8848fn runtime_guest_path_mappings(vm: &VmState) -> Vec<RuntimeGuestPathMapping> {
8849    let mut mappings = vm
8850        .configuration
8851        .mounts
8852        .iter()
8853        .filter_map(|mount| {
8854            ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
8855                .then(|| {
8856                    mount_config_host_path(&mount.plugin.config).map(|host_path| {
8857                        RuntimeGuestPathMapping {
8858                            guest_path: normalize_path(&mount.guest_path),
8859                            host_path,
8860                            read_only: mount.read_only,
8861                        }
8862                    })
8863                })
8864                .flatten()
8865        })
8866        .collect::<Vec<_>>();
8867    let mut command_root_mappings = vm
8868        .command_guest_paths
8869        .values()
8870        .filter_map(|guest_path| {
8871            Path::new(guest_path)
8872                .parent()
8873                .and_then(|parent| parent.to_str())
8874                .map(normalize_path)
8875        })
8876        .collect::<BTreeSet<_>>()
8877        .into_iter()
8878        .map(|guest_path| RuntimeGuestPathMapping {
8879            host_path: resolve_vm_guest_path_to_host(vm, &guest_path)
8880                .to_string_lossy()
8881                .into_owned(),
8882            guest_path,
8883            read_only: false,
8884        })
8885        .collect::<Vec<_>>();
8886    mappings.append(&mut command_root_mappings);
8887    let mut extra_node_modules_roots = mappings
8888        .iter()
8889        .filter(|mapping| mapping.guest_path.starts_with("/root/node_modules/"))
8890        .filter_map(|mapping| {
8891            host_node_modules_root(Path::new(&mapping.host_path)).map(|host_root| {
8892                RuntimeGuestPathMapping {
8893                    guest_path: String::from("/root/node_modules"),
8894                    host_path: host_root.to_string_lossy().into_owned(),
8895                    read_only: mapping.read_only,
8896                }
8897            })
8898        })
8899        .collect::<Vec<_>>();
8900    mappings.append(&mut extra_node_modules_roots);
8901    mappings.push(RuntimeGuestPathMapping {
8902        guest_path: String::from("/"),
8903        host_path: vm.cwd.to_string_lossy().into_owned(),
8904        read_only: false,
8905    });
8906    mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.guest_path.len()));
8907    mappings.dedup_by(|left, right| {
8908        left.guest_path == right.guest_path && left.host_path == right.host_path
8909    });
8910    mappings
8911}
8912
8913/// Build a `Send`-able, read-only VFS module reader over the VM's read-only
8914/// `host_dir`/`module_access` mounts (and the derived `/root/node_modules` root
8915/// for nested mounts). When present, the V8 bridge thread resolves modules
8916/// inline against this reader — concurrently with the service loop — so a large
8917/// cold-start module graph never serializes behind / starves an in-flight ACP
8918/// `session/new` bootstrap on the single service-loop thread. The reader reads
8919/// the same mounted tree the guest sees (anchored `openat2`, escaping-symlink
8920/// refusal), never the host-direct path translator. Returns `None` when the VM
8921/// has no usable read-only mount, so resolution falls back to the service-loop
8922/// kernel reader.
8923fn build_module_reader(vm: &VmState) -> Option<crate::plugins::host_dir::HostDirModuleReader> {
8924    let mut pairs: Vec<(String, PathBuf)> = vm
8925        .configuration
8926        .mounts
8927        .iter()
8928        .filter(|mount| mount.read_only)
8929        .filter(|mount| (mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
8930        .filter_map(|mount| {
8931            mount_config_host_path(&mount.plugin.config)
8932                .map(|host_path| (normalize_path(&mount.guest_path), PathBuf::from(host_path)))
8933        })
8934        .collect();
8935
8936    // Mirror runtime_guest_path_mappings: a mount nested under
8937    // `/root/node_modules/<pkg>` implies a `/root/node_modules` root the resolver
8938    // walks, so expose that root too (e.g. software-package mounts).
8939    let extra_roots: Vec<(String, PathBuf)> = pairs
8940        .iter()
8941        .filter(|(guest_path, _)| guest_path.starts_with("/root/node_modules/"))
8942        .filter_map(|(_, host_path)| {
8943            host_node_modules_root(host_path).map(|root| (String::from("/root/node_modules"), root))
8944        })
8945        .collect();
8946    pairs.extend(extra_roots);
8947
8948    crate::plugins::host_dir::HostDirModuleReader::from_mounts(pairs)
8949}
8950
8951fn host_node_modules_root(path: &Path) -> Option<PathBuf> {
8952    if let Some(root) = path
8953        .ancestors()
8954        .filter(|candidate| {
8955            candidate.file_name().and_then(|name| name.to_str()) == Some("node_modules")
8956        })
8957        .last()
8958        .map(Path::to_path_buf)
8959    {
8960        return Some(root);
8961    }
8962
8963    fs::canonicalize(path)
8964        .ok()?
8965        .ancestors()
8966        .filter(|candidate| {
8967            candidate.file_name().and_then(|name| name.to_str()) == Some("node_modules")
8968        })
8969        .last()
8970        .map(Path::to_path_buf)
8971}
8972
8973#[cfg(test)]
8974mod runtime_guest_path_mapping_tests {
8975    use super::{host_node_modules_root, javascript_sync_rpc_option_bool};
8976    use serde_json::json;
8977    use std::fs;
8978    use std::time::{SystemTime, UNIX_EPOCH};
8979
8980    #[test]
8981    fn host_node_modules_root_prefers_workspace_root_over_pnpm_package_node_modules() {
8982        let unique = SystemTime::now()
8983            .duration_since(UNIX_EPOCH)
8984            .expect("clock should be monotonic")
8985            .as_nanos();
8986        let temp = std::env::temp_dir().join(format!("secure-exec-sidecar-node-modules-{unique}"));
8987        let workspace_node_modules = temp.join("node_modules");
8988        let package_root = workspace_node_modules
8989            .join(".pnpm")
8990            .join("example@1.0.0")
8991            .join("node_modules")
8992            .join("@scope")
8993            .join("pkg");
8994        fs::create_dir_all(&package_root).expect("package root should be created");
8995
8996        let resolved =
8997            host_node_modules_root(&package_root).expect("node_modules root should resolve");
8998
8999        assert_eq!(resolved, workspace_node_modules);
9000
9001        fs::remove_dir_all(&temp).expect("temp tree should be removed");
9002    }
9003
9004    #[test]
9005    fn host_node_modules_root_preserves_symlinked_workspace_node_modules_path() {
9006        let unique = SystemTime::now()
9007            .duration_since(UNIX_EPOCH)
9008            .expect("clock should be monotonic")
9009            .as_nanos();
9010        let temp =
9011            std::env::temp_dir().join(format!("secure-exec-sidecar-node-modules-symlink-{unique}"));
9012        let workspace_node_modules = temp.join("node_modules");
9013        let package_link = workspace_node_modules.join("@scope").join("pkg");
9014        let real_package = temp.join("registry").join("agent").join("pkg");
9015        fs::create_dir_all(package_link.parent().expect("package parent should exist"))
9016            .expect("scoped parent should be created");
9017        fs::create_dir_all(&real_package).expect("real package root should be created");
9018        std::os::unix::fs::symlink(&real_package, &package_link)
9019            .expect("package symlink should be created");
9020
9021        let resolved =
9022            host_node_modules_root(&package_link).expect("node_modules root should resolve");
9023
9024        assert_eq!(resolved, workspace_node_modules);
9025
9026        fs::remove_dir_all(&temp).expect("temp tree should be removed");
9027    }
9028
9029    #[test]
9030    fn javascript_sync_rpc_option_bool_accepts_boolean_recursive_argument() {
9031        assert_eq!(
9032            javascript_sync_rpc_option_bool(&[json!("/workspace"), json!(true)], 1, "recursive"),
9033            Some(true)
9034        );
9035        assert_eq!(
9036            javascript_sync_rpc_option_bool(
9037                &[json!("/workspace"), json!({ "recursive": false })],
9038                1,
9039                "recursive"
9040            ),
9041            Some(false)
9042        );
9043    }
9044}
9045
9046#[cfg(test)]
9047mod kernel_poll_sync_rpc_tests {
9048    use super::{
9049        service_javascript_kernel_poll_sync_rpc, ActiveExecution, ActiveProcess,
9050        JavascriptSyncRpcRequest, KernelPollFdResponse, SidecarKernel, ToolExecution,
9051        EXECUTION_DRIVER_NAME, JAVASCRIPT_COMMAND,
9052    };
9053    use secure_exec_kernel::command_registry::CommandDriver;
9054    use secure_exec_kernel::kernel::{KernelVmConfig, SpawnOptions};
9055    use secure_exec_kernel::mount_table::MountTable;
9056    use secure_exec_kernel::permissions::Permissions;
9057    use secure_exec_kernel::poll::{POLLHUP, POLLIN};
9058    use secure_exec_kernel::vfs::MemoryFileSystem;
9059    use serde_json::{json, Value};
9060    #[test]
9061    fn javascript_kernel_poll_sync_rpc_reports_multiple_kernel_fds() {
9062        let mut config = KernelVmConfig::new("vm-js-kernel-poll");
9063        config.permissions = Permissions::allow_all();
9064        let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config);
9065        kernel
9066            .register_driver(CommandDriver::new(
9067                EXECUTION_DRIVER_NAME,
9068                [JAVASCRIPT_COMMAND],
9069            ))
9070            .expect("register execution driver");
9071
9072        let kernel_handle = kernel
9073            .spawn_process(
9074                JAVASCRIPT_COMMAND,
9075                Vec::new(),
9076                SpawnOptions {
9077                    requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
9078                    ..SpawnOptions::default()
9079                },
9080            )
9081            .expect("spawn javascript kernel process");
9082        let pid = kernel_handle.pid();
9083
9084        let (stdin_read_fd, stdin_write_fd) = kernel
9085            .open_pipe(EXECUTION_DRIVER_NAME, pid)
9086            .expect("open kernel stdin pipe");
9087        kernel
9088            .fd_dup2(EXECUTION_DRIVER_NAME, pid, stdin_read_fd, 0)
9089            .expect("dup stdin pipe onto fd 0");
9090        kernel
9091            .fd_close(EXECUTION_DRIVER_NAME, pid, stdin_read_fd)
9092            .expect("close original stdin read fd");
9093
9094        let process = ActiveProcess::new(
9095            pid,
9096            kernel_handle,
9097            super::GuestRuntimeKind::JavaScript,
9098            ActiveExecution::Tool(ToolExecution::default()),
9099        );
9100
9101        kernel
9102            .fd_write(EXECUTION_DRIVER_NAME, pid, stdin_write_fd, b"poll-ready")
9103            .expect("write kernel stdin payload");
9104        kernel
9105            .fd_close(EXECUTION_DRIVER_NAME, pid, stdin_write_fd)
9106            .expect("close kernel stdin writer");
9107
9108        let response = service_javascript_kernel_poll_sync_rpc(
9109            &mut kernel,
9110            &process,
9111            &JavascriptSyncRpcRequest {
9112                id: 1,
9113                method: String::from("__kernel_poll"),
9114                args: vec![
9115                    json!([
9116                        { "fd": 0, "events": POLLIN.bits() },
9117                        { "fd": 1, "events": POLLIN.bits() }
9118                    ]),
9119                    json!(250),
9120                ],
9121            },
9122        )
9123        .expect("poll kernel fds");
9124
9125        assert_eq!(response["readyCount"], Value::from(1));
9126        let fds: Vec<KernelPollFdResponse> =
9127            serde_json::from_value(response["fds"].clone()).expect("kernel poll fd response");
9128        assert_eq!(
9129            fds,
9130            vec![
9131                KernelPollFdResponse {
9132                    fd: 0,
9133                    events: POLLIN.bits(),
9134                    revents: (POLLIN | POLLHUP).bits(),
9135                },
9136                KernelPollFdResponse {
9137                    fd: 1,
9138                    events: POLLIN.bits(),
9139                    revents: 0,
9140                },
9141            ]
9142        );
9143
9144        process.kernel_handle.finish(0);
9145        kernel.waitpid(pid).expect("wait javascript kernel process");
9146    }
9147}
9148
9149fn dedupe_strings(values: &[String]) -> Vec<String> {
9150    let mut seen = BTreeSet::new();
9151    let mut deduped = Vec::new();
9152    for value in values {
9153        if seen.insert(value.clone()) {
9154            deduped.push(value.clone());
9155        }
9156    }
9157    deduped
9158}
9159
9160fn dedupe_host_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
9161    let mut seen = BTreeSet::new();
9162    let mut deduped = Vec::new();
9163    for path in paths {
9164        let normalized = normalize_host_path(path);
9165        let key = normalized.to_string_lossy().into_owned();
9166        if seen.insert(key) {
9167            deduped.push(normalized);
9168        }
9169    }
9170    deduped
9171}
9172
9173fn expand_host_access_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
9174    let mut expanded = Vec::new();
9175    let mut seen = BTreeSet::new();
9176
9177    let mut add_path = |candidate: PathBuf| {
9178        let normalized = normalize_host_path(&candidate);
9179        let key = normalized.to_string_lossy().into_owned();
9180        if seen.insert(key) {
9181            expanded.push(normalized);
9182        }
9183    };
9184
9185    for host_path in paths {
9186        add_path(host_path.clone());
9187        if let Ok(realpath) = fs::canonicalize(host_path) {
9188            add_path(realpath);
9189        }
9190
9191        if host_path.file_name().and_then(|name| name.to_str()) != Some("node_modules") {
9192            continue;
9193        }
9194
9195        let mut current = host_path.parent();
9196        while let Some(parent) = current {
9197            let candidate = parent.join("node_modules");
9198            if candidate.exists() {
9199                add_path(candidate.clone());
9200                if let Ok(realpath) = fs::canonicalize(&candidate) {
9201                    add_path(realpath);
9202                }
9203            }
9204            current = parent.parent();
9205        }
9206    }
9207
9208    expanded
9209}
9210
9211fn prepare_javascript_shadow(
9212    vm: &mut VmState,
9213    resolved: &ResolvedChildProcessExecution,
9214) -> Result<(), SidecarError> {
9215    let guest_entrypoint = resolved
9216        .env
9217        .get("AGENT_OS_GUEST_ENTRYPOINT")
9218        .cloned()
9219        // An absolute `entrypoint` may be a host path that lives inside the VM's
9220        // host cwd (callers can pass a fully-qualified host path). The guest sees
9221        // it at its translated guest path (host_cwd -> guest_cwd), so the shadow
9222        // must be keyed by that guest path rather than the raw host path. Falling
9223        // back to the host path here would materialize the file at the wrong guest
9224        // location and the runtime's `require()` would fail with "Cannot find
9225        // module".
9226        .or_else(|| {
9227            resolve_host_entrypoint_within_vm_host_cwd(vm, &resolved.entrypoint)
9228                .map(|(guest_entrypoint, _)| guest_entrypoint)
9229        })
9230        .or_else(|| {
9231            resolved
9232                .entrypoint
9233                .starts_with('/')
9234                .then(|| normalize_path(&resolved.entrypoint))
9235        });
9236    let Some(guest_entrypoint) = guest_entrypoint else {
9237        return Ok(());
9238    };
9239    if host_mount_path_for_guest_path(vm, &guest_entrypoint).is_some() {
9240        return Ok(());
9241    }
9242    if vm.kernel.lstat(&guest_entrypoint).is_err() {
9243        let host_entrypoint = {
9244            let candidate = Path::new(&resolved.entrypoint);
9245            if candidate.is_absolute() {
9246                candidate.to_path_buf()
9247            } else {
9248                resolved.host_cwd.join(candidate)
9249            }
9250        };
9251        if host_entrypoint.exists() {
9252            materialize_host_path_to_shadow(vm, &guest_entrypoint, &host_entrypoint)?;
9253            // The shadow write only stages the file on the host side; the runtime
9254            // resolves modules against the kernel VFS, so the staged entrypoint
9255            // must be synced into the kernel before execution starts (otherwise
9256            // `require()` reports "Cannot find module").
9257            return sync_shadow_entrypoint_into_kernel(vm, &guest_entrypoint);
9258        }
9259    }
9260    materialize_guest_path_to_shadow(vm, &guest_entrypoint)
9261}
9262
9263/// Sync a freshly-staged shadow entrypoint into the kernel VFS so the runtime's
9264/// kernel-backed module resolver can read it. Mirrors the host->kernel file sync
9265/// used by the broader shadow reconciliation, but scoped to the single
9266/// entrypoint we just materialized.
9267fn sync_shadow_entrypoint_into_kernel(
9268    vm: &mut VmState,
9269    guest_entrypoint: &str,
9270) -> Result<(), SidecarError> {
9271    if vm.kernel.exists(guest_entrypoint).unwrap_or(false) {
9272        return Ok(());
9273    }
9274    let shadow_path = shadow_path_for_guest(vm, guest_entrypoint);
9275    let bytes = match fs::read(&shadow_path) {
9276        Ok(bytes) => bytes,
9277        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
9278        Err(error) => {
9279            return Err(SidecarError::Io(format!(
9280                "failed to read staged shadow entrypoint {}: {error}",
9281                shadow_path.display()
9282            )));
9283        }
9284    };
9285    if let Some(parent) = guest_parent_path(guest_entrypoint) {
9286        if !vm.kernel.exists(&parent).unwrap_or(false) {
9287            vm.kernel.mkdir(&parent, true).map_err(kernel_error)?;
9288        }
9289    }
9290    vm.kernel
9291        .write_file(guest_entrypoint, bytes)
9292        .map_err(kernel_error)?;
9293    Ok(())
9294}
9295
9296fn guest_parent_path(guest_path: &str) -> Option<String> {
9297    let parent = Path::new(guest_path).parent()?;
9298    let parent = parent.to_string_lossy();
9299    if parent.is_empty() || parent == "/" {
9300        None
9301    } else {
9302        Some(parent.into_owned())
9303    }
9304}
9305
9306fn materialize_host_path_to_shadow(
9307    vm: &VmState,
9308    guest_path: &str,
9309    host_path: &Path,
9310) -> Result<(), SidecarError> {
9311    let shadow_path = shadow_path_for_guest(vm, guest_path);
9312    let metadata = fs::symlink_metadata(host_path)
9313        .map_err(|error| SidecarError::Io(format!("failed to stat host entrypoint: {error}")))?;
9314
9315    if metadata.file_type().is_symlink() {
9316        if let Some(parent) = shadow_path.parent() {
9317            fs::create_dir_all(parent).map_err(|error| {
9318                SidecarError::Io(format!("failed to create shadow symlink parent: {error}"))
9319            })?;
9320        }
9321        let _ = fs::remove_file(&shadow_path);
9322        let _ = fs::remove_dir_all(&shadow_path);
9323        let target = fs::read_link(host_path)
9324            .map_err(|error| SidecarError::Io(format!("failed to read host symlink: {error}")))?;
9325        std::os::unix::fs::symlink(&target, &shadow_path)
9326            .map_err(|error| SidecarError::Io(format!("failed to mirror host symlink: {error}")))?;
9327        return Ok(());
9328    }
9329
9330    if metadata.is_dir() {
9331        fs::create_dir_all(&shadow_path).map_err(|error| {
9332            SidecarError::Io(format!("failed to create shadow directory: {error}"))
9333        })?;
9334        fs::set_permissions(
9335            &shadow_path,
9336            fs::Permissions::from_mode(metadata.permissions().mode() & 0o7777),
9337        )
9338        .map_err(|error| {
9339            SidecarError::Io(format!(
9340                "failed to set shadow directory mode on {}: {error}",
9341                shadow_path.display()
9342            ))
9343        })?;
9344        return Ok(());
9345    }
9346
9347    if let Some(parent) = shadow_path.parent() {
9348        fs::create_dir_all(parent).map_err(|error| {
9349            SidecarError::Io(format!("failed to create shadow parent: {error}"))
9350        })?;
9351    }
9352    let bytes = fs::read(host_path)
9353        .map_err(|error| SidecarError::Io(format!("failed to read host entrypoint: {error}")))?;
9354    fs::write(&shadow_path, bytes).map_err(|error| {
9355        SidecarError::Io(format!(
9356            "failed to mirror host file into shadow root: {error}"
9357        ))
9358    })?;
9359    fs::set_permissions(
9360        &shadow_path,
9361        fs::Permissions::from_mode(metadata.permissions().mode() & 0o7777),
9362    )
9363    .map_err(|error| {
9364        SidecarError::Io(format!(
9365            "failed to set shadow file mode on {}: {error}",
9366            shadow_path.display()
9367        ))
9368    })?;
9369    Ok(())
9370}
9371
9372fn materialize_guest_path_to_shadow(
9373    vm: &mut VmState,
9374    guest_path: &str,
9375) -> Result<(), SidecarError> {
9376    let stat = vm.kernel.lstat(guest_path).map_err(kernel_error)?;
9377    let shadow_path = shadow_path_for_guest(vm, guest_path);
9378
9379    if stat.is_symbolic_link {
9380        if let Some(parent) = shadow_path.parent() {
9381            fs::create_dir_all(parent).map_err(|error| {
9382                SidecarError::Io(format!("failed to create shadow symlink parent: {error}"))
9383            })?;
9384        }
9385        let _ = fs::remove_file(&shadow_path);
9386        let _ = fs::remove_dir_all(&shadow_path);
9387        let target = vm.kernel.read_link(guest_path).map_err(kernel_error)?;
9388        std::os::unix::fs::symlink(&target, &shadow_path)
9389            .map_err(|error| SidecarError::Io(format!("failed to mirror symlink: {error}")))?;
9390        return Ok(());
9391    }
9392
9393    if stat.is_directory {
9394        fs::create_dir_all(&shadow_path).map_err(|error| {
9395            SidecarError::Io(format!("failed to create shadow directory: {error}"))
9396        })?;
9397        fs::set_permissions(&shadow_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err(
9398            |error| {
9399                SidecarError::Io(format!(
9400                    "failed to set shadow directory mode on {}: {error}",
9401                    shadow_path.display()
9402                ))
9403            },
9404        )?;
9405        return Ok(());
9406    }
9407
9408    if let Some(parent) = shadow_path.parent() {
9409        fs::create_dir_all(parent).map_err(|error| {
9410            SidecarError::Io(format!("failed to create shadow parent: {error}"))
9411        })?;
9412    }
9413    let bytes = vm.kernel.read_file(guest_path).map_err(kernel_error)?;
9414    fs::write(&shadow_path, bytes).map_err(|error| {
9415        SidecarError::Io(format!(
9416            "failed to mirror guest file into shadow root: {error}"
9417        ))
9418    })?;
9419    fs::set_permissions(&shadow_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err(
9420        |error| {
9421            SidecarError::Io(format!(
9422                "failed to set shadow file mode on {}: {error}",
9423                shadow_path.display()
9424            ))
9425        },
9426    )?;
9427    Ok(())
9428}
9429
9430fn load_javascript_entrypoint_source(
9431    vm: &mut VmState,
9432    host_cwd: &Path,
9433    entrypoint: &str,
9434    env: &BTreeMap<String, String>,
9435) -> Option<String> {
9436    let mut read_guest_file = |path: &str| {
9437        vm.kernel
9438            .read_file(path)
9439            .ok()
9440            .and_then(|bytes| String::from_utf8(bytes).ok())
9441    };
9442
9443    if let Some(source) = env
9444        .get("AGENT_OS_GUEST_ENTRYPOINT")
9445        .filter(|path| path.starts_with('/'))
9446        .and_then(|path| read_guest_file(path))
9447    {
9448        return Some(source);
9449    }
9450
9451    if entrypoint.starts_with('/') {
9452        if let Some(source) = read_guest_file(entrypoint) {
9453            return Some(source);
9454        }
9455    }
9456
9457    let host_entrypoint = if Path::new(entrypoint).is_absolute() {
9458        PathBuf::from(entrypoint)
9459    } else {
9460        host_cwd.join(entrypoint)
9461    };
9462    let normalized_entrypoint = normalize_host_path(&host_entrypoint);
9463    let sandbox_root = normalize_host_path(&vm.cwd);
9464    let host_cwd = normalize_host_path(&vm.host_cwd);
9465    if !path_is_within_root(&normalized_entrypoint, &sandbox_root)
9466        && !path_is_within_root(&normalized_entrypoint, &host_cwd)
9467    {
9468        return None;
9469    }
9470
9471    fs::read_to_string(&normalized_entrypoint).ok()
9472}
9473
9474fn apply_wasm_limit_env(env: &mut BTreeMap<String, String>, limits: &ResourceLimits) {
9475    if let Some(limit) = limits.max_wasm_fuel {
9476        env.insert(String::from(WASM_MAX_FUEL_ENV), limit.to_string());
9477    }
9478    if let Some(limit) = limits.max_wasm_memory_bytes {
9479        env.insert(String::from(WASM_MAX_MEMORY_BYTES_ENV), limit.to_string());
9480    }
9481    if let Some(limit) = limits.max_wasm_stack_bytes {
9482        env.insert(String::from(WASM_MAX_STACK_BYTES_ENV), limit.to_string());
9483    }
9484}
9485
9486fn emit_dns_resolution_event<B>(
9487    bridge: &SharedBridge<B>,
9488    vm_id: &str,
9489    hostname: &str,
9490    source: KernelDnsResolutionSource,
9491    addresses: &[IpAddr],
9492    dns: &VmDnsConfig,
9493) where
9494    B: NativeSidecarBridge + Send + 'static,
9495    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9496{
9497    let _ = emit_structured_event(
9498        bridge,
9499        vm_id,
9500        "network.dns.resolved",
9501        audit_fields([
9502            ("hostname", hostname.to_owned()),
9503            ("source", source.as_str().to_owned()),
9504            (
9505                "addresses",
9506                addresses
9507                    .iter()
9508                    .map(ToString::to_string)
9509                    .collect::<Vec<_>>()
9510                    .join(","),
9511            ),
9512            ("address_count", addresses.len().to_string()),
9513            ("resolver_count", dns.name_servers.len().to_string()),
9514            (
9515                "resolvers",
9516                dns.name_servers
9517                    .iter()
9518                    .map(ToString::to_string)
9519                    .collect::<Vec<_>>()
9520                    .join(","),
9521            ),
9522        ]),
9523    );
9524}
9525
9526fn emit_dns_record_resolution_event<B>(
9527    bridge: &SharedBridge<B>,
9528    vm_id: &str,
9529    hostname: &str,
9530    resolution: &DnsRecordResolution,
9531    dns: &VmDnsConfig,
9532) where
9533    B: NativeSidecarBridge + Send + 'static,
9534    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9535{
9536    if let Some(addresses) = dns_resolution_ip_addrs(resolution.records()) {
9537        emit_dns_resolution_event(
9538            bridge,
9539            vm_id,
9540            hostname,
9541            resolution.source(),
9542            &addresses,
9543            dns,
9544        );
9545        return;
9546    }
9547
9548    let _ = emit_structured_event(
9549        bridge,
9550        vm_id,
9551        "network.dns.resolved",
9552        audit_fields([
9553            ("hostname", hostname.to_owned()),
9554            ("source", resolution.source().as_str().to_owned()),
9555            (
9556                "addresses",
9557                resolution
9558                    .records()
9559                    .iter()
9560                    .map(summarize_dns_record)
9561                    .collect::<Vec<_>>()
9562                    .join(","),
9563            ),
9564            ("address_count", resolution.records().len().to_string()),
9565            ("resolver_count", dns.name_servers.len().to_string()),
9566            (
9567                "resolvers",
9568                dns.name_servers
9569                    .iter()
9570                    .map(ToString::to_string)
9571                    .collect::<Vec<_>>()
9572                    .join(","),
9573            ),
9574        ]),
9575    );
9576}
9577
9578fn emit_dns_resolution_failure_event<B>(
9579    bridge: &SharedBridge<B>,
9580    vm_id: &str,
9581    hostname: &str,
9582    dns: &VmDnsConfig,
9583    error: &SidecarError,
9584) where
9585    B: NativeSidecarBridge + Send + 'static,
9586    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9587{
9588    let _ = emit_structured_event(
9589        bridge,
9590        vm_id,
9591        "network.dns.resolve_failed",
9592        audit_fields([
9593            ("hostname", hostname.to_owned()),
9594            ("reason", error.to_string()),
9595            ("resolver_count", dns.name_servers.len().to_string()),
9596            (
9597                "resolvers",
9598                dns.name_servers
9599                    .iter()
9600                    .map(ToString::to_string)
9601                    .collect::<Vec<_>>()
9602                    .join(","),
9603            ),
9604        ]),
9605    );
9606}
9607
9608fn parse_dns_record_type(rrtype: &str) -> Result<RecordType, SidecarError> {
9609    match rrtype {
9610        "A" => Ok(RecordType::A),
9611        "AAAA" => Ok(RecordType::AAAA),
9612        "MX" => Ok(RecordType::MX),
9613        "TXT" => Ok(RecordType::TXT),
9614        "SRV" => Ok(RecordType::SRV),
9615        "CNAME" => Ok(RecordType::CNAME),
9616        "PTR" => Ok(RecordType::PTR),
9617        "NS" => Ok(RecordType::NS),
9618        "SOA" => Ok(RecordType::SOA),
9619        "NAPTR" => Ok(RecordType::NAPTR),
9620        "CAA" => Ok(RecordType::CAA),
9621        "ANY" => Ok(RecordType::ANY),
9622        other => Err(SidecarError::Execution(format!(
9623            "ERR_NOT_IMPLEMENTED: dns rrtype {other} is not supported by the secure-exec dns bridge"
9624        ))),
9625    }
9626}
9627
9628fn dns_resolution_to_node_value(
9629    resolution: &DnsRecordResolution,
9630    requested_type: &str,
9631) -> Result<Value, SidecarError> {
9632    let safe_ips = dns_resolution_safe_ip_set(resolution.records(), resolution.hostname())?;
9633    match requested_type {
9634        "A" | "AAAA" => Ok(Value::Array(
9635            resolution
9636                .records()
9637                .iter()
9638                .filter_map(|record| dns_record_ip_string(record, &safe_ips))
9639                .map(Value::String)
9640                .collect(),
9641        )),
9642        "MX" => Ok(Value::Array(
9643            resolution
9644                .records()
9645                .iter()
9646                .filter_map(|record| match record.data() {
9647                    RData::MX(mx) => Some(json!({
9648                        "priority": mx.preference,
9649                        "exchange": normalize_dns_name_for_node(&mx.exchange),
9650                        "type": "MX",
9651                    })),
9652                    _ => None,
9653                })
9654                .collect(),
9655        )),
9656        "TXT" => Ok(Value::Array(
9657            resolution
9658                .records()
9659                .iter()
9660                .filter_map(|record| match record.data() {
9661                    RData::TXT(txt) => Some(Value::Array(
9662                        txt.txt_data
9663                            .iter()
9664                            .map(|entry| Value::String(String::from_utf8_lossy(entry).into_owned()))
9665                            .collect(),
9666                    )),
9667                    _ => None,
9668                })
9669                .collect(),
9670        )),
9671        "SRV" => Ok(Value::Array(
9672            resolution
9673                .records()
9674                .iter()
9675                .filter_map(|record| match record.data() {
9676                    RData::SRV(srv) => Some(json!({
9677                        "priority": srv.priority,
9678                        "weight": srv.weight,
9679                        "port": srv.port,
9680                        "name": normalize_dns_name_for_node(&srv.target),
9681                        "type": "SRV",
9682                    })),
9683                    _ => None,
9684                })
9685                .collect(),
9686        )),
9687        "CNAME" => Ok(Value::Array(
9688            resolution
9689                .records()
9690                .iter()
9691                .filter_map(|record| match record.data() {
9692                    RData::CNAME(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9693                    _ => None,
9694                })
9695                .collect(),
9696        )),
9697        "PTR" => Ok(Value::Array(
9698            resolution
9699                .records()
9700                .iter()
9701                .filter_map(|record| match record.data() {
9702                    RData::PTR(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9703                    _ => None,
9704                })
9705                .collect(),
9706        )),
9707        "NS" => Ok(Value::Array(
9708            resolution
9709                .records()
9710                .iter()
9711                .filter_map(|record| match record.data() {
9712                    RData::NS(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9713                    _ => None,
9714                })
9715                .collect(),
9716        )),
9717        "SOA" => resolution
9718            .records()
9719            .iter()
9720            .find_map(|record| match record.data() {
9721                RData::SOA(soa) => Some(json!({
9722                    "nsname": normalize_dns_name_for_node(&soa.mname),
9723                    "hostmaster": normalize_dns_name_for_node(&soa.rname),
9724                    "serial": soa.serial,
9725                    "refresh": soa.refresh,
9726                    "retry": soa.retry,
9727                    "expire": soa.expire,
9728                    "minttl": soa.minimum,
9729                })),
9730                _ => None,
9731            })
9732            .ok_or_else(|| {
9733                SidecarError::Execution(String::from("failed to resolve DNS SOA record"))
9734            }),
9735        "NAPTR" => Ok(Value::Array(
9736            resolution
9737                .records()
9738                .iter()
9739                .filter_map(|record| match record.data() {
9740                    RData::NAPTR(naptr) => Some(json!({
9741                        "flags": String::from_utf8_lossy(&naptr.flags).into_owned(),
9742                        "service": String::from_utf8_lossy(&naptr.services).into_owned(),
9743                        "regexp": String::from_utf8_lossy(&naptr.regexp).into_owned(),
9744                        "replacement": normalize_dns_name_for_node(&naptr.replacement),
9745                        "order": naptr.order,
9746                        "preference": naptr.preference,
9747                    })),
9748                    _ => None,
9749                })
9750                .collect(),
9751        )),
9752        "CAA" => Ok(Value::Array(
9753            resolution
9754                .records()
9755                .iter()
9756                .filter_map(|record| match record.data() {
9757                    RData::CAA(caa) => {
9758                        let mut value = serde_json::Map::new();
9759                        value.insert(
9760                            "critical".to_owned(),
9761                            Value::from(u8::from(caa.issuer_critical)),
9762                        );
9763                        value.insert("type".to_owned(), Value::String(String::from("CAA")));
9764                        if caa.tag.eq_ignore_ascii_case("iodef") {
9765                            value.insert(
9766                                "iodef".to_owned(),
9767                                Value::String(
9768                                    caa.value_as_iodef()
9769                                        .map(|url| url.to_string())
9770                                        .unwrap_or_else(|_| {
9771                                            String::from_utf8_lossy(&caa.value).into_owned()
9772                                        }),
9773                                ),
9774                            );
9775                        } else if let Ok((issuer, _params)) = caa.value_as_issue() {
9776                            let field = if caa.tag.eq_ignore_ascii_case("issuewild") {
9777                                "issuewild"
9778                            } else {
9779                                "issue"
9780                            };
9781                            value.insert(
9782                                field.to_owned(),
9783                                Value::String(
9784                                    issuer.as_ref().map(ToString::to_string).unwrap_or_else(|| {
9785                                        String::from_utf8_lossy(&caa.value).into_owned()
9786                                    }),
9787                                ),
9788                            );
9789                        } else {
9790                            value.insert(
9791                                caa.tag.to_ascii_lowercase(),
9792                                Value::String(String::from_utf8_lossy(&caa.value).into_owned()),
9793                            );
9794                        }
9795                        Some(Value::Object(value))
9796                    }
9797                    _ => None,
9798                })
9799                .collect(),
9800        )),
9801        "ANY" => Ok(Value::Array(
9802            resolution
9803                .records()
9804                .iter()
9805                .filter_map(|record| dns_any_record_to_value(record, &safe_ips))
9806                .collect(),
9807        )),
9808        other => Err(SidecarError::Execution(format!(
9809            "ERR_NOT_IMPLEMENTED: dns rrtype {other} is not supported by the secure-exec dns bridge"
9810        ))),
9811    }
9812}
9813
9814fn dns_resolution_safe_ip_set(
9815    records: &[Record],
9816    hostname: &str,
9817) -> Result<BTreeSet<IpAddr>, SidecarError> {
9818    let ips = records
9819        .iter()
9820        .filter_map(dns_record_ip_addr)
9821        .collect::<Vec<_>>();
9822    if ips.is_empty() {
9823        return Ok(BTreeSet::new());
9824    }
9825    Ok(filter_dns_safe_ip_addrs(ips, hostname)?
9826        .into_iter()
9827        .collect())
9828}
9829
9830fn dns_resolution_ip_addrs(records: &[Record]) -> Option<Vec<IpAddr>> {
9831    let ips = records
9832        .iter()
9833        .filter_map(dns_record_ip_addr)
9834        .collect::<Vec<_>>();
9835    if ips.is_empty() {
9836        return None;
9837    }
9838    Some(ips)
9839}
9840
9841fn dns_record_ip_addr(record: &Record) -> Option<IpAddr> {
9842    match record.data() {
9843        RData::A(address) => Some(IpAddr::V4(**address)),
9844        RData::AAAA(address) => Some(IpAddr::V6(**address)),
9845        _ => None,
9846    }
9847}
9848
9849fn dns_record_ip_string(record: &Record, safe_ips: &BTreeSet<IpAddr>) -> Option<String> {
9850    let ip = dns_record_ip_addr(record)?;
9851    safe_ips.contains(&ip).then(|| ip.to_string())
9852}
9853
9854fn dns_any_record_to_value(record: &Record, safe_ips: &BTreeSet<IpAddr>) -> Option<Value> {
9855    let value = match record.data() {
9856        RData::A(_) | RData::AAAA(_) => json!({
9857            "address": dns_record_ip_string(record, safe_ips)?,
9858            "ttl": record.ttl(),
9859            "type": record.record_type().to_string(),
9860        }),
9861        RData::MX(mx) => json!({
9862            "exchange": normalize_dns_name_for_node(&mx.exchange),
9863            "priority": mx.preference,
9864            "type": "MX",
9865        }),
9866        RData::TXT(txt) => json!({
9867            "entries": txt
9868                .txt_data
9869                .iter()
9870                .map(|entry| String::from_utf8_lossy(entry).into_owned())
9871                .collect::<Vec<_>>(),
9872            "type": "TXT",
9873        }),
9874        RData::SRV(srv) => json!({
9875            "name": normalize_dns_name_for_node(&srv.target),
9876            "port": srv.port,
9877            "priority": srv.priority,
9878            "weight": srv.weight,
9879            "type": "SRV",
9880        }),
9881        RData::CNAME(name) => json!({
9882            "value": normalize_dns_name_for_node(&name.0),
9883            "type": "CNAME",
9884        }),
9885        RData::PTR(name) => json!({
9886            "value": normalize_dns_name_for_node(&name.0),
9887            "type": "PTR",
9888        }),
9889        RData::NS(name) => json!({
9890            "value": normalize_dns_name_for_node(&name.0),
9891            "type": "NS",
9892        }),
9893        RData::SOA(soa) => json!({
9894            "nsname": normalize_dns_name_for_node(&soa.mname),
9895            "hostmaster": normalize_dns_name_for_node(&soa.rname),
9896            "serial": soa.serial,
9897            "refresh": soa.refresh,
9898            "retry": soa.retry,
9899            "expire": soa.expire,
9900            "minttl": soa.minimum,
9901            "type": "SOA",
9902        }),
9903        RData::NAPTR(naptr) => json!({
9904            "flags": String::from_utf8_lossy(&naptr.flags).into_owned(),
9905            "service": String::from_utf8_lossy(&naptr.services).into_owned(),
9906            "regexp": String::from_utf8_lossy(&naptr.regexp).into_owned(),
9907            "replacement": normalize_dns_name_for_node(&naptr.replacement),
9908            "order": naptr.order,
9909            "preference": naptr.preference,
9910            "type": "NAPTR",
9911        }),
9912        RData::CAA(caa) => {
9913            let mut value = serde_json::Map::new();
9914            value.insert(
9915                "critical".to_owned(),
9916                Value::from(u8::from(caa.issuer_critical)),
9917            );
9918            value.insert("type".to_owned(), Value::String(String::from("CAA")));
9919            if caa.tag.eq_ignore_ascii_case("iodef") {
9920                value.insert(
9921                    "iodef".to_owned(),
9922                    Value::String(
9923                        caa.value_as_iodef()
9924                            .map(|url| url.to_string())
9925                            .unwrap_or_else(|_| String::from_utf8_lossy(&caa.value).into_owned()),
9926                    ),
9927                );
9928            } else if let Ok((issuer, _params)) = caa.value_as_issue() {
9929                let field = if caa.tag.eq_ignore_ascii_case("issuewild") {
9930                    "issuewild"
9931                } else {
9932                    "issue"
9933                };
9934                value.insert(
9935                    field.to_owned(),
9936                    Value::String(
9937                        issuer
9938                            .as_ref()
9939                            .map(ToString::to_string)
9940                            .unwrap_or_else(|| String::from_utf8_lossy(&caa.value).into_owned()),
9941                    ),
9942                );
9943            }
9944            Value::Object(value)
9945        }
9946        _ => return None,
9947    };
9948    Some(value)
9949}
9950
9951fn normalize_dns_name_for_node(name: &impl ToString) -> String {
9952    name.to_string().trim_end_matches('.').to_owned()
9953}
9954
9955fn summarize_dns_record(record: &Record) -> String {
9956    match record.data() {
9957        RData::A(_) | RData::AAAA(_) => record.data().to_string(),
9958        _ => format!("{} {}", record.record_type(), record.data()),
9959    }
9960}
9961
9962// build_root_filesystem, convert_root_lower_descriptor, convert_root_filesystem_entry,
9963// root_snapshot_entry moved to crate::bootstrap
9964
9965// apply_root_filesystem_entry, ensure_parent_directories moved to crate::bootstrap
9966
9967// ProcNetEntry moved to crate::state
9968
9969fn find_socket_state_entry(
9970    vm: Option<&VmState>,
9971    kind: SocketQueryKind,
9972    request: &FindListenerRequest,
9973) -> Result<Option<SocketStateEntry>, SidecarError> {
9974    let vm = vm.ok_or_else(|| SidecarError::InvalidState(String::from("unknown sidecar VM")))?;
9975
9976    for (process_id, process) in &vm.active_processes {
9977        if let Some(path) = request.path.as_deref() {
9978            if matches!(kind, SocketQueryKind::TcpListener) {
9979                for listener in process.unix_listeners.values() {
9980                    if listener.path() != path {
9981                        continue;
9982                    }
9983                    return Ok(Some(SocketStateEntry {
9984                        process_id: process_id.to_owned(),
9985                        host: None,
9986                        port: None,
9987                        path: Some(path.to_owned()),
9988                    }));
9989                }
9990            }
9991        }
9992
9993        if request.path.is_none() {
9994            if let Some(entry) =
9995                find_kernel_socket_state_entry(&vm.kernel, process_id, process, kind, request)?
9996            {
9997                return Ok(Some(entry));
9998            }
9999
10000            match kind {
10001                SocketQueryKind::TcpListener => {
10002                    for listener in process.tcp_listeners.values() {
10003                        if listener.kernel_socket_id.is_some() {
10004                            continue;
10005                        }
10006                        let local_addr = listener.guest_local_addr();
10007                        let local_host = local_addr.ip().to_string();
10008                        if !socket_host_matches(request.host.as_deref(), &local_host) {
10009                            continue;
10010                        }
10011                        if let Some(port) = request.port {
10012                            if local_addr.port() != port {
10013                                continue;
10014                            }
10015                        }
10016                        return Ok(Some(SocketStateEntry {
10017                            process_id: process_id.to_owned(),
10018                            host: Some(local_host),
10019                            port: Some(local_addr.port()),
10020                            path: None,
10021                        }));
10022                    }
10023                }
10024                SocketQueryKind::UdpBound => {
10025                    for socket in process.udp_sockets.values() {
10026                        if socket.kernel_socket_id.is_some() {
10027                            continue;
10028                        }
10029                        let Some(local_addr) = socket.local_addr() else {
10030                            continue;
10031                        };
10032                        let local_host = local_addr.ip().to_string();
10033                        if !socket_host_matches(request.host.as_deref(), &local_host) {
10034                            continue;
10035                        }
10036                        if let Some(port) = request.port {
10037                            if local_addr.port() != port {
10038                                continue;
10039                            }
10040                        }
10041                        return Ok(Some(SocketStateEntry {
10042                            process_id: process_id.to_owned(),
10043                            host: Some(local_host),
10044                            port: Some(local_addr.port()),
10045                            path: None,
10046                        }));
10047                    }
10048                }
10049            }
10050        }
10051
10052        let child_pid = process.execution.child_pid();
10053        let inodes = socket_inodes_for_pid(child_pid)?;
10054        if inodes.is_empty() {
10055            continue;
10056        }
10057
10058        if let Some(path) = request.path.as_deref() {
10059            if let Some(listener) = find_unix_socket_for_pid(child_pid, &inodes, path, process_id)?
10060            {
10061                return Ok(Some(listener));
10062            }
10063            continue;
10064        }
10065
10066        let table_paths = match kind {
10067            SocketQueryKind::TcpListener => [
10068                format!("/proc/{child_pid}/net/tcp"),
10069                format!("/proc/{child_pid}/net/tcp6"),
10070            ],
10071            SocketQueryKind::UdpBound => [
10072                format!("/proc/{child_pid}/net/udp"),
10073                format!("/proc/{child_pid}/net/udp6"),
10074            ],
10075        };
10076        for table_path in table_paths {
10077            if let Some(entry) = find_inet_socket_for_pid(
10078                &table_path,
10079                &inodes,
10080                kind,
10081                request.host.as_deref(),
10082                request.port,
10083                process_id,
10084            )? {
10085                return Ok(Some(entry));
10086            }
10087        }
10088    }
10089
10090    Ok(None)
10091}
10092
10093fn require_vm_inspection_permission<B>(
10094    bridge: &SharedBridge<B>,
10095    vm_id: &str,
10096    capability: &str,
10097    domain: &str,
10098    resource: &str,
10099) -> Result<(), SidecarError>
10100where
10101    B: NativeSidecarBridge + Send + 'static,
10102    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
10103{
10104    let decision = bridge.static_permission_decision(vm_id, capability, domain, Some(resource));
10105    if decision.as_ref().is_some_and(|decision| decision.allow) {
10106        return Ok(());
10107    }
10108
10109    let reason = decision
10110        .and_then(|decision| decision.reason)
10111        .unwrap_or_else(|| format!("{capability} permission required"));
10112    Err(SidecarError::Execution(format!(
10113        "EACCES: permission denied, {resource}: {reason}"
10114    )))
10115}
10116
10117fn socket_query_resource(kind: SocketQueryKind, request: &FindListenerRequest) -> String {
10118    if let Some(path) = request.path.as_deref() {
10119        return format!("unix://{path}");
10120    }
10121
10122    let host = request.host.as_deref().unwrap_or("*");
10123    let port = request
10124        .port
10125        .map_or_else(|| String::from("*"), |port| port.to_string());
10126    match kind {
10127        SocketQueryKind::TcpListener => format!("tcp://{host}:{port}"),
10128        SocketQueryKind::UdpBound => format!("udp://{host}:{port}"),
10129    }
10130}
10131
10132fn snapshot_vm_processes(vm: &VmState) -> Vec<ProcessSnapshotEntry> {
10133    let process_table = vm.kernel.list_processes();
10134    snapshot_vm_processes_inner(vm, &process_table)
10135}
10136
10137fn snapshot_vm_processes_inner(
10138    vm: &VmState,
10139    process_table: &BTreeMap<u32, secure_exec_kernel::process_table::ProcessInfo>,
10140) -> Vec<ProcessSnapshotEntry> {
10141    let mut entries = Vec::new();
10142
10143    for (process_id, process) in &vm.active_processes {
10144        collect_process_snapshot_entries(process_id, process, process_table, &mut entries);
10145    }
10146
10147    for exited in &vm.exited_process_snapshots {
10148        entries.push(exited.process.clone());
10149    }
10150
10151    entries
10152}
10153
10154fn prune_exited_process_snapshots(vm: &mut VmState) {
10155    let cutoff = Instant::now() - EXITED_PROCESS_SNAPSHOT_RETENTION;
10156    while vm
10157        .exited_process_snapshots
10158        .front()
10159        .is_some_and(|snapshot| snapshot.captured_at < cutoff)
10160    {
10161        vm.exited_process_snapshots.pop_front();
10162    }
10163}
10164
10165fn build_process_snapshot_entry(
10166    process_id: &str,
10167    process: &ActiveProcess,
10168    info: &secure_exec_kernel::process_table::ProcessInfo,
10169    exit_code: Option<i32>,
10170) -> ProcessSnapshotEntry {
10171    ProcessSnapshotEntry {
10172        process_id: process_id.to_owned(),
10173        pid: info.pid,
10174        ppid: info.ppid,
10175        pgid: info.pgid,
10176        sid: info.sid,
10177        driver: info.driver.clone(),
10178        command: info.command.clone(),
10179        args: Vec::new(),
10180        cwd: process.guest_cwd.clone(),
10181        status: if exit_code.is_some() {
10182            ProcessSnapshotStatus::Exited
10183        } else {
10184            match info.status {
10185                ProcessStatus::Running => ProcessSnapshotStatus::Running,
10186                ProcessStatus::Stopped => ProcessSnapshotStatus::Stopped,
10187                ProcessStatus::Exited => ProcessSnapshotStatus::Exited,
10188            }
10189        },
10190        exit_code: exit_code.or(info.exit_code),
10191    }
10192}
10193
10194fn collect_process_snapshot_entries(
10195    process_id: &str,
10196    process: &ActiveProcess,
10197    process_table: &BTreeMap<u32, secure_exec_kernel::process_table::ProcessInfo>,
10198    entries: &mut Vec<ProcessSnapshotEntry>,
10199) {
10200    if let Some(info) = process_table.get(&process.kernel_pid) {
10201        entries.push(build_process_snapshot_entry(
10202            process_id, process, info, None,
10203        ));
10204    }
10205
10206    for (child_id, child) in &process.child_processes {
10207        let child_process_id = format!("{process_id}/{child_id}");
10208        collect_process_snapshot_entries(&child_process_id, child, process_table, entries);
10209    }
10210}
10211
10212fn find_kernel_socket_state_entry(
10213    kernel: &SidecarKernel,
10214    process_id: &str,
10215    process: &ActiveProcess,
10216    kind: SocketQueryKind,
10217    request: &FindListenerRequest,
10218) -> Result<Option<SocketStateEntry>, SidecarError> {
10219    let entry = match kind {
10220        SocketQueryKind::TcpListener => process
10221            .tcp_listeners
10222            .values()
10223            .filter_map(|listener| listener.kernel_socket_id)
10224            .find_map(|socket_id| {
10225                kernel_socket_state_entry(kernel, process_id, socket_id, kind, request)
10226            }),
10227        SocketQueryKind::UdpBound => process
10228            .udp_sockets
10229            .values()
10230            .filter_map(|socket| socket.kernel_socket_id)
10231            .find_map(|socket_id| {
10232                kernel_socket_state_entry(kernel, process_id, socket_id, kind, request)
10233            }),
10234    };
10235
10236    if entry.is_some() {
10237        return Ok(entry);
10238    }
10239
10240    for child in process.child_processes.values() {
10241        if let Some(entry) =
10242            find_kernel_socket_state_entry(kernel, process_id, child, kind, request)?
10243        {
10244            return Ok(Some(entry));
10245        }
10246    }
10247
10248    Ok(None)
10249}
10250
10251fn kernel_socket_state_entry(
10252    kernel: &SidecarKernel,
10253    process_id: &str,
10254    socket_id: SocketId,
10255    kind: SocketQueryKind,
10256    request: &FindListenerRequest,
10257) -> Option<SocketStateEntry> {
10258    let record = kernel.socket_get(socket_id)?;
10259    let local_address = record.local_address()?;
10260    match kind {
10261        SocketQueryKind::TcpListener if record.state() == SocketState::Listening => {}
10262        SocketQueryKind::TcpListener => return None,
10263        SocketQueryKind::UdpBound => {}
10264    }
10265
10266    if !socket_host_matches(request.host.as_deref(), local_address.host()) {
10267        return None;
10268    }
10269    if request
10270        .port
10271        .is_some_and(|port| local_address.port() != port)
10272    {
10273        return None;
10274    }
10275
10276    Some(SocketStateEntry {
10277        process_id: process_id.to_owned(),
10278        host: Some(local_address.host().to_owned()),
10279        port: Some(local_address.port()),
10280        path: None,
10281    })
10282}
10283
10284fn socket_inodes_for_pid(pid: u32) -> Result<BTreeSet<u64>, SidecarError> {
10285    let fd_dir = PathBuf::from(format!("/proc/{pid}/fd"));
10286    let entries = match fs::read_dir(&fd_dir) {
10287        Ok(entries) => entries,
10288        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(BTreeSet::new()),
10289        Err(error) => {
10290            return Err(SidecarError::Io(format!(
10291                "failed to read socket descriptors for process {pid}: {error}"
10292            )));
10293        }
10294    };
10295
10296    let mut inodes = BTreeSet::new();
10297    for entry in entries {
10298        let entry = entry.map_err(|error| {
10299            SidecarError::Io(format!(
10300                "failed to inspect fd entry for process {pid}: {error}"
10301            ))
10302        })?;
10303        let target = match fs::read_link(entry.path()) {
10304            Ok(target) => target,
10305            Err(_) => continue,
10306        };
10307        if let Some(inode) = parse_socket_inode(&target) {
10308            inodes.insert(inode);
10309        }
10310    }
10311
10312    Ok(inodes)
10313}
10314
10315fn parse_socket_inode(target: &Path) -> Option<u64> {
10316    let value = target.to_string_lossy();
10317    let trimmed = value.strip_prefix("socket:[")?.strip_suffix(']')?;
10318    trimmed.parse().ok()
10319}
10320
10321fn unix_socket_path(addr: &UnixSocketAddr) -> Option<String> {
10322    addr.as_pathname()
10323        .map(|path| path.to_string_lossy().into_owned())
10324}
10325
10326fn find_unix_socket_for_pid(
10327    pid: u32,
10328    inodes: &BTreeSet<u64>,
10329    path: &str,
10330    process_id: &str,
10331) -> Result<Option<SocketStateEntry>, SidecarError> {
10332    let table_path = format!("/proc/{pid}/net/unix");
10333    let contents = match fs::read_to_string(&table_path) {
10334        Ok(contents) => contents,
10335        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
10336        Err(error) => {
10337            return Err(SidecarError::Io(format!(
10338                "failed to inspect unix sockets for process {pid}: {error}"
10339            )));
10340        }
10341    };
10342
10343    for line in contents.lines().skip(1) {
10344        let columns = line.split_whitespace().collect::<Vec<_>>();
10345        if columns.len() < 8 {
10346            continue;
10347        }
10348        let Ok(inode) = columns[6].parse::<u64>() else {
10349            continue;
10350        };
10351        if !inodes.contains(&inode) || columns[7] != path {
10352            continue;
10353        }
10354        return Ok(Some(SocketStateEntry {
10355            process_id: process_id.to_owned(),
10356            host: None,
10357            port: None,
10358            path: Some(path.to_owned()),
10359        }));
10360    }
10361
10362    Ok(None)
10363}
10364
10365fn find_inet_socket_for_pid(
10366    table_path: &str,
10367    inodes: &BTreeSet<u64>,
10368    kind: SocketQueryKind,
10369    requested_host: Option<&str>,
10370    requested_port: Option<u16>,
10371    process_id: &str,
10372) -> Result<Option<SocketStateEntry>, SidecarError> {
10373    for entry in parse_proc_net_entries(table_path)? {
10374        if !inodes.contains(&entry.inode) {
10375            continue;
10376        }
10377        if matches!(kind, SocketQueryKind::TcpListener) && entry.state != "0A" {
10378            continue;
10379        }
10380        if !socket_host_matches(requested_host, &entry.local_host) {
10381            continue;
10382        }
10383        if let Some(port) = requested_port {
10384            if entry.local_port != port {
10385                continue;
10386            }
10387        }
10388        return Ok(Some(SocketStateEntry {
10389            process_id: process_id.to_owned(),
10390            host: Some(entry.local_host),
10391            port: Some(entry.local_port),
10392            path: None,
10393        }));
10394    }
10395
10396    Ok(None)
10397}
10398
10399fn is_unspecified_socket_host(host: &str) -> bool {
10400    host == "0.0.0.0" || host == "::"
10401}
10402
10403fn is_loopback_socket_host(host: &str) -> bool {
10404    host == "127.0.0.1" || host == "::1" || host.eq_ignore_ascii_case("localhost")
10405}
10406
10407pub(crate) fn vm_network_resource_counts(vm: &VmState) -> NetworkResourceCounts {
10408    let snapshot = vm.kernel.resource_snapshot();
10409    let mut counts = NetworkResourceCounts {
10410        sockets: snapshot.sockets,
10411        connections: snapshot.socket_connections,
10412    };
10413    for process in vm.active_processes.values() {
10414        let process_counts = process.sidecar_only_network_resource_counts();
10415        counts.sockets += process_counts.sockets;
10416        counts.connections += process_counts.connections;
10417    }
10418    counts
10419}
10420
10421fn collect_javascript_socket_port_state(
10422    kernel: &SidecarKernel,
10423    process: &ActiveProcess,
10424    tcp_guest_to_host: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10425    udp_guest_to_host: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10426    udp_host_to_guest: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10427    used_tcp_ports: &mut BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10428    used_udp_ports: &mut BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10429) {
10430    for (family, port) in process.tcp_port_reservations.values() {
10431        used_tcp_ports.entry(*family).or_default().insert(*port);
10432    }
10433
10434    let mut record_tcp_listener = |guest_addr: SocketAddr, host_port: u16| {
10435        let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10436        used_tcp_ports
10437            .entry(family)
10438            .or_default()
10439            .insert(guest_addr.port());
10440        // VM-local loopback connects should also resolve listeners bound to
10441        // unspecified guest addresses like 0.0.0.0/::.
10442        tcp_guest_to_host.insert((family, guest_addr.port()), host_port);
10443    };
10444
10445    for listener in process.tcp_listeners.values() {
10446        let local_addr = listener
10447            .kernel_socket_id
10448            .and_then(|socket_id| kernel.socket_get(socket_id))
10449            .and_then(|record| record.local_address().cloned())
10450            .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
10451            .unwrap_or_else(|| listener.guest_local_addr());
10452        record_tcp_listener(local_addr, local_addr.port());
10453    }
10454
10455    for server in process.http_servers.values() {
10456        let host_port = match server.listener.local_addr() {
10457            Ok(addr) => addr.port(),
10458            Err(_) => continue,
10459        };
10460        record_tcp_listener(server.guest_local_addr, host_port);
10461    }
10462
10463    if let Ok(http2) = process.http2.shared.lock() {
10464        for server in http2.servers.values() {
10465            record_tcp_listener(server.guest_local_addr, server.actual_local_addr.port());
10466        }
10467    }
10468
10469    for socket in process.tcp_sockets.values() {
10470        let guest_addr = socket
10471            .kernel_socket_id
10472            .and_then(|socket_id| kernel.socket_get(socket_id))
10473            .and_then(|record| record.local_address().cloned())
10474            .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
10475            .unwrap_or(socket.guest_local_addr);
10476        let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10477        used_tcp_ports
10478            .entry(family)
10479            .or_default()
10480            .insert(guest_addr.port());
10481    }
10482
10483    for socket in process.udp_sockets.values() {
10484        let guest_addr = socket
10485            .kernel_socket_id
10486            .and_then(|socket_id| kernel.socket_get(socket_id))
10487            .and_then(|record| record.local_address().cloned())
10488            .and_then(|address| {
10489                resolve_udp_bind_addr(address.host(), address.port(), socket.family).ok()
10490            })
10491            .or_else(|| socket.local_addr());
10492        let Some(guest_addr) = guest_addr else {
10493            continue;
10494        };
10495        let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10496        used_udp_ports
10497            .entry(family)
10498            .or_default()
10499            .insert(guest_addr.port());
10500        if let Some(host_addr) = socket
10501            .socket
10502            .as_ref()
10503            .and_then(|socket| socket.local_addr().ok())
10504        {
10505            if is_loopback_ip(guest_addr.ip()) {
10506                udp_guest_to_host.insert((family, guest_addr.port()), host_addr.port());
10507                udp_host_to_guest.insert((family, host_addr.port()), guest_addr.port());
10508            }
10509        } else if socket.kernel_socket_id.is_some() && is_loopback_ip(guest_addr.ip()) {
10510            udp_guest_to_host.insert((family, guest_addr.port()), guest_addr.port());
10511            udp_host_to_guest.insert((family, guest_addr.port()), guest_addr.port());
10512        }
10513    }
10514
10515    for child in process.child_processes.values() {
10516        collect_javascript_socket_port_state(
10517            kernel,
10518            child,
10519            tcp_guest_to_host,
10520            udp_guest_to_host,
10521            udp_host_to_guest,
10522            used_tcp_ports,
10523            used_udp_ports,
10524        );
10525    }
10526}
10527
10528pub(crate) fn build_javascript_socket_path_context(
10529    vm: &VmState,
10530) -> Result<JavascriptSocketPathContext, SidecarError> {
10531    let mut loopback_exempt_ports = vm.create_loopback_exempt_ports.clone();
10532    loopback_exempt_ports.extend(vm.configuration.loopback_exempt_ports.iter().copied());
10533    let mut tcp_loopback_guest_to_host_ports = BTreeMap::new();
10534    let mut udp_loopback_guest_to_host_ports = BTreeMap::new();
10535    let mut udp_loopback_host_to_guest_ports = BTreeMap::new();
10536    let mut used_tcp_guest_ports = BTreeMap::new();
10537    let mut used_udp_guest_ports = BTreeMap::new();
10538    for process in vm.active_processes.values() {
10539        collect_javascript_socket_port_state(
10540            &vm.kernel,
10541            process,
10542            &mut tcp_loopback_guest_to_host_ports,
10543            &mut udp_loopback_guest_to_host_ports,
10544            &mut udp_loopback_host_to_guest_ports,
10545            &mut used_tcp_guest_ports,
10546            &mut used_udp_guest_ports,
10547        );
10548    }
10549    Ok(JavascriptSocketPathContext {
10550        sandbox_root: vm.cwd.clone(),
10551        mounts: vm.configuration.mounts.clone(),
10552        listen_policy: vm.listen_policy,
10553        loopback_exempt_ports,
10554        tcp_loopback_guest_to_host_ports,
10555        udp_loopback_guest_to_host_ports,
10556        udp_loopback_host_to_guest_ports,
10557        used_tcp_guest_ports,
10558        used_udp_guest_ports,
10559    })
10560}
10561
10562fn check_network_resource_limit(
10563    limit: Option<usize>,
10564    current: usize,
10565    additional: usize,
10566    label: &str,
10567) -> Result<(), SidecarError> {
10568    if let Some(limit) = limit {
10569        if current.saturating_add(additional) > limit {
10570            return Err(SidecarError::Execution(format!(
10571                "EAGAIN: maximum {label} count reached"
10572            )));
10573        }
10574    }
10575    Ok(())
10576}
10577
10578fn normalize_tcp_listen_host(
10579    host: Option<&str>,
10580) -> Result<(JavascriptSocketFamily, &'static str, &'static str), SidecarError> {
10581    match host.unwrap_or("127.0.0.1") {
10582        "127.0.0.1" | "localhost" => Ok((JavascriptSocketFamily::Ipv4, "127.0.0.1", "127.0.0.1")),
10583        "::1" => Ok((JavascriptSocketFamily::Ipv6, "::1", "::1")),
10584        "0.0.0.0" => Ok((JavascriptSocketFamily::Ipv4, "127.0.0.1", "0.0.0.0")),
10585        "::" => Ok((JavascriptSocketFamily::Ipv6, "::1", "::")),
10586        other => Err(SidecarError::Execution(format!(
10587            "EACCES: TCP listeners must bind to loopback or unspecified addresses, got {other}"
10588        ))),
10589    }
10590}
10591
10592fn normalize_udp_bind_host(
10593    host: Option<&str>,
10594    family: JavascriptUdpFamily,
10595) -> Result<(&'static str, &'static str, JavascriptSocketFamily), SidecarError> {
10596    match (family, host) {
10597        (JavascriptUdpFamily::Ipv4, None) | (JavascriptUdpFamily::Ipv4, Some("0.0.0.0")) => {
10598            Ok(("127.0.0.1", "0.0.0.0", JavascriptSocketFamily::Ipv4))
10599        }
10600        (JavascriptUdpFamily::Ipv4, Some("127.0.0.1"))
10601        | (JavascriptUdpFamily::Ipv4, Some("localhost")) => {
10602            Ok(("127.0.0.1", "127.0.0.1", JavascriptSocketFamily::Ipv4))
10603        }
10604        (JavascriptUdpFamily::Ipv6, None) | (JavascriptUdpFamily::Ipv6, Some("::")) => {
10605            Ok(("::1", "::", JavascriptSocketFamily::Ipv6))
10606        }
10607        (JavascriptUdpFamily::Ipv6, Some("::1"))
10608        | (JavascriptUdpFamily::Ipv6, Some("localhost")) => {
10609            Ok(("::1", "::1", JavascriptSocketFamily::Ipv6))
10610        }
10611        (JavascriptUdpFamily::Ipv4, Some(other)) => Err(SidecarError::Execution(format!(
10612            "EACCES: udp4 sockets must bind to 127.0.0.1 or 0.0.0.0, got {other}"
10613        ))),
10614        (JavascriptUdpFamily::Ipv6, Some(other)) => Err(SidecarError::Execution(format!(
10615            "EACCES: udp6 sockets must bind to ::1 or ::, got {other}"
10616        ))),
10617    }
10618}
10619
10620fn allocate_guest_listen_port(
10621    requested_port: u16,
10622    family: JavascriptSocketFamily,
10623    used_ports: &BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10624    policy: VmListenPolicy,
10625) -> Result<u16, SidecarError> {
10626    let is_allowed = |port: u16| {
10627        port >= policy.port_min
10628            && port <= policy.port_max
10629            && (policy.allow_privileged || port >= 1024)
10630    };
10631    let used = used_ports.get(&family);
10632
10633    if requested_port != 0 {
10634        if !is_allowed(requested_port) {
10635            let reason = if requested_port < 1024 && !policy.allow_privileged {
10636                format!(
10637                    "EACCES: privileged listen port {requested_port} requires {}=true",
10638                    VM_LISTEN_ALLOW_PRIVILEGED_METADATA_KEY
10639                )
10640            } else {
10641                format!(
10642                    "EACCES: listen port {requested_port} is outside the allowed range {}-{}",
10643                    policy.port_min, policy.port_max
10644                )
10645            };
10646            return Err(SidecarError::Execution(reason));
10647        }
10648        if used.is_some_and(|ports| ports.contains(&requested_port)) {
10649            return Err(sidecar_net_error(std::io::Error::from_raw_os_error(
10650                libc::EADDRINUSE,
10651            )));
10652        }
10653        return Ok(requested_port);
10654    }
10655
10656    let allocation_start = policy
10657        .port_min
10658        .max(if policy.allow_privileged { 1 } else { 1024 });
10659    for candidate in allocation_start..=policy.port_max {
10660        if used.is_some_and(|ports| ports.contains(&candidate)) {
10661            continue;
10662        }
10663        return Ok(candidate);
10664    }
10665
10666    Err(sidecar_net_error(std::io::Error::from_raw_os_error(
10667        libc::EADDRINUSE,
10668    )))
10669}
10670
10671fn socket_host_matches(requested: Option<&str>, actual: &str) -> bool {
10672    match requested {
10673        None => true,
10674        Some(requested) if requested == actual => true,
10675        Some(requested)
10676            if is_unspecified_socket_host(requested) && is_unspecified_socket_host(actual) =>
10677        {
10678            true
10679        }
10680        Some(requested) if is_unspecified_socket_host(requested) => is_loopback_socket_host(actual),
10681        Some(requested) if requested.eq_ignore_ascii_case("localhost") => {
10682            is_loopback_socket_host(actual)
10683        }
10684        _ => false,
10685    }
10686}
10687
10688fn parse_proc_net_entries(table_path: &str) -> Result<Vec<ProcNetEntry>, SidecarError> {
10689    let contents = match fs::read_to_string(table_path) {
10690        Ok(contents) => contents,
10691        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
10692        Err(error) => {
10693            return Err(SidecarError::Io(format!(
10694                "failed to inspect socket table {table_path}: {error}"
10695            )));
10696        }
10697    };
10698
10699    let mut entries = Vec::new();
10700    for line in contents.lines().skip(1) {
10701        let columns = line.split_whitespace().collect::<Vec<_>>();
10702        if columns.len() < 10 {
10703            continue;
10704        }
10705        let Some((host, port)) = parse_proc_ip_port(columns[1]) else {
10706            continue;
10707        };
10708        let Ok(inode) = columns[9].parse::<u64>() else {
10709            continue;
10710        };
10711        entries.push(ProcNetEntry {
10712            local_host: host,
10713            local_port: port,
10714            state: columns[3].to_owned(),
10715            inode,
10716        });
10717    }
10718
10719    Ok(entries)
10720}
10721
10722fn parse_proc_ip_port(value: &str) -> Option<(String, u16)> {
10723    let (raw_ip, raw_port) = value.split_once(':')?;
10724    let port = u16::from_str_radix(raw_port, 16).ok()?;
10725    let host = match raw_ip.len() {
10726        8 => {
10727            let raw = u32::from_str_radix(raw_ip, 16).ok()?;
10728            Ipv4Addr::from(raw.to_le_bytes()).to_string()
10729        }
10730        32 => {
10731            let mut bytes = [0_u8; 16];
10732            for (index, chunk) in raw_ip.as_bytes().chunks(8).enumerate() {
10733                let word = u32::from_str_radix(std::str::from_utf8(chunk).ok()?, 16).ok()?;
10734                bytes[index * 4..(index + 1) * 4].copy_from_slice(&word.to_le_bytes());
10735            }
10736            Ipv6Addr::from(bytes).to_string()
10737        }
10738        _ => return None,
10739    };
10740    Some((host, port))
10741}
10742
10743fn python_file_entrypoint(entrypoint: &str) -> Option<PathBuf> {
10744    let path = Path::new(entrypoint);
10745    (path.extension().and_then(|extension| extension.to_str()) == Some("py"))
10746        .then(|| path.to_path_buf())
10747}
10748
10749fn add_runtime_guest_path_mapping(
10750    env: &mut BTreeMap<String, String>,
10751    guest_path: &str,
10752    host_path: &Path,
10753) {
10754    let mut mappings = env
10755        .get("AGENT_OS_GUEST_PATH_MAPPINGS")
10756        .and_then(|value| serde_json::from_str::<Vec<Value>>(value).ok())
10757        .unwrap_or_default();
10758    mappings.retain(|mapping| {
10759        mapping
10760            .get("guestPath")
10761            .and_then(Value::as_str)
10762            .map(|existing| normalize_path(existing) != normalize_path(guest_path))
10763            .unwrap_or(true)
10764    });
10765    mappings.push(json!({
10766        "guestPath": normalize_path(guest_path),
10767        "hostPath": host_path.display().to_string(),
10768    }));
10769    if let Ok(serialized) = serde_json::to_string(&mappings) {
10770        env.insert(String::from("AGENT_OS_GUEST_PATH_MAPPINGS"), serialized);
10771    }
10772}
10773
10774fn add_runtime_host_access_path(
10775    env: &mut BTreeMap<String, String>,
10776    key: &str,
10777    host_path: &Path,
10778    expand: bool,
10779) {
10780    let existing = env
10781        .get(key)
10782        .and_then(|value| serde_json::from_str::<Vec<String>>(value).ok())
10783        .unwrap_or_default()
10784        .into_iter()
10785        .map(PathBuf::from)
10786        .collect::<Vec<_>>();
10787    let mut paths = existing;
10788    paths.push(host_path.to_path_buf());
10789    let normalized = if expand {
10790        expand_host_access_paths(&paths)
10791    } else {
10792        dedupe_host_paths(&paths)
10793    };
10794    let serialized = normalized
10795        .iter()
10796        .map(|path| path.to_string_lossy().into_owned())
10797        .collect::<Vec<_>>();
10798    if let Ok(serialized) = serde_json::to_string(&serialized) {
10799        env.insert(key.to_owned(), serialized);
10800    }
10801}
10802
10803// discover_command_guest_paths moved to crate::bootstrap
10804
10805fn is_path_like_specifier(specifier: &str) -> bool {
10806    specifier.starts_with('/')
10807        || specifier.starts_with("./")
10808        || specifier.starts_with("../")
10809        || specifier.starts_with("file:")
10810}
10811
10812fn execution_wasm_permission_tier(tier: WasmPermissionTier) -> ExecutionWasmPermissionTier {
10813    match tier {
10814        WasmPermissionTier::Full => ExecutionWasmPermissionTier::Full,
10815        WasmPermissionTier::ReadWrite => ExecutionWasmPermissionTier::ReadWrite,
10816        WasmPermissionTier::ReadOnly => ExecutionWasmPermissionTier::ReadOnly,
10817        WasmPermissionTier::Isolated => ExecutionWasmPermissionTier::Isolated,
10818    }
10819}
10820
10821fn resolve_wasm_permission_tier(
10822    vm: &VmState,
10823    command_name: Option<&str>,
10824    explicit_tier: Option<WasmPermissionTier>,
10825    entrypoint: &str,
10826) -> WasmPermissionTier {
10827    explicit_tier
10828        .or_else(|| command_name.and_then(|command| vm.command_permissions.get(command).copied()))
10829        .or_else(|| {
10830            Path::new(entrypoint)
10831                .file_name()
10832                .and_then(|name| name.to_str())
10833                .and_then(|command| vm.command_permissions.get(command).copied())
10834        })
10835        .unwrap_or(WasmPermissionTier::Full)
10836}
10837
10838fn tokenize_shell_free_command(command: &str) -> Vec<String> {
10839    command
10840        .split_whitespace()
10841        .filter(|segment| !segment.is_empty())
10842        .map(str::to_owned)
10843        .collect()
10844}
10845
10846fn is_posix_shell_builtin(command: &str) -> bool {
10847    matches!(
10848        command,
10849        "." | ":"
10850            | "break"
10851            | "cd"
10852            | "continue"
10853            | "eval"
10854            | "exec"
10855            | "exit"
10856            | "export"
10857            | "readonly"
10858            | "return"
10859            | "set"
10860            | "shift"
10861            | "times"
10862            | "trap"
10863            | "umask"
10864            | "unset"
10865    )
10866}
10867
10868/// Single-token checks for shell-mode commands whose first word forces a real
10869/// shell even when the command string has no shell metacharacters. This is not
10870/// a parser: env-assignment prefixes (`FOO=bar cmd`) and shell reserved words
10871/// have no meaning outside `sh`, so whitespace-tokenizing them would silently
10872/// run the wrong program.
10873fn shell_first_token_requires_shell(token: &str) -> bool {
10874    token.contains('=') || is_shell_reserved_word(token)
10875}
10876
10877fn is_shell_reserved_word(token: &str) -> bool {
10878    matches!(
10879        token,
10880        "if" | "then"
10881            | "elif"
10882            | "else"
10883            | "fi"
10884            | "for"
10885            | "in"
10886            | "do"
10887            | "done"
10888            | "while"
10889            | "until"
10890            | "case"
10891            | "esac"
10892            | "{"
10893            | "}"
10894            | "!"
10895    )
10896}
10897
10898fn command_requires_shell(command: &str) -> bool {
10899    command.chars().any(|ch| {
10900        matches!(
10901            ch,
10902            '|' | '&'
10903                | ';'
10904                | '<'
10905                | '>'
10906                | '('
10907                | ')'
10908                | '$'
10909                | '`'
10910                | '*'
10911                | '?'
10912                | '['
10913                | ']'
10914                | '{'
10915                | '}'
10916                | '~'
10917                | '\''
10918                | '"'
10919                | '\\'
10920                | '\n'
10921        )
10922    })
10923}
10924
10925fn host_mount_path_for_guest_path(vm: &VmState, guest_path: &str) -> Option<PathBuf> {
10926    let normalized = normalize_path(guest_path);
10927
10928    let mut mounts = vm
10929        .configuration
10930        .mounts
10931        .iter()
10932        .filter_map(|mount| {
10933            ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
10934                .then(|| {
10935                    mount_config_host_path(&mount.plugin.config)
10936                        .map(|host_path| (mount.guest_path.as_str(), host_path))
10937                })
10938                .flatten()
10939        })
10940        .collect::<Vec<_>>();
10941    mounts.sort_by_key(|mount| std::cmp::Reverse(mount.0.len()));
10942
10943    for (guest_root, host_root) in mounts {
10944        if normalized != guest_root && !normalized.starts_with(&format!("{guest_root}/")) {
10945            continue;
10946        }
10947
10948        let suffix = normalized
10949            .strip_prefix(guest_root)
10950            .unwrap_or_default()
10951            .trim_start_matches('/');
10952        let mut path = PathBuf::from(host_root);
10953        if !suffix.is_empty() {
10954            path.push(suffix);
10955        }
10956        return Some(path);
10957    }
10958
10959    None
10960}
10961
10962fn host_runtime_path_for_guest_path_with_env(
10963    vm: &VmState,
10964    runtime_env: &BTreeMap<String, String>,
10965    guest_path: &str,
10966    default_host_cwd: &Path,
10967) -> Option<PathBuf> {
10968    if let Some(path) = host_mount_path_for_guest_path(vm, guest_path) {
10969        return Some(path);
10970    }
10971    if let Some(path) = host_path_from_runtime_guest_mappings(runtime_env, guest_path) {
10972        return Some(path);
10973    }
10974
10975    let normalized = normalize_path(guest_path);
10976    let virtual_home = runtime_env
10977        .get("AGENT_OS_VIRTUAL_OS_HOMEDIR")
10978        .or_else(|| vm.guest_env.get("AGENT_OS_VIRTUAL_OS_HOMEDIR"))
10979        .filter(|value| value.starts_with('/'))
10980        .cloned()
10981        .unwrap_or_else(|| String::from("/root"));
10982
10983    if normalized == virtual_home || normalized.starts_with(&format!("{virtual_home}/")) {
10984        let suffix = normalized
10985            .strip_prefix(&virtual_home)
10986            .unwrap_or_default()
10987            .trim_start_matches('/');
10988        let mut host_path = default_host_cwd.to_path_buf();
10989        if !suffix.is_empty() {
10990            host_path.push(suffix);
10991        }
10992        return Some(host_path);
10993    }
10994
10995    None
10996}
10997
10998#[derive(Deserialize, Serialize)]
10999struct RuntimeGuestPathMapping {
11000    #[serde(rename = "guestPath")]
11001    guest_path: String,
11002    #[serde(rename = "hostPath")]
11003    host_path: String,
11004    #[serde(rename = "readOnly", default)]
11005    read_only: bool,
11006}
11007
11008pub(crate) fn host_path_from_runtime_guest_mappings(
11009    runtime_env: &BTreeMap<String, String>,
11010    guest_path: &str,
11011) -> Option<PathBuf> {
11012    let mappings = runtime_env
11013        .get("AGENT_OS_GUEST_PATH_MAPPINGS")
11014        .and_then(|value| serde_json::from_str::<Vec<RuntimeGuestPathMapping>>(value).ok())?;
11015    let normalized = normalize_path(guest_path);
11016
11017    let mut sorted_mappings = mappings
11018        .into_iter()
11019        .filter_map(|mapping| {
11020            (!mapping.guest_path.is_empty() && !mapping.host_path.is_empty()).then_some((
11021                normalize_path(&mapping.guest_path),
11022                PathBuf::from(mapping.host_path),
11023            ))
11024        })
11025        .collect::<Vec<_>>();
11026    sorted_mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.0.len()));
11027
11028    for (guest_root, mut host_root) in sorted_mappings {
11029        if guest_root != "/"
11030            && normalized != guest_root
11031            && !normalized.starts_with(&format!("{guest_root}/"))
11032        {
11033            continue;
11034        }
11035        if guest_root == "/" && !normalized.starts_with('/') {
11036            continue;
11037        }
11038
11039        if host_root.is_relative() {
11040            host_root = std::env::current_dir().ok()?.join(host_root);
11041        }
11042
11043        let suffix = if guest_root == "/" {
11044            normalized.trim_start_matches('/')
11045        } else {
11046            normalized
11047                .strip_prefix(&guest_root)
11048                .unwrap_or_default()
11049                .trim_start_matches('/')
11050        };
11051        if !suffix.is_empty() {
11052            host_root.push(suffix);
11053        }
11054        return Some(host_root);
11055    }
11056
11057    None
11058}
11059
11060fn guest_runtime_path_for_host_path(
11061    runtime_env: &BTreeMap<String, String>,
11062    cwd: &Path,
11063    host_path: &str,
11064) -> Option<String> {
11065    let resolved = if host_path.starts_with("file://") {
11066        PathBuf::from(host_path.trim_start_matches("file://"))
11067    } else if host_path.starts_with("file:") {
11068        PathBuf::from(host_path.trim_start_matches("file:"))
11069    } else {
11070        let candidate = PathBuf::from(host_path);
11071        if candidate.is_absolute() {
11072            candidate
11073        } else if host_path.starts_with("./") || host_path.starts_with("../") {
11074            cwd.join(candidate)
11075        } else {
11076            return None;
11077        }
11078    };
11079    let normalized = normalize_host_path(&resolved);
11080
11081    if let Some(path) = guest_path_from_runtime_host_mappings(runtime_env, &normalized) {
11082        return Some(path);
11083    }
11084
11085    let normalized_cwd = normalize_host_path(cwd);
11086    if !path_is_within_root(&normalized, &normalized_cwd) {
11087        return None;
11088    }
11089
11090    let virtual_home = runtime_env
11091        .get("AGENT_OS_VIRTUAL_OS_HOMEDIR")
11092        .filter(|value| value.starts_with('/'))
11093        .cloned()
11094        .unwrap_or_else(|| String::from("/root"));
11095    let suffix = normalized
11096        .strip_prefix(&normalized_cwd)
11097        .ok()?
11098        .to_string_lossy()
11099        .replace('\\', "/")
11100        .trim_start_matches('/')
11101        .to_owned();
11102
11103    Some(if suffix.is_empty() {
11104        virtual_home
11105    } else {
11106        normalize_path(&format!("{virtual_home}/{suffix}"))
11107    })
11108}
11109
11110fn guest_path_from_runtime_host_mappings(
11111    runtime_env: &BTreeMap<String, String>,
11112    host_path: &Path,
11113) -> Option<String> {
11114    let mappings = runtime_env
11115        .get("AGENT_OS_GUEST_PATH_MAPPINGS")
11116        .and_then(|value| serde_json::from_str::<Vec<RuntimeGuestPathMapping>>(value).ok())?;
11117    let normalized = normalize_host_path(host_path);
11118
11119    let mut sorted_mappings = mappings
11120        .into_iter()
11121        .filter_map(|mapping| {
11122            (!mapping.guest_path.is_empty() && !mapping.host_path.is_empty()).then_some((
11123                normalize_path(&mapping.guest_path),
11124                normalize_host_path(Path::new(&mapping.host_path)),
11125            ))
11126        })
11127        .collect::<Vec<_>>();
11128    sorted_mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.1.as_os_str().len()));
11129
11130    for (guest_root, host_root) in sorted_mappings {
11131        if !path_is_within_root(&normalized, &host_root) {
11132            continue;
11133        }
11134        let suffix = normalized
11135            .strip_prefix(&host_root)
11136            .ok()?
11137            .to_string_lossy()
11138            .replace('\\', "/")
11139            .trim_start_matches('/')
11140            .to_owned();
11141
11142        return Some(if suffix.is_empty() {
11143            guest_root
11144        } else if guest_root == "/" {
11145            normalize_path(&format!("/{suffix}"))
11146        } else {
11147            normalize_path(&format!("{guest_root}/{suffix}"))
11148        });
11149    }
11150
11151    None
11152}
11153
11154fn host_mount_path_for_guest_path_from_mounts(
11155    mounts: &[crate::protocol::MountDescriptor],
11156    guest_path: &str,
11157) -> Option<PathBuf> {
11158    let normalized = normalize_path(guest_path);
11159
11160    let mut host_mounts = mounts
11161        .iter()
11162        .filter_map(|mount| {
11163            ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
11164                .then(|| {
11165                    mount_config_host_path(&mount.plugin.config)
11166                        .map(|host_path| (mount.guest_path.as_str(), host_path))
11167                })
11168                .flatten()
11169        })
11170        .collect::<Vec<_>>();
11171    host_mounts.sort_by_key(|mount| std::cmp::Reverse(mount.0.len()));
11172
11173    for (guest_root, host_root) in host_mounts {
11174        if normalized != guest_root && !normalized.starts_with(&format!("{guest_root}/")) {
11175            continue;
11176        }
11177
11178        let suffix = normalized
11179            .strip_prefix(guest_root)
11180            .unwrap_or_default()
11181            .trim_start_matches('/');
11182        let mut path = PathBuf::from(host_root);
11183        if !suffix.is_empty() {
11184            path.push(suffix);
11185        }
11186        return Some(path);
11187    }
11188
11189    None
11190}
11191
11192#[cfg(test)]
11193mod host_mount_path_for_guest_path_from_mounts_tests {
11194    use super::host_mount_path_for_guest_path_from_mounts;
11195    use crate::protocol::{MountDescriptor, MountPluginDescriptor};
11196    use serde_json::json;
11197    use std::path::PathBuf;
11198
11199    #[test]
11200    fn resolves_module_access_mount_paths() {
11201        let mounts = vec![MountDescriptor {
11202            guest_path: String::from("/root/node_modules"),
11203            read_only: true,
11204            plugin: MountPluginDescriptor {
11205                id: String::from("module_access"),
11206                config: json!({
11207                    "hostPath": "/tmp/workspace/node_modules",
11208                })
11209                .to_string(),
11210            },
11211        }];
11212
11213        let resolved =
11214            host_mount_path_for_guest_path_from_mounts(&mounts, "/root/node_modules/pkg/index.js")
11215                .expect("module_access mount should resolve");
11216
11217        assert_eq!(
11218            resolved,
11219            PathBuf::from("/tmp/workspace/node_modules/pkg/index.js")
11220        );
11221    }
11222}
11223
11224fn resolve_guest_socket_host_path(
11225    context: &JavascriptSocketPathContext,
11226    guest_path: &str,
11227) -> PathBuf {
11228    if let Some(path) = host_mount_path_for_guest_path_from_mounts(&context.mounts, guest_path) {
11229        return path;
11230    }
11231
11232    let normalized = normalize_path(guest_path);
11233    let mut host_path = context.sandbox_root.clone();
11234    let suffix = normalized.trim_start_matches('/');
11235    if !suffix.is_empty() {
11236        host_path.push(suffix);
11237    }
11238    host_path
11239}
11240
11241fn ensure_kernel_parent_directories(
11242    kernel: &mut SidecarKernel,
11243    path: &str,
11244) -> Result<(), SidecarError> {
11245    let parent = dirname(path);
11246    if parent != "/" && !kernel.exists(&parent).map_err(kernel_error)? {
11247        kernel.mkdir(&parent, true).map_err(kernel_error)?;
11248    }
11249    Ok(())
11250}
11251
11252// JavascriptChildProcessSpawnOptions, JavascriptChildProcessSpawnRequest moved to crate::protocol
11253// ResolvedChildProcessExecution moved to crate::state
11254
11255pub(crate) fn sanitize_javascript_child_process_internal_bootstrap_env(
11256    env: &BTreeMap<String, String>,
11257) -> BTreeMap<String, String> {
11258    const ALLOWED_KEYS: &[&str] = &[
11259        "AGENT_OS_ALLOWED_NODE_BUILTINS",
11260        "AGENT_OS_GUEST_PATH_MAPPINGS",
11261        "AGENT_OS_LOOPBACK_EXEMPT_PORTS",
11262        "AGENT_OS_VIRTUAL_PROCESS_EXEC_PATH",
11263        "AGENT_OS_VIRTUAL_PROCESS_UID",
11264        "AGENT_OS_VIRTUAL_PROCESS_GID",
11265        "AGENT_OS_VIRTUAL_PROCESS_VERSION",
11266    ];
11267
11268    env.iter()
11269        .filter(|(key, _)| {
11270            ALLOWED_KEYS.contains(&key.as_str()) || key.starts_with("AGENT_OS_VIRTUAL_OS_")
11271        })
11272        .map(|(key, value)| (key.clone(), value.clone()))
11273        .collect()
11274}
11275
11276// Network request types moved to crate::protocol
11277
11278// VmDnsConfig, DnsResolutionSource moved to crate::state
11279
11280fn resolve_tcp_bind_addr(host: &str, port: u16) -> Result<SocketAddr, SidecarError> {
11281    (host, port)
11282        .to_socket_addrs()
11283        .map_err(sidecar_net_error)?
11284        .next()
11285        .ok_or_else(|| {
11286            SidecarError::Execution(format!("failed to resolve TCP bind address {host}:{port}"))
11287        })
11288}
11289
11290pub(crate) fn format_dns_resource(hostname: &str) -> String {
11291    format!("dns://{hostname}")
11292}
11293
11294pub(crate) fn format_tcp_resource(host: &str, port: u16) -> String {
11295    format!("tcp://{host}:{port}")
11296}
11297
11298fn is_loopback_ip(ip: IpAddr) -> bool {
11299    match ip {
11300        IpAddr::V4(ip) => ip.is_loopback(),
11301        IpAddr::V6(ip) => {
11302            ip.is_loopback()
11303                || ip
11304                    .to_ipv4_mapped()
11305                    .is_some_and(|mapped| mapped.is_loopback())
11306        }
11307    }
11308}
11309
11310fn loopback_cidr(ip: IpAddr) -> &'static str {
11311    match ip {
11312        IpAddr::V4(ip) if ip.is_loopback() => "127.0.0.0/8",
11313        IpAddr::V6(ip)
11314            if ip
11315                .to_ipv4_mapped()
11316                .is_some_and(|mapped| mapped.is_loopback()) =>
11317        {
11318            "127.0.0.0/8"
11319        }
11320        IpAddr::V6(_) => "::1/128",
11321        IpAddr::V4(_) => "127.0.0.0/8",
11322    }
11323}
11324
11325/// Returns the embedded IPv4 address of an IPv4-compatible IPv6 address
11326/// (`::a.b.c.d`): the first six 16-bit segments are zero and the final 32 bits
11327/// hold the IPv4 address. The all-zero (`::`) and loopback (`::1`) addresses are
11328/// deliberately excluded so they are handled by the unspecified/loopback paths
11329/// rather than treated as IPv4-compatible.
11330fn ipv4_compatible_embedded(ip: Ipv6Addr) -> Option<Ipv4Addr> {
11331    let segments = ip.segments();
11332    if segments[0..6].iter().any(|&s| s != 0) {
11333        return None;
11334    }
11335    let embedded = (u32::from(segments[6]) << 16) | u32::from(segments[7]);
11336    // Skip :: (0.0.0.0) and ::1 (0.0.0.1) — these are the IPv6 unspecified /
11337    // loopback addresses, not IPv4-compatible representations of an IPv4 host.
11338    if embedded == 0 || embedded == 1 {
11339        return None;
11340    }
11341    Some(Ipv4Addr::from(embedded))
11342}
11343
11344fn restricted_non_loopback_ip_range(ip: IpAddr) -> Option<(&'static str, &'static str)> {
11345    match ip {
11346        IpAddr::V4(ip) => {
11347            if ip.is_unspecified() {
11348                // 0.0.0.0 is unspecified; the host stack routes a connect() to
11349                // it back to 127.0.0.1, so it must not bypass the loopback gate.
11350                return Some(("0.0.0.0/32", "unspecified"));
11351            }
11352            let [first, second, ..] = ip.octets();
11353            match (first, second) {
11354                (10, _) => Some(("10.0.0.0/8", "private")),
11355                (100, 64..=127) => Some(("100.64.0.0/10", "carrier-grade-nat")),
11356                (172, 16..=31) => Some(("172.16.0.0/12", "private")),
11357                (192, 168) => Some(("192.168.0.0/16", "private")),
11358                (169, 254) => Some(("169.254.0.0/16", "link-local")),
11359                // 224.0.0.0/4 is the IPv4 multicast range and 240.0.0.0/4 is
11360                // reserved/future-use (255.255.255.255 broadcast falls in it).
11361                // Neither is a legitimate unicast egress target, so a guest
11362                // connect to them must be denied rather than attempted.
11363                (224..=239, _) => Some(("224.0.0.0/4", "multicast")),
11364                (240..=255, _) => Some(("240.0.0.0/4", "reserved")),
11365                _ => None,
11366            }
11367        }
11368        IpAddr::V6(ip) => {
11369            if let Some(mapped) = ip.to_ipv4_mapped() {
11370                return restricted_non_loopback_ip_range(IpAddr::V4(mapped));
11371            }
11372            // IPv4-compatible IPv6 (::a.b.c.d): the first six segments are zero
11373            // and the last two carry an embedded IPv4 address. `to_ipv4_mapped`
11374            // returns None for this form, so without canonicalizing it here a
11375            // guest could spell a restricted IPv4 target (e.g. cloud-metadata
11376            // ::169.254.169.254) and bypass the IPv4 classifier. `::`/`::1` are
11377            // excluded so they fall through to the unspecified/loopback paths.
11378            if let Some(compat) = ipv4_compatible_embedded(ip) {
11379                return restricted_non_loopback_ip_range(IpAddr::V4(compat));
11380            }
11381
11382            if ip.is_unspecified() {
11383                // :: is the IPv6 unspecified address; same routing hazard as
11384                // 0.0.0.0, so deny it rather than letting it reach the host.
11385                return Some(("::/128", "unspecified"));
11386            }
11387
11388            let segments = ip.segments();
11389            if (segments[0] & 0xfe00) == 0xfc00 {
11390                return Some(("fc00::/7", "unique-local"));
11391            }
11392            if (segments[0] & 0xffc0) == 0xfe80 {
11393                return Some(("fe80::/10", "link-local"));
11394            }
11395            None
11396        }
11397    }
11398}
11399
11400fn blocked_dns_resolution_error(
11401    resource: &str,
11402    ip: IpAddr,
11403    cidr: &str,
11404    label: &str,
11405) -> SidecarError {
11406    SidecarError::Execution(format!(
11407        "EACCES: blocked outbound network access to {resource}: {ip} is within restricted {label} range {cidr}"
11408    ))
11409}
11410
11411fn blocked_loopback_connect_error(resource: &str, ip: IpAddr, port: u16) -> SidecarError {
11412    SidecarError::Execution(format!(
11413        "EACCES: blocked outbound network access to {resource}: {ip} is loopback ({}) and port {port} is not owned by this VM and is not listed in {LOOPBACK_EXEMPT_PORTS_ENV}",
11414        loopback_cidr(ip)
11415    ))
11416}
11417
11418fn filter_dns_safe_ip_addrs(
11419    addresses: Vec<IpAddr>,
11420    hostname: &str,
11421) -> Result<Vec<IpAddr>, SidecarError> {
11422    let resource = format_dns_resource(hostname);
11423    let mut allowed = Vec::new();
11424    let mut blocked = None;
11425
11426    for ip in addresses {
11427        if let Some((cidr, label)) = restricted_non_loopback_ip_range(ip) {
11428            blocked.get_or_insert((ip, cidr, label));
11429            continue;
11430        }
11431        allowed.push(ip);
11432    }
11433
11434    if allowed.is_empty() {
11435        let (ip, cidr, label) = blocked.expect("blocked DNS results should capture a reason");
11436        return Err(blocked_dns_resolution_error(&resource, ip, cidr, label));
11437    }
11438
11439    Ok(allowed)
11440}
11441
11442fn loopback_connect_allowed(context: &JavascriptSocketPathContext, port: u16) -> bool {
11443    context.loopback_port_allowed(port)
11444}
11445
11446fn filter_tcp_connect_ip_addrs(
11447    addresses: Vec<IpAddr>,
11448    host: &str,
11449    port: u16,
11450    context: &JavascriptSocketPathContext,
11451) -> Result<Vec<IpAddr>, SidecarError> {
11452    let resource = format_tcp_resource(host, port);
11453    let mut allowed = Vec::new();
11454    let mut blocked = None;
11455
11456    for ip in addresses {
11457        if let Some((cidr, label)) = restricted_non_loopback_ip_range(ip) {
11458            blocked.get_or_insert_with(|| blocked_dns_resolution_error(&resource, ip, cidr, label));
11459            continue;
11460        }
11461        if is_loopback_ip(ip) && !loopback_connect_allowed(context, port) {
11462            blocked.get_or_insert_with(|| blocked_loopback_connect_error(&resource, ip, port));
11463            continue;
11464        }
11465        allowed.push(ip);
11466    }
11467
11468    if allowed.is_empty() {
11469        return Err(blocked.expect("blocked TCP connect results should capture a reason"));
11470    }
11471
11472    Ok(allowed)
11473}
11474
11475fn resolve_tcp_connect_addr<B>(
11476    bridge: &SharedBridge<B>,
11477    kernel: &SidecarKernel,
11478    vm_id: &str,
11479    dns: &VmDnsConfig,
11480    host: &str,
11481    port: u16,
11482    context: &JavascriptSocketPathContext,
11483) -> Result<ResolvedTcpConnectAddr, SidecarError>
11484where
11485    B: NativeSidecarBridge + Send + 'static,
11486    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11487{
11488    let allowed = filter_tcp_connect_ip_addrs(
11489        resolve_dns_ip_addrs(
11490            bridge,
11491            kernel,
11492            vm_id,
11493            dns,
11494            host,
11495            DnsLookupPolicy::SkipPermissions,
11496        )?,
11497        host,
11498        port,
11499        context,
11500    )?;
11501    let ip = allowed
11502        .iter()
11503        .copied()
11504        .find(|candidate| {
11505            let family = JavascriptSocketFamily::from_ip(*candidate);
11506            context.translate_tcp_loopback_port(family, port).is_some()
11507        })
11508        // We do not implement Happy Eyeballs yet, so prefer IPv4 over a
11509        // verbatim IPv6-first DNS answer for general outbound TCP connects.
11510        .or_else(|| allowed.iter().copied().find(IpAddr::is_ipv4))
11511        .or_else(|| allowed.first().copied())
11512        .ok_or_else(|| {
11513            SidecarError::Execution(format!("failed to resolve TCP address {host}:{port}"))
11514        })?;
11515    let family = JavascriptSocketFamily::from_ip(ip);
11516    let use_kernel_loopback =
11517        is_loopback_ip(ip) && context.translate_tcp_loopback_port(family, port).is_some();
11518    let actual_port = if is_loopback_ip(ip) {
11519        context
11520            .translate_tcp_loopback_port(family, port)
11521            .unwrap_or(port)
11522    } else {
11523        port
11524    };
11525    Ok(ResolvedTcpConnectAddr {
11526        actual_addr: SocketAddr::new(ip, actual_port),
11527        guest_remote_addr: SocketAddr::new(ip, port),
11528        use_kernel_loopback,
11529    })
11530}
11531
11532fn resolve_dns_ip_addrs<B>(
11533    bridge: &SharedBridge<B>,
11534    kernel: &SidecarKernel,
11535    vm_id: &str,
11536    dns: &VmDnsConfig,
11537    hostname: &str,
11538    policy: DnsLookupPolicy,
11539) -> Result<Vec<IpAddr>, SidecarError>
11540where
11541    B: NativeSidecarBridge + Send + 'static,
11542    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11543{
11544    let resolution = match kernel.resolve_dns(hostname, policy) {
11545        Ok(resolution) => resolution,
11546        Err(error) => {
11547            let sidecar_error = kernel_error(error.clone());
11548            if error.code() != "EACCES" {
11549                emit_dns_resolution_failure_event(bridge, vm_id, hostname, dns, &sidecar_error);
11550            }
11551            return Err(sidecar_error);
11552        }
11553    };
11554    emit_dns_resolution_event(
11555        bridge,
11556        vm_id,
11557        hostname,
11558        resolution.source(),
11559        resolution.addresses(),
11560        dns,
11561    );
11562    Ok(resolution.addresses().to_vec())
11563}
11564
11565fn resolve_dns_records<B>(
11566    bridge: &SharedBridge<B>,
11567    kernel: &SidecarKernel,
11568    vm_id: &str,
11569    dns: &VmDnsConfig,
11570    hostname: &str,
11571    record_type: RecordType,
11572    policy: DnsLookupPolicy,
11573) -> Result<DnsRecordResolution, SidecarError>
11574where
11575    B: NativeSidecarBridge + Send + 'static,
11576    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11577{
11578    let resolution = match kernel.resolve_dns_records(hostname, record_type, policy) {
11579        Ok(resolution) => resolution,
11580        Err(error) => {
11581            let sidecar_error = kernel_error(error.clone());
11582            if error.code() != "EACCES" {
11583                emit_dns_resolution_failure_event(bridge, vm_id, hostname, dns, &sidecar_error);
11584            }
11585            return Err(sidecar_error);
11586        }
11587    };
11588    emit_dns_record_resolution_event(bridge, vm_id, hostname, &resolution, dns);
11589    Ok(resolution)
11590}
11591
11592fn filter_dns_ip_addrs(
11593    addresses: Vec<IpAddr>,
11594    family: Option<u8>,
11595) -> Result<Vec<IpAddr>, SidecarError> {
11596    let filtered: Vec<_> = match family.unwrap_or(0) {
11597        0 => addresses,
11598        4 => addresses
11599            .into_iter()
11600            .filter(|ip| matches!(ip, IpAddr::V4(_)))
11601            .collect(),
11602        6 => addresses
11603            .into_iter()
11604            .filter(|ip| matches!(ip, IpAddr::V6(_)))
11605            .collect(),
11606        other => {
11607            return Err(SidecarError::InvalidState(format!(
11608                "unsupported dns family {other}"
11609            )));
11610        }
11611    };
11612
11613    if filtered.is_empty() {
11614        return Err(SidecarError::Execution(String::from(
11615            "failed to resolve DNS address for requested family",
11616        )));
11617    }
11618
11619    Ok(filtered)
11620}
11621
11622fn resolve_udp_bind_addr(
11623    host: &str,
11624    port: u16,
11625    family: JavascriptUdpFamily,
11626) -> Result<SocketAddr, SidecarError> {
11627    (host, port)
11628        .to_socket_addrs()
11629        .map_err(sidecar_net_error)?
11630        .find(|addr| family.matches_addr(addr))
11631        .ok_or_else(|| {
11632            SidecarError::Execution(format!(
11633                "failed to resolve {} UDP bind address {host}:{port}",
11634                family.socket_type()
11635            ))
11636        })
11637}
11638
11639fn resolve_udp_addr<B>(request: UdpRemoteAddrRequest<'_, B>) -> Result<SocketAddr, SidecarError>
11640where
11641    B: NativeSidecarBridge + Send + 'static,
11642    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11643{
11644    let UdpRemoteAddrRequest {
11645        bridge,
11646        kernel,
11647        vm_id,
11648        dns,
11649        host,
11650        port,
11651        family,
11652        context,
11653    } = request;
11654    resolve_dns_ip_addrs(
11655        bridge,
11656        kernel,
11657        vm_id,
11658        dns,
11659        host,
11660        DnsLookupPolicy::SkipPermissions,
11661    )?
11662    .into_iter()
11663    .map(|ip| {
11664        let family_key = JavascriptSocketFamily::from_ip(ip);
11665        let actual_port = if is_loopback_ip(ip) {
11666            context
11667                .translate_udp_loopback_port(family_key, port)
11668                .unwrap_or(port)
11669        } else {
11670            port
11671        };
11672        SocketAddr::new(ip, actual_port)
11673    })
11674    .find(|addr| family.matches_addr(addr))
11675    .ok_or_else(|| {
11676        SidecarError::Execution(format!(
11677            "failed to resolve {} UDP address {host}:{port}",
11678            family.socket_type()
11679        ))
11680    })
11681}
11682
11683fn socket_addr_family(addr: &SocketAddr) -> &'static str {
11684    match addr {
11685        SocketAddr::V4(_) => "IPv4",
11686        SocketAddr::V6(_) => "IPv6",
11687    }
11688}
11689
11690fn javascript_net_timeout_value() -> Value {
11691    Value::String(String::from(JAVASCRIPT_NET_TIMEOUT_SENTINEL))
11692}
11693
11694fn javascript_net_json_string(value: Value, label: &str) -> Result<Value, SidecarError> {
11695    serde_json::to_string(&value)
11696        .map(Value::String)
11697        .map_err(|error| {
11698            SidecarError::InvalidState(format!("failed to serialize {label} payload: {error}"))
11699        })
11700}
11701
11702fn javascript_net_read_value(
11703    event: Option<JavascriptTcpSocketEvent>,
11704) -> Result<Value, SidecarError> {
11705    match event {
11706        Some(JavascriptTcpSocketEvent::Data(chunk)) => Ok(Value::String(
11707            base64::engine::general_purpose::STANDARD.encode(chunk),
11708        )),
11709        Some(JavascriptTcpSocketEvent::End | JavascriptTcpSocketEvent::Close { .. }) => {
11710            Ok(Value::Null)
11711        }
11712        Some(JavascriptTcpSocketEvent::Error { code, message }) => {
11713            let detail = code.unwrap_or_else(|| String::from("socket read"));
11714            Err(SidecarError::Execution(format!("{detail}: {message}")))
11715        }
11716        None => Ok(javascript_net_timeout_value()),
11717    }
11718}
11719
11720fn io_error_code(error: &std::io::Error) -> Option<String> {
11721    match error.raw_os_error() {
11722        Some(libc::EADDRINUSE) => Some(String::from("EADDRINUSE")),
11723        Some(libc::EADDRNOTAVAIL) => Some(String::from("EADDRNOTAVAIL")),
11724        Some(libc::ECONNREFUSED) => Some(String::from("ECONNREFUSED")),
11725        Some(libc::ECONNRESET) => Some(String::from("ECONNRESET")),
11726        Some(libc::EINVAL) => Some(String::from("EINVAL")),
11727        Some(libc::EPIPE) => Some(String::from("EPIPE")),
11728        Some(libc::ETIMEDOUT) => Some(String::from("ETIMEDOUT")),
11729        Some(libc::EHOSTUNREACH) => Some(String::from("EHOSTUNREACH")),
11730        Some(libc::ENETUNREACH) => Some(String::from("ENETUNREACH")),
11731        _ => None,
11732    }
11733}
11734
11735fn sidecar_net_error(error: std::io::Error) -> SidecarError {
11736    let message = match io_error_code(&error) {
11737        Some(code) => format!("{code}: {error}"),
11738        None => error.to_string(),
11739    };
11740    SidecarError::Execution(message)
11741}
11742
11743fn tls_provider() -> Arc<rustls::crypto::CryptoProvider> {
11744    Arc::new(aws_lc_rs::default_provider())
11745}
11746
11747fn tls_local_certificates(
11748    options: &JavascriptTlsBridgeOptions,
11749) -> Result<Vec<Vec<u8>>, SidecarError> {
11750    let Some(certificates) = options.cert.as_ref() else {
11751        return Ok(Vec::new());
11752    };
11753    tls_material_entries(certificates)
11754}
11755
11756fn tls_material_entries(material: &JavascriptTlsMaterial) -> Result<Vec<Vec<u8>>, SidecarError> {
11757    match material {
11758        JavascriptTlsMaterial::Single(entry) => tls_data_value(entry).map(|value| vec![value]),
11759        JavascriptTlsMaterial::Many(entries) => entries.iter().map(tls_data_value).collect(),
11760    }
11761}
11762
11763fn tls_data_value(value: &JavascriptTlsDataValue) -> Result<Vec<u8>, SidecarError> {
11764    match value {
11765        JavascriptTlsDataValue::Buffer { data } => base64::engine::general_purpose::STANDARD
11766            .decode(data)
11767            .map_err(|error| {
11768                SidecarError::InvalidState(format!("TLS material contains invalid base64: {error}"))
11769            }),
11770        JavascriptTlsDataValue::String { data } => Ok(data.as_bytes().to_vec()),
11771    }
11772}
11773
11774fn tls_certificates_from_material(
11775    material: &JavascriptTlsMaterial,
11776) -> Result<Vec<CertificateDer<'static>>, SidecarError> {
11777    let mut certificates = Vec::new();
11778    for entry in tls_material_entries(material)? {
11779        let mut reader = std::io::BufReader::new(Cursor::new(entry.clone()));
11780        let parsed = rustls_pemfile::certs(&mut reader)
11781            .collect::<Result<Vec<_>, _>>()
11782            .map_err(sidecar_net_error)?;
11783        if parsed.is_empty() {
11784            certificates.push(CertificateDer::from(entry));
11785        } else {
11786            certificates.extend(parsed);
11787        }
11788    }
11789    if certificates.is_empty() {
11790        return Err(SidecarError::InvalidState(String::from(
11791            "TLS certificate material did not contain any certificates",
11792        )));
11793    }
11794    Ok(certificates)
11795}
11796
11797fn tls_private_key_from_material(
11798    material: &JavascriptTlsMaterial,
11799) -> Result<PrivateKeyDer<'static>, SidecarError> {
11800    for entry in tls_material_entries(material)? {
11801        let mut reader = std::io::BufReader::new(Cursor::new(entry));
11802        if let Some(key) = rustls_pemfile::private_key(&mut reader).map_err(sidecar_net_error)? {
11803            return Ok(key);
11804        }
11805    }
11806    Err(SidecarError::InvalidState(String::from(
11807        "TLS private key material did not contain a supported key",
11808    )))
11809}
11810
11811fn tls_root_store(options: &JavascriptTlsBridgeOptions) -> Result<RootCertStore, SidecarError> {
11812    let mut roots = RootCertStore::empty();
11813    if let Some(ca) = options.ca.as_ref() {
11814        for certificate in tls_certificates_from_material(ca)? {
11815            roots.add(certificate).map_err(|error| {
11816                SidecarError::InvalidState(format!("failed to add TLS CA certificate: {error}"))
11817            })?;
11818        }
11819        return Ok(roots);
11820    }
11821
11822    for certificate in rustls_native_certs::load_native_certs().certs {
11823        roots.add(certificate).map_err(|error| {
11824            SidecarError::InvalidState(format!(
11825                "failed to add native TLS certificate to root store: {error}"
11826            ))
11827        })?;
11828    }
11829    Ok(roots)
11830}
11831
11832fn build_client_tls_stream(
11833    stream: TcpStream,
11834    options: &JavascriptTlsBridgeOptions,
11835) -> Result<rustls::StreamOwned<ClientConnection, TcpStream>, SidecarError> {
11836    let config = build_client_tls_config(options)?;
11837    let server_name = options
11838        .servername
11839        .clone()
11840        .unwrap_or_else(|| String::from("localhost"));
11841    let server_name = ServerName::try_from(server_name)
11842        .map_err(|_| SidecarError::InvalidState(String::from("invalid TLS servername")))?;
11843    stream
11844        .set_read_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
11845        .map_err(sidecar_net_error)?;
11846    stream
11847        .set_write_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
11848        .map_err(sidecar_net_error)?;
11849    let mut tls_stream = rustls::StreamOwned::new(
11850        ClientConnection::new(Arc::new(config), server_name).map_err(|error| {
11851            SidecarError::Execution(format!("failed to start TLS client: {error}"))
11852        })?,
11853        stream,
11854    );
11855    while tls_stream.conn.is_handshaking() {
11856        tls_stream
11857            .conn
11858            .complete_io(&mut tls_stream.sock)
11859            .map_err(sidecar_net_error)?;
11860    }
11861    tls_stream
11862        .sock
11863        .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
11864        .map_err(sidecar_net_error)?;
11865    tls_stream
11866        .sock
11867        .set_write_timeout(None)
11868        .map_err(sidecar_net_error)?;
11869    Ok(tls_stream)
11870}
11871
11872fn build_client_loopback_tls_stream(
11873    transport: crate::state::LoopbackTlsEndpoint,
11874    options: &JavascriptTlsBridgeOptions,
11875) -> Result<rustls::StreamOwned<ClientConnection, crate::state::LoopbackTlsEndpoint>, SidecarError>
11876{
11877    let config = build_client_tls_config(options)?;
11878    let server_name = options
11879        .servername
11880        .clone()
11881        .unwrap_or_else(|| String::from("localhost"));
11882    let server_name = ServerName::try_from(server_name)
11883        .map_err(|_| SidecarError::InvalidState(String::from("invalid TLS servername")))?;
11884    let mut tls_stream = rustls::StreamOwned::new(
11885        ClientConnection::new(Arc::new(config), server_name).map_err(|error| {
11886            SidecarError::Execution(format!("failed to start TLS client: {error}"))
11887        })?,
11888        transport,
11889    );
11890    match tls_stream.conn.complete_io(&mut tls_stream.sock) {
11891        Ok(_) => {}
11892        Err(error)
11893            if matches!(
11894                error.kind(),
11895                std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
11896            ) => {}
11897        Err(error) => return Err(sidecar_net_error(error)),
11898    }
11899    Ok(tls_stream)
11900}
11901
11902fn build_client_tls_config(
11903    options: &JavascriptTlsBridgeOptions,
11904) -> Result<ClientConfig, SidecarError> {
11905    let provider = tls_provider();
11906    let builder = ClientConfig::builder_with_provider(provider.clone())
11907        .with_safe_default_protocol_versions()
11908        .map_err(|error| {
11909            SidecarError::InvalidState(format!("invalid TLS protocol config: {error}"))
11910        })?;
11911
11912    let mut config = if options.reject_unauthorized == Some(false) {
11913        let verifier = Arc::new(InsecureTlsVerifier {
11914            supported_schemes: provider
11915                .signature_verification_algorithms
11916                .supported_schemes(),
11917        });
11918        builder
11919            .dangerous()
11920            .with_custom_certificate_verifier(verifier)
11921            .with_no_client_auth()
11922    } else {
11923        builder
11924            .with_root_certificates(tls_root_store(options)?)
11925            .with_no_client_auth()
11926    };
11927
11928    if let Some(protocols) = options.alpn_protocols.as_ref() {
11929        config.alpn_protocols = protocols
11930            .iter()
11931            .map(|protocol| protocol.as_bytes().to_vec())
11932            .collect();
11933    }
11934    Ok(config)
11935}
11936
11937fn build_server_tls_stream(
11938    stream: TcpStream,
11939    options: &JavascriptTlsBridgeOptions,
11940) -> Result<rustls::StreamOwned<ServerConnection, TcpStream>, SidecarError> {
11941    let config = build_server_tls_config(options)?;
11942    stream
11943        .set_read_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
11944        .map_err(sidecar_net_error)?;
11945    stream
11946        .set_write_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
11947        .map_err(sidecar_net_error)?;
11948    let mut tls_stream = rustls::StreamOwned::new(
11949        ServerConnection::new(Arc::new(config)).map_err(|error| {
11950            SidecarError::Execution(format!("failed to start TLS server: {error}"))
11951        })?,
11952        stream,
11953    );
11954    while tls_stream.conn.is_handshaking() {
11955        tls_stream
11956            .conn
11957            .complete_io(&mut tls_stream.sock)
11958            .map_err(sidecar_net_error)?;
11959    }
11960    tls_stream
11961        .sock
11962        .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
11963        .map_err(sidecar_net_error)?;
11964    tls_stream
11965        .sock
11966        .set_write_timeout(None)
11967        .map_err(sidecar_net_error)?;
11968    Ok(tls_stream)
11969}
11970
11971fn build_server_loopback_tls_stream(
11972    transport: crate::state::LoopbackTlsEndpoint,
11973    options: &JavascriptTlsBridgeOptions,
11974) -> Result<rustls::StreamOwned<ServerConnection, crate::state::LoopbackTlsEndpoint>, SidecarError>
11975{
11976    let config = build_server_tls_config(options)?;
11977    Ok(rustls::StreamOwned::new(
11978        ServerConnection::new(Arc::new(config)).map_err(|error| {
11979            SidecarError::Execution(format!("failed to start TLS server: {error}"))
11980        })?,
11981        transport,
11982    ))
11983}
11984
11985fn build_server_tls_config(
11986    options: &JavascriptTlsBridgeOptions,
11987) -> Result<ServerConfig, SidecarError> {
11988    let certificates = tls_certificates_from_material(options.cert.as_ref().ok_or_else(|| {
11989        SidecarError::InvalidState(String::from("TLS server upgrade requires a certificate"))
11990    })?)?;
11991    let key = tls_private_key_from_material(options.key.as_ref().ok_or_else(|| {
11992        SidecarError::InvalidState(String::from("TLS server upgrade requires a private key"))
11993    })?)?;
11994
11995    let mut config = ServerConfig::builder_with_provider(tls_provider())
11996        .with_safe_default_protocol_versions()
11997        .map_err(|error| {
11998            SidecarError::InvalidState(format!("invalid TLS protocol config: {error}"))
11999        })?
12000        .with_no_client_auth()
12001        .with_single_cert(certificates, key)
12002        .map_err(|error| {
12003            SidecarError::InvalidState(format!("invalid TLS server config: {error}"))
12004        })?;
12005
12006    if let Some(protocols) = options.alpn_protocols.as_ref() {
12007        config.alpn_protocols = protocols
12008            .iter()
12009            .map(|protocol| protocol.as_bytes().to_vec())
12010            .collect();
12011    }
12012    Ok(config)
12013}
12014
12015fn tls_protocol_name(version: rustls::ProtocolVersion) -> String {
12016    match version {
12017        rustls::ProtocolVersion::TLSv1_2 => String::from("TLSv1.2"),
12018        rustls::ProtocolVersion::TLSv1_3 => String::from("TLSv1.3"),
12019        other => other
12020            .as_str()
12021            .map(str::to_owned)
12022            .unwrap_or_else(|| format!("{other:?}")),
12023    }
12024}
12025
12026fn tls_cipher_bridge_value(suite: rustls::SupportedCipherSuite) -> Value {
12027    tls_bridge_object(vec![
12028        (
12029            "name",
12030            suite
12031                .suite()
12032                .as_str()
12033                .map(|value| Value::String(value.to_owned()))
12034                .unwrap_or(Value::Null),
12035        ),
12036        (
12037            "standardName",
12038            suite
12039                .suite()
12040                .as_str()
12041                .map(|value| Value::String(value.to_owned()))
12042                .unwrap_or(Value::Null),
12043        ),
12044        (
12045            "version",
12046            Value::String(if suite.tls13().is_some() {
12047                String::from("TLSv1.3")
12048            } else {
12049                String::from("TLSv1.2")
12050            }),
12051        ),
12052    ])
12053}
12054
12055fn tls_certificate_bridge_value(certificate: &[u8], detailed: bool) -> Value {
12056    let mut fields = vec![("raw", tls_bridge_buffer_value(certificate))];
12057    if detailed {
12058        fields.push(("issuerCertificate", tls_bridge_undefined_value()));
12059    }
12060    tls_bridge_object(fields)
12061}
12062
12063fn tls_bridge_buffer_value(bytes: &[u8]) -> Value {
12064    json!({
12065        "type": "buffer",
12066        "data": base64::engine::general_purpose::STANDARD.encode(bytes),
12067    })
12068}
12069
12070fn tls_bridge_object(entries: Vec<(&str, Value)>) -> Value {
12071    let value = entries
12072        .into_iter()
12073        .map(|(key, value)| (key.to_owned(), value))
12074        .collect::<serde_json::Map<String, Value>>();
12075    json!({
12076        "type": "object",
12077        "id": 1,
12078        "value": value,
12079    })
12080}
12081
12082fn tls_bridge_undefined_value() -> Value {
12083    json!({
12084        "type": "undefined",
12085    })
12086}
12087
12088fn spawn_tcp_socket_reader(
12089    stream: TcpStream,
12090    sender: Sender<JavascriptTcpSocketEvent>,
12091    tls_mode: Arc<AtomicBool>,
12092    saw_local_shutdown: Arc<AtomicBool>,
12093    saw_remote_end: Arc<AtomicBool>,
12094    close_notified: Arc<AtomicBool>,
12095) {
12096    thread::spawn(move || {
12097        let mut stream = stream;
12098        let mut buffer = vec![0_u8; 64 * 1024];
12099        loop {
12100            if tls_mode.load(Ordering::SeqCst) {
12101                break;
12102            }
12103            match stream.read(&mut buffer) {
12104                Ok(0) => {
12105                    saw_remote_end.store(true, Ordering::SeqCst);
12106                    let _ = sender.send(JavascriptTcpSocketEvent::End);
12107                    if saw_local_shutdown.load(Ordering::SeqCst)
12108                        && !close_notified.swap(true, Ordering::SeqCst)
12109                    {
12110                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12111                    }
12112                    break;
12113                }
12114                Ok(bytes_read) => {
12115                    if sender
12116                        .send(JavascriptTcpSocketEvent::Data(
12117                            buffer[..bytes_read].to_vec(),
12118                        ))
12119                        .is_err()
12120                    {
12121                        break;
12122                    }
12123                }
12124                Err(error)
12125                    if matches!(
12126                        error.kind(),
12127                        std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
12128                    ) =>
12129                {
12130                    continue;
12131                }
12132                Err(error) => {
12133                    let code = io_error_code(&error);
12134                    let _ = sender.send(JavascriptTcpSocketEvent::Error {
12135                        code,
12136                        message: error.to_string(),
12137                    });
12138                    if !close_notified.swap(true, Ordering::SeqCst) {
12139                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12140                    }
12141                    break;
12142                }
12143            }
12144        }
12145    });
12146}
12147
12148fn spawn_tls_socket_reader(
12149    tls_stream: Arc<Mutex<Option<ActiveTlsStream>>>,
12150    sender: Sender<JavascriptTcpSocketEvent>,
12151    saw_local_shutdown: Arc<AtomicBool>,
12152    saw_remote_end: Arc<AtomicBool>,
12153    close_notified: Arc<AtomicBool>,
12154) {
12155    thread::spawn(move || {
12156        let mut buffer = vec![0_u8; 64 * 1024];
12157        loop {
12158            let read_result = {
12159                let mut guard = match tls_stream.lock() {
12160                    Ok(guard) => guard,
12161                    Err(_) => return,
12162                };
12163                let Some(stream) = guard.as_mut() else {
12164                    return;
12165                };
12166                stream.read(&mut buffer)
12167            };
12168
12169            match read_result {
12170                Ok(0) => {
12171                    saw_remote_end.store(true, Ordering::SeqCst);
12172                    let _ = sender.send(JavascriptTcpSocketEvent::End);
12173                    if saw_local_shutdown.load(Ordering::SeqCst)
12174                        && !close_notified.swap(true, Ordering::SeqCst)
12175                    {
12176                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12177                    }
12178                    break;
12179                }
12180                Ok(bytes_read) => {
12181                    if sender
12182                        .send(JavascriptTcpSocketEvent::Data(
12183                            buffer[..bytes_read].to_vec(),
12184                        ))
12185                        .is_err()
12186                    {
12187                        break;
12188                    }
12189                }
12190                Err(error)
12191                    if matches!(
12192                        error.kind(),
12193                        std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
12194                    ) =>
12195                {
12196                    // The TLS reader and writer share one rustls stream mutex. Yield after
12197                    // timed-out reads so request writes can acquire the lock promptly.
12198                    std::thread::sleep(Duration::from_millis(1));
12199                    continue;
12200                }
12201                Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => {
12202                    saw_remote_end.store(true, Ordering::SeqCst);
12203                    let _ = sender.send(JavascriptTcpSocketEvent::End);
12204                    if saw_local_shutdown.load(Ordering::SeqCst)
12205                        && !close_notified.swap(true, Ordering::SeqCst)
12206                    {
12207                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12208                    }
12209                    break;
12210                }
12211                Err(error) => {
12212                    let code = io_error_code(&error);
12213                    let _ = sender.send(JavascriptTcpSocketEvent::Error {
12214                        code,
12215                        message: error.to_string(),
12216                    });
12217                    if !close_notified.swap(true, Ordering::SeqCst) {
12218                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12219                    }
12220                    break;
12221                }
12222            }
12223        }
12224    });
12225}
12226
12227fn spawn_unix_socket_reader(
12228    stream: UnixStream,
12229    sender: Sender<JavascriptTcpSocketEvent>,
12230    saw_local_shutdown: Arc<AtomicBool>,
12231    saw_remote_end: Arc<AtomicBool>,
12232    close_notified: Arc<AtomicBool>,
12233) {
12234    thread::spawn(move || {
12235        let mut stream = stream;
12236        let mut buffer = vec![0_u8; 64 * 1024];
12237        loop {
12238            match stream.read(&mut buffer) {
12239                Ok(0) => {
12240                    saw_remote_end.store(true, Ordering::SeqCst);
12241                    let _ = sender.send(JavascriptTcpSocketEvent::End);
12242                    if saw_local_shutdown.load(Ordering::SeqCst)
12243                        && !close_notified.swap(true, Ordering::SeqCst)
12244                    {
12245                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12246                    }
12247                    break;
12248                }
12249                Ok(bytes_read) => {
12250                    if sender
12251                        .send(JavascriptTcpSocketEvent::Data(
12252                            buffer[..bytes_read].to_vec(),
12253                        ))
12254                        .is_err()
12255                    {
12256                        break;
12257                    }
12258                }
12259                Err(error) => {
12260                    let code = io_error_code(&error);
12261                    let _ = sender.send(JavascriptTcpSocketEvent::Error {
12262                        code,
12263                        message: error.to_string(),
12264                    });
12265                    if !close_notified.swap(true, Ordering::SeqCst) {
12266                        let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12267                    }
12268                    break;
12269                }
12270            }
12271        }
12272    });
12273}
12274
12275fn terminate_child_process_tree(kernel: &mut SidecarKernel, process: &mut ActiveProcess) {
12276    let sqlite_database_ids = process.sqlite_databases.keys().copied().collect::<Vec<_>>();
12277    for database_id in sqlite_database_ids {
12278        let _ = close_sqlite_database(kernel, process, database_id);
12279    }
12280    process.sqlite_statements.clear();
12281    process.http_servers.clear();
12282    process.pending_http_requests.clear();
12283    if let Ok(mut http2) = process.http2.shared.lock() {
12284        let sessions = http2.sessions.values().cloned().collect::<Vec<_>>();
12285        http2.server_events.clear();
12286        http2.session_events.clear();
12287        http2.streams.clear();
12288        http2.servers.clear();
12289        http2.sessions.clear();
12290        drop(http2);
12291        for session in sessions {
12292            let (respond_to, _rx) = mpsc::channel();
12293            let _ = session.command_tx.send(Http2SessionCommand::Close {
12294                abrupt: true,
12295                respond_to,
12296            });
12297        }
12298    }
12299
12300    let listener_ids = process.tcp_listeners.keys().cloned().collect::<Vec<_>>();
12301    for listener_id in listener_ids {
12302        if let Some(listener) = process.tcp_listeners.remove(&listener_id) {
12303            let _ = listener.close(kernel, process.kernel_pid);
12304        }
12305    }
12306
12307    let sockets = process.tcp_sockets.keys().cloned().collect::<Vec<_>>();
12308    for socket_id in sockets {
12309        if let Some(socket) = process.tcp_sockets.remove(&socket_id) {
12310            let _ = socket.close(kernel, process.kernel_pid);
12311        }
12312    }
12313
12314    let unix_listener_ids = process.unix_listeners.keys().cloned().collect::<Vec<_>>();
12315    for listener_id in unix_listener_ids {
12316        if let Some(listener) = process.unix_listeners.remove(&listener_id) {
12317            let _ = listener.close();
12318        }
12319    }
12320
12321    let unix_sockets = process.unix_sockets.keys().cloned().collect::<Vec<_>>();
12322    for socket_id in unix_sockets {
12323        if let Some(socket) = process.unix_sockets.remove(&socket_id) {
12324            let _ = socket.close();
12325        }
12326    }
12327
12328    let udp_socket_ids = process.udp_sockets.keys().cloned().collect::<Vec<_>>();
12329    for socket_id in udp_socket_ids {
12330        if let Some(mut socket) = process.udp_sockets.remove(&socket_id) {
12331            socket.close(kernel, process.kernel_pid);
12332        }
12333    }
12334
12335    let child_ids = process.child_processes.keys().cloned().collect::<Vec<_>>();
12336    for child_id in child_ids {
12337        let Some(mut child) = process.child_processes.remove(&child_id) else {
12338            continue;
12339        };
12340        terminate_child_process_tree(kernel, &mut child);
12341        let _ = kernel.kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, SIGTERM);
12342        let _ = signal_runtime_process(child.execution.child_pid(), SIGTERM);
12343        child.kernel_handle.finish(0);
12344        let _ = kernel.wait_and_reap(child.kernel_pid);
12345    }
12346}
12347
12348fn service_javascript_sqlite_sync_rpc(
12349    kernel: &mut SidecarKernel,
12350    process: &mut ActiveProcess,
12351    request: &JavascriptSyncRpcRequest,
12352) -> Result<Value, SidecarError> {
12353    match request.method.as_str() {
12354        "sqlite.constants" => Ok(json!({})),
12355        "sqlite.open" => sqlite_open_database(kernel, process, request),
12356        "sqlite.close" => {
12357            let database_id =
12358                javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.close database id")?;
12359            close_sqlite_database(kernel, process, database_id)?;
12360            Ok(Value::Null)
12361        }
12362        "sqlite.exec" => sqlite_exec_database(kernel, process, request),
12363        "sqlite.query" => sqlite_query_database(process, request),
12364        "sqlite.prepare" => sqlite_prepare_statement(process, request),
12365        "sqlite.location" => {
12366            let database_id =
12367                javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.location database id")?;
12368            let database = sqlite_database(process, database_id)?;
12369            Ok(database
12370                .vm_path
12371                .as_ref()
12372                .map(|path| Value::String(path.clone()))
12373                .unwrap_or(Value::Null))
12374        }
12375        "sqlite.checkpoint" => {
12376            let database_id =
12377                javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.checkpoint database id")?;
12378            let kernel_pid = process.kernel_pid;
12379            let database = sqlite_database_mut(process, database_id)?;
12380            sqlite_sync_database(kernel, kernel_pid, database)?;
12381            Ok(Value::Null)
12382        }
12383        "sqlite.statement.run" => sqlite_run_statement(kernel, process, request),
12384        "sqlite.statement.get" => sqlite_get_statement(process, request),
12385        "sqlite.statement.all" | "sqlite.statement.iterate" => {
12386            sqlite_all_statement(process, request)
12387        }
12388        "sqlite.statement.columns" => sqlite_statement_columns(process, request),
12389        "sqlite.statement.setReturnArrays" => {
12390            let statement_id = javascript_sync_rpc_arg_u64(
12391                &request.args,
12392                0,
12393                "sqlite.statement.setReturnArrays statement id",
12394            )?;
12395            let enabled = javascript_sync_rpc_arg_bool(
12396                &request.args,
12397                1,
12398                "sqlite.statement.setReturnArrays enabled",
12399            )?;
12400            sqlite_statement_mut(process, statement_id)?.return_arrays = enabled;
12401            Ok(Value::Null)
12402        }
12403        "sqlite.statement.setReadBigInts" => {
12404            let statement_id = javascript_sync_rpc_arg_u64(
12405                &request.args,
12406                0,
12407                "sqlite.statement.setReadBigInts statement id",
12408            )?;
12409            let enabled = javascript_sync_rpc_arg_bool(
12410                &request.args,
12411                1,
12412                "sqlite.statement.setReadBigInts enabled",
12413            )?;
12414            sqlite_statement_mut(process, statement_id)?.read_bigints = enabled;
12415            Ok(Value::Null)
12416        }
12417        "sqlite.statement.setAllowBareNamedParameters" => {
12418            let statement_id = javascript_sync_rpc_arg_u64(
12419                &request.args,
12420                0,
12421                "sqlite.statement.setAllowBareNamedParameters statement id",
12422            )?;
12423            let enabled = javascript_sync_rpc_arg_bool(
12424                &request.args,
12425                1,
12426                "sqlite.statement.setAllowBareNamedParameters enabled",
12427            )?;
12428            sqlite_statement_mut(process, statement_id)?.allow_bare_named_parameters = enabled;
12429            Ok(Value::Null)
12430        }
12431        "sqlite.statement.setAllowUnknownNamedParameters" => {
12432            let statement_id = javascript_sync_rpc_arg_u64(
12433                &request.args,
12434                0,
12435                "sqlite.statement.setAllowUnknownNamedParameters statement id",
12436            )?;
12437            let enabled = javascript_sync_rpc_arg_bool(
12438                &request.args,
12439                1,
12440                "sqlite.statement.setAllowUnknownNamedParameters enabled",
12441            )?;
12442            sqlite_statement_mut(process, statement_id)?.allow_unknown_named_parameters = enabled;
12443            Ok(Value::Null)
12444        }
12445        "sqlite.statement.finalize" => {
12446            let statement_id = javascript_sync_rpc_arg_u64(
12447                &request.args,
12448                0,
12449                "sqlite.statement.finalize statement id",
12450            )?;
12451            process
12452                .sqlite_statements
12453                .remove(&statement_id)
12454                .ok_or_else(|| {
12455                    SidecarError::InvalidState(format!(
12456                        "sqlite statement handle not found: {statement_id}"
12457                    ))
12458                })?;
12459            Ok(Value::Null)
12460        }
12461        other => Err(SidecarError::InvalidState(format!(
12462            "unsupported JavaScript sqlite sync RPC method {other}"
12463        ))),
12464    }
12465}
12466
12467fn sqlite_open_database(
12468    kernel: &mut SidecarKernel,
12469    process: &mut ActiveProcess,
12470    request: &JavascriptSyncRpcRequest,
12471) -> Result<Value, SidecarError> {
12472    ensure_per_process_state_handle_capacity(process.sqlite_databases.len(), "sqlite database")?;
12473    let path = request.args.first().and_then(Value::as_str);
12474    let vm_path = path.filter(|value| !value.is_empty() && *value != ":memory:");
12475    let options = request.args.get(1);
12476    let read_only = sqlite_option_bool(options, "readOnly").unwrap_or(false);
12477    let create = sqlite_option_bool(options, "create").unwrap_or(!read_only);
12478    let timeout_ms = sqlite_option_u64(options, "timeout");
12479
12480    process.next_sqlite_database_id += 1;
12481    let database_id = process.next_sqlite_database_id;
12482
12483    let host_path = if vm_path.is_some() {
12484        Some(
12485            std::env::temp_dir()
12486                .join(format!(
12487                    "secure-exec-sidecar-sqlite-{}-{database_id}",
12488                    process.kernel_pid
12489                ))
12490                .join("database.sqlite"),
12491        )
12492    } else {
12493        None
12494    };
12495
12496    if let Some(host_path) = host_path.as_ref() {
12497        if let Some(parent) = host_path.parent() {
12498            fs::create_dir_all(parent).map_err(|error| {
12499                SidecarError::Io(format!(
12500                    "failed to prepare sqlite temp directory {}: {error}",
12501                    parent.display()
12502                ))
12503            })?;
12504        }
12505    }
12506
12507    if let (Some(vm_path), Some(host_path)) = (vm_path, host_path.as_ref()) {
12508        if kernel
12509            .exists_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, vm_path)
12510            .map_err(kernel_error)?
12511        {
12512            let contents = kernel
12513                .read_file_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, vm_path)
12514                .map_err(kernel_error)?;
12515            fs::write(host_path, contents).map_err(|error| {
12516                SidecarError::Io(format!(
12517                    "failed to materialize sqlite database {}: {error}",
12518                    host_path.display()
12519                ))
12520            })?;
12521        } else if read_only && !create {
12522            return Err(SidecarError::InvalidState(format!(
12523                "sqlite database does not exist: {vm_path}"
12524            )));
12525        }
12526    }
12527
12528    let target = host_path
12529        .as_ref()
12530        .map(|path| path.to_string_lossy().into_owned())
12531        .unwrap_or_else(|| String::from(":memory:"));
12532    let mut flags = if read_only {
12533        SqliteOpenFlags::SQLITE_OPEN_READ_ONLY
12534    } else {
12535        SqliteOpenFlags::SQLITE_OPEN_READ_WRITE
12536    };
12537    if create && !read_only {
12538        flags |= SqliteOpenFlags::SQLITE_OPEN_CREATE;
12539    }
12540
12541    let connection = SqliteConnection::open_with_flags(&target, flags).map_err(|error| {
12542        SidecarError::InvalidState(format!(
12543            "sqlite database open failed for {}: {error}",
12544            vm_path.unwrap_or(":memory:")
12545        ))
12546    })?;
12547    if let Some(timeout_ms) = timeout_ms {
12548        connection
12549            .busy_timeout(Duration::from_millis(timeout_ms))
12550            .map_err(sqlite_error)?;
12551    }
12552    if host_path.is_some() && !read_only {
12553        let _ = connection.pragma_update(None, "journal_mode", "WAL");
12554    }
12555
12556    process.sqlite_databases.insert(
12557        database_id,
12558        ActiveSqliteDatabase {
12559            connection,
12560            host_path,
12561            vm_path: vm_path.map(String::from),
12562            dirty: false,
12563            transaction_depth: 0,
12564            read_only,
12565        },
12566    );
12567
12568    Ok(json!(database_id))
12569}
12570
12571fn sqlite_exec_database(
12572    kernel: &mut SidecarKernel,
12573    process: &mut ActiveProcess,
12574    request: &JavascriptSyncRpcRequest,
12575) -> Result<Value, SidecarError> {
12576    let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.exec database id")?;
12577    let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.exec sql")?;
12578    let kernel_pid = process.kernel_pid;
12579    let database = sqlite_database_mut(process, database_id)?;
12580    let before = database.connection.total_changes();
12581    database
12582        .connection
12583        .execute_batch(sql)
12584        .map_err(sqlite_error)?;
12585    mark_sqlite_mutation(database, sql);
12586    sqlite_sync_database(kernel, kernel_pid, database)?;
12587    Ok(json!(database
12588        .connection
12589        .total_changes()
12590        .saturating_sub(before)))
12591}
12592
12593fn sqlite_query_database(
12594    process: &mut ActiveProcess,
12595    request: &JavascriptSyncRpcRequest,
12596) -> Result<Value, SidecarError> {
12597    let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.query database id")?;
12598    let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.query sql")?;
12599    let params = request.args.get(2);
12600    let options = request.args.get(3);
12601    let return_arrays = sqlite_option_bool(options, "returnArrays").unwrap_or(false);
12602    let read_bigints = sqlite_option_bool(options, "readBigInts").unwrap_or(false);
12603    let database = sqlite_database_mut(process, database_id)?;
12604    sqlite_query_rows(
12605        &mut database.connection,
12606        sql,
12607        params,
12608        return_arrays,
12609        read_bigints,
12610        true,
12611        false,
12612    )
12613}
12614
12615fn sqlite_prepare_statement(
12616    process: &mut ActiveProcess,
12617    request: &JavascriptSyncRpcRequest,
12618) -> Result<Value, SidecarError> {
12619    ensure_per_process_state_handle_capacity(process.sqlite_statements.len(), "sqlite statement")?;
12620    let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.prepare database id")?;
12621    let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.prepare sql")?;
12622    let _ = sqlite_database(process, database_id)?;
12623    process.next_sqlite_statement_id += 1;
12624    let statement_id = process.next_sqlite_statement_id;
12625    process.sqlite_statements.insert(
12626        statement_id,
12627        ActiveSqliteStatement {
12628            database_id,
12629            sql: sql.to_owned(),
12630            return_arrays: false,
12631            read_bigints: false,
12632            allow_bare_named_parameters: false,
12633            allow_unknown_named_parameters: false,
12634        },
12635    );
12636    Ok(json!(statement_id))
12637}
12638
12639fn sqlite_run_statement(
12640    kernel: &mut SidecarKernel,
12641    process: &mut ActiveProcess,
12642    request: &JavascriptSyncRpcRequest,
12643) -> Result<Value, SidecarError> {
12644    let statement_id =
12645        javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.run statement id")?;
12646    let params = request.args.get(1);
12647    let statement_state = sqlite_statement(process, statement_id)?.clone();
12648    let kernel_pid = process.kernel_pid;
12649    let database = sqlite_database_mut(process, statement_state.database_id)?;
12650    let before = database.connection.total_changes();
12651    {
12652        let mut statement = database
12653            .connection
12654            .prepare(&statement_state.sql)
12655            .map_err(sqlite_error)?;
12656        bind_sqlite_parameters(
12657            &mut statement,
12658            params,
12659            statement_state.allow_bare_named_parameters,
12660            statement_state.allow_unknown_named_parameters,
12661        )?;
12662        statement.raw_execute().map_err(sqlite_error)?;
12663    }
12664    let changes = database.connection.total_changes().saturating_sub(before);
12665    let last_insert_rowid = database.connection.last_insert_rowid();
12666    mark_sqlite_mutation(database, &statement_state.sql);
12667    sqlite_sync_database(kernel, kernel_pid, database)?;
12668    let result = json!({
12669        "changes": changes,
12670        "lastInsertRowid": encode_sqlite_integer(last_insert_rowid, true),
12671    });
12672    Ok(result)
12673}
12674
12675fn sqlite_get_statement(
12676    process: &mut ActiveProcess,
12677    request: &JavascriptSyncRpcRequest,
12678) -> Result<Value, SidecarError> {
12679    let statement_id =
12680        javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.get statement id")?;
12681    let params = request.args.get(1);
12682    let statement_state = sqlite_statement(process, statement_id)?.clone();
12683    let database = sqlite_database_mut(process, statement_state.database_id)?;
12684    let rows = sqlite_query_rows(
12685        &mut database.connection,
12686        &statement_state.sql,
12687        params,
12688        statement_state.return_arrays,
12689        statement_state.read_bigints,
12690        statement_state.allow_bare_named_parameters,
12691        statement_state.allow_unknown_named_parameters,
12692    )?;
12693    Ok(rows
12694        .as_array()
12695        .and_then(|rows| rows.first().cloned())
12696        .unwrap_or(Value::Null))
12697}
12698
12699fn sqlite_all_statement(
12700    process: &mut ActiveProcess,
12701    request: &JavascriptSyncRpcRequest,
12702) -> Result<Value, SidecarError> {
12703    let statement_id =
12704        javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.all statement id")?;
12705    let params = request.args.get(1);
12706    let statement_state = sqlite_statement(process, statement_id)?.clone();
12707    let database = sqlite_database_mut(process, statement_state.database_id)?;
12708    sqlite_query_rows(
12709        &mut database.connection,
12710        &statement_state.sql,
12711        params,
12712        statement_state.return_arrays,
12713        statement_state.read_bigints,
12714        statement_state.allow_bare_named_parameters,
12715        statement_state.allow_unknown_named_parameters,
12716    )
12717}
12718
12719fn sqlite_statement_columns(
12720    process: &mut ActiveProcess,
12721    request: &JavascriptSyncRpcRequest,
12722) -> Result<Value, SidecarError> {
12723    let statement_id =
12724        javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.columns statement id")?;
12725    let statement_state = sqlite_statement(process, statement_id)?.clone();
12726    let database = sqlite_database_mut(process, statement_state.database_id)?;
12727    let statement = database
12728        .connection
12729        .prepare(&statement_state.sql)
12730        .map_err(sqlite_error)?;
12731    Ok(Value::Array(
12732        statement
12733            .column_names()
12734            .iter()
12735            .map(|name| json!({ "name": name }))
12736            .collect(),
12737    ))
12738}
12739
12740fn sqlite_query_rows(
12741    connection: &mut SqliteConnection,
12742    sql: &str,
12743    params: Option<&Value>,
12744    return_arrays: bool,
12745    read_bigints: bool,
12746    allow_bare_named_parameters: bool,
12747    allow_unknown_named_parameters: bool,
12748) -> Result<Value, SidecarError> {
12749    let mut statement = connection.prepare(sql).map_err(sqlite_error)?;
12750    let column_names = statement
12751        .column_names()
12752        .iter()
12753        .map(|name| (*name).to_owned())
12754        .collect::<Vec<_>>();
12755    let column_count = statement.column_count();
12756    bind_sqlite_parameters(
12757        &mut statement,
12758        params,
12759        allow_bare_named_parameters,
12760        allow_unknown_named_parameters,
12761    )?;
12762    let mut rows = statement.raw_query();
12763    let mut encoded_rows = Vec::new();
12764    while let Some(row) = rows.next().map_err(sqlite_error)? {
12765        encoded_rows.push(encode_sqlite_row(
12766            row,
12767            &column_names,
12768            column_count,
12769            return_arrays,
12770            read_bigints,
12771        )?);
12772    }
12773    Ok(Value::Array(encoded_rows))
12774}
12775
12776fn encode_sqlite_row(
12777    row: &rusqlite::Row<'_>,
12778    column_names: &[String],
12779    column_count: usize,
12780    return_arrays: bool,
12781    read_bigints: bool,
12782) -> Result<Value, SidecarError> {
12783    if return_arrays {
12784        let mut values = Vec::with_capacity(column_count);
12785        for index in 0..column_count {
12786            values.push(encode_sqlite_value_ref(
12787                row.get_ref(index).map_err(sqlite_error)?,
12788                read_bigints,
12789            )?);
12790        }
12791        return Ok(Value::Array(values));
12792    }
12793
12794    let mut object = Map::with_capacity(column_count);
12795    for (index, name) in column_names.iter().enumerate() {
12796        object.insert(
12797            name.clone(),
12798            encode_sqlite_value_ref(row.get_ref(index).map_err(sqlite_error)?, read_bigints)?,
12799        );
12800    }
12801    Ok(Value::Object(object))
12802}
12803
12804fn encode_sqlite_value_ref(
12805    value: SqliteValueRef<'_>,
12806    read_bigints: bool,
12807) -> Result<Value, SidecarError> {
12808    Ok(match value {
12809        SqliteValueRef::Null => Value::Null,
12810        SqliteValueRef::Integer(number) => encode_sqlite_integer(number, read_bigints),
12811        SqliteValueRef::Real(number) => json!(number),
12812        SqliteValueRef::Text(text) => Value::String(String::from_utf8_lossy(text).into_owned()),
12813        SqliteValueRef::Blob(bytes) => json!({
12814            "__agentosSqliteType": "uint8array",
12815            "value": base64::engine::general_purpose::STANDARD.encode(bytes),
12816        }),
12817    })
12818}
12819
12820fn encode_sqlite_integer(number: i64, read_bigints: bool) -> Value {
12821    if read_bigints || number.abs() > SQLITE_JS_SAFE_INTEGER_MAX {
12822        json!({
12823            "__agentosSqliteType": "bigint",
12824            "value": number.to_string(),
12825        })
12826    } else {
12827        json!(number)
12828    }
12829}
12830
12831fn bind_sqlite_parameters(
12832    statement: &mut SqliteStatement<'_>,
12833    params: Option<&Value>,
12834    allow_bare_named_parameters: bool,
12835    allow_unknown_named_parameters: bool,
12836) -> Result<(), SidecarError> {
12837    let Some(params) = params else {
12838        return Ok(());
12839    };
12840    match params {
12841        Value::Null => Ok(()),
12842        Value::Array(values) => {
12843            for (index, value) in values.iter().enumerate() {
12844                statement
12845                    .raw_bind_parameter(index + 1, decode_sqlite_parameter(value)?)
12846                    .map_err(sqlite_error)?;
12847            }
12848            Ok(())
12849        }
12850        Value::Object(map)
12851            if map
12852                .get("__agentosSqliteType")
12853                .and_then(Value::as_str)
12854                .is_none() =>
12855        {
12856            for (key, value) in map {
12857                let index =
12858                    resolve_sqlite_parameter_index(statement, key, allow_bare_named_parameters)?;
12859                let Some(index) = index else {
12860                    if allow_unknown_named_parameters {
12861                        continue;
12862                    }
12863                    return Err(SidecarError::InvalidState(format!(
12864                        "sqlite named parameter not found: {key}"
12865                    )));
12866                };
12867                statement
12868                    .raw_bind_parameter(index, decode_sqlite_parameter(value)?)
12869                    .map_err(sqlite_error)?;
12870            }
12871            Ok(())
12872        }
12873        other => statement
12874            .raw_bind_parameter(1, decode_sqlite_parameter(other)?)
12875            .map_err(sqlite_error),
12876    }
12877}
12878
12879fn resolve_sqlite_parameter_index(
12880    statement: &mut SqliteStatement<'_>,
12881    key: &str,
12882    allow_bare_named_parameters: bool,
12883) -> Result<Option<usize>, SidecarError> {
12884    let mut candidates = vec![key.to_owned()];
12885    if allow_bare_named_parameters
12886        && !key.starts_with(':')
12887        && !key.starts_with('@')
12888        && !key.starts_with('$')
12889    {
12890        candidates.push(format!(":{key}"));
12891        candidates.push(format!("@{key}"));
12892        candidates.push(format!("${key}"));
12893    }
12894    for candidate in candidates {
12895        if let Some(index) = statement
12896            .parameter_index(&candidate)
12897            .map_err(sqlite_error)?
12898        {
12899            return Ok(Some(index));
12900        }
12901    }
12902    Ok(None)
12903}
12904
12905fn decode_sqlite_parameter(value: &Value) -> Result<rusqlite::types::Value, SidecarError> {
12906    Ok(match value {
12907        Value::Null => rusqlite::types::Value::Null,
12908        Value::Bool(value) => rusqlite::types::Value::Integer(i64::from(*value)),
12909        Value::Number(value) => match (value.as_i64(), value.as_f64()) {
12910            (Some(integer), _) => rusqlite::types::Value::Integer(integer),
12911            (_, Some(real)) => rusqlite::types::Value::Real(real),
12912            _ => {
12913                return Err(SidecarError::InvalidState(String::from(
12914                    "sqlite parameter number is not representable",
12915                )));
12916            }
12917        },
12918        Value::String(value) => rusqlite::types::Value::Text(value.clone()),
12919        Value::Array(_) => {
12920            return Err(SidecarError::InvalidState(String::from(
12921                "sqlite parameters do not support nested arrays",
12922            )));
12923        }
12924        Value::Object(map) => match map.get("__agentosSqliteType").and_then(Value::as_str) {
12925            Some("bigint") => rusqlite::types::Value::Integer(
12926                map.get("value")
12927                    .and_then(Value::as_str)
12928                    .ok_or_else(|| {
12929                        SidecarError::InvalidState(String::from(
12930                            "sqlite bigint parameter missing string value",
12931                        ))
12932                    })?
12933                    .parse::<i64>()
12934                    .map_err(|error| {
12935                        SidecarError::InvalidState(format!(
12936                            "sqlite bigint parameter is not a signed 64-bit integer: {error}"
12937                        ))
12938                    })?,
12939            ),
12940            Some("uint8array") => rusqlite::types::Value::Blob(
12941                base64::engine::general_purpose::STANDARD
12942                    .decode(map.get("value").and_then(Value::as_str).ok_or_else(|| {
12943                        SidecarError::InvalidState(String::from(
12944                            "sqlite blob parameter missing base64 value",
12945                        ))
12946                    })?)
12947                    .map_err(|error| {
12948                        SidecarError::InvalidState(format!(
12949                            "sqlite blob parameter contains invalid base64: {error}"
12950                        ))
12951                    })?,
12952            ),
12953            Some(other) => {
12954                return Err(SidecarError::InvalidState(format!(
12955                    "unsupported sqlite tagged parameter type {other}"
12956                )));
12957            }
12958            None => {
12959                return Err(SidecarError::InvalidState(String::from(
12960                    "sqlite named parameter objects must be passed as the top-level params object",
12961                )));
12962            }
12963        },
12964    })
12965}
12966
12967fn close_sqlite_database(
12968    kernel: &mut SidecarKernel,
12969    process: &mut ActiveProcess,
12970    database_id: u64,
12971) -> Result<(), SidecarError> {
12972    let mut database = process
12973        .sqlite_databases
12974        .remove(&database_id)
12975        .ok_or_else(|| {
12976            SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
12977        })?;
12978    process
12979        .sqlite_statements
12980        .retain(|_, statement| statement.database_id != database_id);
12981    sqlite_sync_database(kernel, process.kernel_pid, &mut database)?;
12982    let host_path = database.host_path.clone();
12983    drop(database);
12984    cleanup_sqlite_host_artifacts(host_path.as_deref())?;
12985    Ok(())
12986}
12987
12988fn ensure_per_process_state_handle_capacity(len: usize, label: &str) -> Result<(), SidecarError> {
12989    if len >= MAX_PER_PROCESS_STATE_HANDLES {
12990        return Err(SidecarError::InvalidState(format!(
12991            "{label} handle limit exceeded: limit is {MAX_PER_PROCESS_STATE_HANDLES}"
12992        )));
12993    }
12994    Ok(())
12995}
12996
12997fn sqlite_sync_database(
12998    kernel: &mut SidecarKernel,
12999    kernel_pid: u32,
13000    database: &mut ActiveSqliteDatabase,
13001) -> Result<(), SidecarError> {
13002    if !database.dirty
13003        || database.transaction_depth > 0
13004        || database.read_only
13005        || database.host_path.is_none()
13006        || database.vm_path.is_none()
13007    {
13008        return Ok(());
13009    }
13010
13011    let _ = database
13012        .connection
13013        .execute_batch("PRAGMA wal_checkpoint(TRUNCATE)");
13014    let host_path = database.host_path.as_ref().expect("sqlite host path");
13015    if !host_path.exists() {
13016        return Ok(());
13017    }
13018    ensure_vm_parent_dir(
13019        kernel,
13020        kernel_pid,
13021        database.vm_path.as_deref().expect("sqlite vm path"),
13022    )?;
13023    let contents = fs::read(host_path).map_err(|error| {
13024        SidecarError::Io(format!(
13025            "failed to read sqlite temp database {}: {error}",
13026            host_path.display()
13027        ))
13028    })?;
13029    kernel
13030        .write_file_for_process(
13031            EXECUTION_DRIVER_NAME,
13032            kernel_pid,
13033            database.vm_path.as_deref().expect("sqlite vm path"),
13034            contents,
13035            None,
13036        )
13037        .map_err(kernel_error)?;
13038    database.dirty = false;
13039    Ok(())
13040}
13041
13042fn cleanup_sqlite_host_artifacts(host_path: Option<&Path>) -> Result<(), SidecarError> {
13043    let Some(host_path) = host_path else {
13044        return Ok(());
13045    };
13046    let parent = host_path.parent().map(PathBuf::from);
13047    for suffix in ["", "-wal", "-shm"] {
13048        let path = PathBuf::from(format!("{}{}", host_path.display(), suffix));
13049        if path.exists() {
13050            fs::remove_file(&path).map_err(|error| {
13051                SidecarError::Io(format!(
13052                    "failed to remove sqlite temp artifact {}: {error}",
13053                    path.display()
13054                ))
13055            })?;
13056        }
13057    }
13058    if let Some(parent) = parent {
13059        let _ = fs::remove_dir_all(parent);
13060    }
13061    Ok(())
13062}
13063
13064fn ensure_vm_parent_dir(
13065    kernel: &mut SidecarKernel,
13066    kernel_pid: u32,
13067    path: &str,
13068) -> Result<(), SidecarError> {
13069    let parent = dirname(path);
13070    if parent == "/" || parent == "." {
13071        return Ok(());
13072    }
13073    let mut current = String::new();
13074    for segment in parent.split('/').filter(|segment| !segment.is_empty()) {
13075        current.push('/');
13076        current.push_str(segment);
13077        if !kernel
13078            .exists_for_process(EXECUTION_DRIVER_NAME, kernel_pid, &current)
13079            .map_err(kernel_error)?
13080        {
13081            kernel
13082                .mkdir_for_process(EXECUTION_DRIVER_NAME, kernel_pid, &current, false, None)
13083                .map_err(kernel_error)?;
13084        }
13085    }
13086    Ok(())
13087}
13088
13089fn sqlite_database(
13090    process: &ActiveProcess,
13091    database_id: u64,
13092) -> Result<&ActiveSqliteDatabase, SidecarError> {
13093    process.sqlite_databases.get(&database_id).ok_or_else(|| {
13094        SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
13095    })
13096}
13097
13098fn sqlite_database_mut(
13099    process: &mut ActiveProcess,
13100    database_id: u64,
13101) -> Result<&mut ActiveSqliteDatabase, SidecarError> {
13102    process
13103        .sqlite_databases
13104        .get_mut(&database_id)
13105        .ok_or_else(|| {
13106            SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
13107        })
13108}
13109
13110fn sqlite_statement(
13111    process: &ActiveProcess,
13112    statement_id: u64,
13113) -> Result<&ActiveSqliteStatement, SidecarError> {
13114    process.sqlite_statements.get(&statement_id).ok_or_else(|| {
13115        SidecarError::InvalidState(format!("sqlite statement handle not found: {statement_id}"))
13116    })
13117}
13118
13119fn sqlite_statement_mut(
13120    process: &mut ActiveProcess,
13121    statement_id: u64,
13122) -> Result<&mut ActiveSqliteStatement, SidecarError> {
13123    process
13124        .sqlite_statements
13125        .get_mut(&statement_id)
13126        .ok_or_else(|| {
13127            SidecarError::InvalidState(format!("sqlite statement handle not found: {statement_id}"))
13128        })
13129}
13130
13131fn mark_sqlite_mutation(database: &mut ActiveSqliteDatabase, sql: &str) {
13132    let normalized = sql.trim_start().to_ascii_lowercase();
13133    if normalized.starts_with("begin") || normalized.starts_with("savepoint") {
13134        database.dirty = true;
13135        database.transaction_depth += 1;
13136        return;
13137    }
13138    if normalized.starts_with("commit") || normalized.starts_with("release savepoint") {
13139        database.dirty = true;
13140        database.transaction_depth = database.transaction_depth.saturating_sub(1);
13141        return;
13142    }
13143    if normalized.starts_with("rollback") && !normalized.starts_with("rollback to") {
13144        database.dirty = true;
13145        database.transaction_depth = database.transaction_depth.saturating_sub(1);
13146        return;
13147    }
13148    if normalized.starts_with("insert")
13149        || normalized.starts_with("update")
13150        || normalized.starts_with("delete")
13151        || normalized.starts_with("replace")
13152        || normalized.starts_with("create")
13153        || normalized.starts_with("alter")
13154        || normalized.starts_with("drop")
13155        || normalized.starts_with("vacuum")
13156        || normalized.starts_with("reindex")
13157        || normalized.starts_with("analyze")
13158        || normalized.starts_with("attach")
13159        || normalized.starts_with("detach")
13160        || normalized.starts_with("pragma")
13161    {
13162        database.dirty = true;
13163    }
13164}
13165
13166fn sqlite_option_bool(options: Option<&Value>, key: &str) -> Option<bool> {
13167    options
13168        .and_then(|value| value.get(key))
13169        .and_then(Value::as_bool)
13170}
13171
13172fn sqlite_option_u64(options: Option<&Value>, key: &str) -> Option<u64> {
13173    options
13174        .and_then(|value| value.get(key))
13175        .and_then(Value::as_u64)
13176}
13177
13178fn sqlite_error(error: rusqlite::Error) -> SidecarError {
13179    SidecarError::InvalidState(format!("sqlite error: {error}"))
13180}
13181
13182pub(crate) fn javascript_sync_rpc_arg_str<'a>(
13183    args: &'a [Value],
13184    index: usize,
13185    label: &str,
13186) -> Result<&'a str, SidecarError> {
13187    args.get(index)
13188        .and_then(Value::as_str)
13189        .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a string argument")))
13190}
13191
13192pub(crate) fn javascript_sync_rpc_arg_bool(
13193    args: &[Value],
13194    index: usize,
13195    label: &str,
13196) -> Result<bool, SidecarError> {
13197    args.get(index)
13198        .and_then(Value::as_bool)
13199        .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a boolean argument")))
13200}
13201
13202pub(crate) fn javascript_sync_rpc_encoding(args: &[Value]) -> Option<String> {
13203    args.get(1).and_then(|value| {
13204        value.as_str().map(str::to_owned).or_else(|| {
13205            value
13206                .get("encoding")
13207                .and_then(Value::as_str)
13208                .map(str::to_owned)
13209        })
13210    })
13211}
13212
13213pub(crate) fn javascript_sync_rpc_option_bool(
13214    args: &[Value],
13215    index: usize,
13216    key: &str,
13217) -> Option<bool> {
13218    let value = args.get(index)?;
13219    if key == "recursive" {
13220        if let Some(boolean) = value.as_bool() {
13221            return Some(boolean);
13222        }
13223    }
13224    value.get(key).and_then(Value::as_bool)
13225}
13226
13227pub(crate) fn javascript_sync_rpc_option_u32(
13228    args: &[Value],
13229    index: usize,
13230    key: &str,
13231) -> Result<Option<u32>, SidecarError> {
13232    let Some(value) = args.get(index).and_then(|value| {
13233        if value.is_object() {
13234            value.get(key)
13235        } else if key == "mode" && value.is_number() {
13236            Some(value)
13237        } else {
13238            None
13239        }
13240    }) else {
13241        return Ok(None);
13242    };
13243    if value.is_null() {
13244        return Ok(None);
13245    }
13246
13247    let numeric = value
13248        .as_u64()
13249        .or_else(|| {
13250            value
13251                .as_f64()
13252                .filter(|number| number.is_finite() && *number >= 0.0)
13253                .map(|number| number as u64)
13254        })
13255        .ok_or_else(|| SidecarError::InvalidState(format!("{key} must be numeric")))?;
13256
13257    u32::try_from(numeric)
13258        .map(Some)
13259        .map_err(|_| SidecarError::InvalidState(format!("{key} must fit within u32")))
13260}
13261
13262pub(crate) fn javascript_sync_rpc_arg_u32(
13263    args: &[Value],
13264    index: usize,
13265    label: &str,
13266) -> Result<u32, SidecarError> {
13267    let value = javascript_sync_rpc_arg_u64(args, index, label)?;
13268    u32::try_from(value)
13269        .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")))
13270}
13271
13272pub(crate) fn javascript_sync_rpc_arg_i32(
13273    args: &[Value],
13274    index: usize,
13275    label: &str,
13276) -> Result<i32, SidecarError> {
13277    let Some(value) = args.get(index) else {
13278        return Err(SidecarError::InvalidState(format!("{label} is required")));
13279    };
13280
13281    let numeric = value
13282        .as_i64()
13283        .or_else(|| {
13284            value
13285                .as_f64()
13286                .filter(|number| number.is_finite())
13287                .map(|number| number as i64)
13288        })
13289        .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a numeric argument")))?;
13290
13291    i32::try_from(numeric)
13292        .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within i32")))
13293}
13294
13295pub(crate) fn javascript_sync_rpc_arg_u32_optional(
13296    args: &[Value],
13297    index: usize,
13298    label: &str,
13299) -> Result<Option<u32>, SidecarError> {
13300    javascript_sync_rpc_arg_u64_optional(args, index, label)?
13301        .map(|value| {
13302            u32::try_from(value)
13303                .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")))
13304        })
13305        .transpose()
13306}
13307
13308pub(crate) fn javascript_sync_rpc_arg_u64(
13309    args: &[Value],
13310    index: usize,
13311    label: &str,
13312) -> Result<u64, SidecarError> {
13313    let Some(value) = args.get(index) else {
13314        return Err(SidecarError::InvalidState(format!("{label} is required")));
13315    };
13316
13317    value
13318        .as_u64()
13319        .or_else(|| {
13320            value
13321                .as_f64()
13322                .filter(|number| number.is_finite() && *number >= 0.0)
13323                .map(|number| number as u64)
13324        })
13325        .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a numeric argument")))
13326}
13327
13328pub(crate) fn javascript_sync_rpc_arg_u64_optional(
13329    args: &[Value],
13330    index: usize,
13331    label: &str,
13332) -> Result<Option<u64>, SidecarError> {
13333    let Some(value) = args.get(index) else {
13334        return Ok(None);
13335    };
13336    if value.is_null() {
13337        return Ok(None);
13338    }
13339    javascript_sync_rpc_arg_u64(args, index, label).map(Some)
13340}
13341
13342pub(crate) fn javascript_sync_rpc_bytes_arg(
13343    args: &[Value],
13344    index: usize,
13345    label: &str,
13346) -> Result<Vec<u8>, SidecarError> {
13347    let Some(value) = args.get(index) else {
13348        return Err(SidecarError::InvalidState(format!("{label} is required")));
13349    };
13350
13351    if let Some(text) = value.as_str() {
13352        return Ok(text.as_bytes().to_vec());
13353    }
13354
13355    let Some(base64_value) = value
13356        .get("__agentOsType")
13357        .and_then(Value::as_str)
13358        .filter(|kind| *kind == "bytes")
13359        .and_then(|_| value.get("base64"))
13360        .and_then(Value::as_str)
13361    else {
13362        return Err(SidecarError::InvalidState(format!(
13363            "{label} must be a string or encoded bytes payload"
13364        )));
13365    };
13366
13367    base64::engine::general_purpose::STANDARD
13368        .decode(base64_value)
13369        .map_err(|error| {
13370            SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
13371        })
13372}
13373
13374pub(crate) fn javascript_sync_rpc_bytes_value(bytes: &[u8]) -> Value {
13375    json!({
13376        "__agentOsType": "bytes",
13377        "base64": base64::engine::general_purpose::STANDARD.encode(bytes),
13378    })
13379}
13380
13381#[derive(Debug, Deserialize)]
13382struct KernelPollFdRequest {
13383    fd: u32,
13384    events: u16,
13385}
13386
13387#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
13388struct KernelPollFdResponse {
13389    fd: u32,
13390    events: u16,
13391    revents: u16,
13392}
13393
13394fn javascript_sync_rpc_base64_arg(
13395    args: &[Value],
13396    index: usize,
13397    label: &str,
13398) -> Result<Vec<u8>, SidecarError> {
13399    let value = javascript_sync_rpc_arg_str(args, index, label)?;
13400    base64::engine::general_purpose::STANDARD
13401        .decode(value)
13402        .map_err(|error| {
13403            SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
13404        })
13405}
13406
13407pub(crate) fn service_javascript_sync_rpc<B>(
13408    request: JavascriptSyncRpcServiceRequest<'_, B>,
13409) -> Result<Value, SidecarError>
13410where
13411    B: NativeSidecarBridge + Send + 'static,
13412    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
13413{
13414    let JavascriptSyncRpcServiceRequest {
13415        bridge,
13416        vm_id,
13417        dns,
13418        socket_paths,
13419        kernel,
13420        process,
13421        sync_request: request,
13422        resource_limits,
13423        network_counts,
13424    } = request;
13425    match request.method.as_str() {
13426        // Module resolution / loading / format detection read the kernel VFS so
13427        // the resolver sees exactly what the guest and `kernel.readFile()` see.
13428        "_resolveModule"
13429        | "_resolveModuleSync"
13430        | "__resolve_module"
13431        | "_batchResolveModules"
13432        | "__batch_resolve_modules"
13433        | "_loadFile"
13434        | "_loadFileSync"
13435        | "__load_file"
13436        | "_moduleFormat"
13437        | "__module_format" => service_javascript_module_sync_rpc(kernel, process, request),
13438        // Polyfills are static guest expressions, not VFS reads.
13439        "_loadPolyfill" | "__load_polyfill" => {
13440            service_javascript_internal_bridge_sync_rpc(process, request)
13441        }
13442        "__kernel_stdin_read" => match &process.execution {
13443            ActiveExecution::Javascript(execution) => execution
13444                .read_kernel_stdin_sync_rpc(request)
13445                .map_err(|error| SidecarError::Execution(error.to_string())),
13446            ActiveExecution::Python(_) | ActiveExecution::Wasm(_) | ActiveExecution::Tool(_) => {
13447                service_javascript_kernel_stdin_sync_rpc(kernel, process, request)
13448            }
13449        },
13450        "__kernel_stdio_write" => {
13451            service_javascript_kernel_stdio_write_sync_rpc(kernel, process, request)
13452        }
13453        "__kernel_poll" => service_javascript_kernel_poll_sync_rpc(kernel, process, request),
13454        "__pty_set_raw_mode" => {
13455            service_javascript_pty_set_raw_mode_sync_rpc(kernel, process, request)
13456        }
13457        "crypto.hashDigest"
13458        | "crypto.hmacDigest"
13459        | "crypto.pbkdf2"
13460        | "crypto.scrypt"
13461        | "crypto.cipheriv"
13462        | "crypto.decipheriv"
13463        | "crypto.cipherivCreate"
13464        | "crypto.cipherivUpdate"
13465        | "crypto.cipherivFinal"
13466        | "crypto.sign"
13467        | "crypto.verify"
13468        | "crypto.asymmetricOp"
13469        | "crypto.createKeyObject"
13470        | "crypto.generateKeyPairSync"
13471        | "crypto.generateKeySync"
13472        | "crypto.generatePrimeSync"
13473        | "crypto.diffieHellman"
13474        | "crypto.diffieHellmanGroup"
13475        | "crypto.diffieHellmanSessionCreate"
13476        | "crypto.diffieHellmanSessionCall"
13477        | "crypto.diffieHellmanSessionDestroy"
13478        | "crypto.subtle" => service_javascript_crypto_sync_rpc(process, request),
13479        "dns.lookup" | "dns.resolve" | "dns.resolve4" | "dns.resolve6" => {
13480            service_javascript_dns_sync_rpc(bridge, kernel, vm_id, dns, request)
13481        }
13482        "net.http_listen" | "net.http_close" | "net.http_wait" | "net.http_respond" => {
13483            service_javascript_net_sync_rpc(JavascriptNetSyncRpcServiceRequest {
13484                bridge,
13485                vm_id,
13486                dns,
13487                socket_paths,
13488                kernel,
13489                process,
13490                sync_request: request,
13491                resource_limits,
13492                network_counts,
13493            })
13494        }
13495        "net.http2_server_listen"
13496        | "net.http2_server_poll"
13497        | "net.http2_server_close"
13498        | "net.http2_server_respond"
13499        | "net.http2_server_wait"
13500        | "net.http2_session_connect"
13501        | "net.http2_session_request"
13502        | "net.http2_session_settings"
13503        | "net.http2_session_set_local_window_size"
13504        | "net.http2_session_goaway"
13505        | "net.http2_session_close"
13506        | "net.http2_session_destroy"
13507        | "net.http2_session_poll"
13508        | "net.http2_session_wait"
13509        | "net.http2_stream_respond"
13510        | "net.http2_stream_push_stream"
13511        | "net.http2_stream_write"
13512        | "net.http2_stream_end"
13513        | "net.http2_stream_close"
13514        | "net.http2_stream_pause"
13515        | "net.http2_stream_resume"
13516        | "net.http2_stream_respond_with_file" => {
13517            service_javascript_http2_sync_rpc(JavascriptHttp2SyncRpcServiceRequest {
13518                bridge,
13519                kernel,
13520                vm_id,
13521                dns,
13522                socket_paths,
13523                process,
13524                sync_request: request,
13525                resource_limits,
13526                network_counts,
13527            })
13528        }
13529        "net.connect"
13530        | "net.reserve_tcp_port"
13531        | "net.release_tcp_port"
13532        | "net.listen"
13533        | "net.poll"
13534        | "net.socket_wait_connect"
13535        | "net.socket_read"
13536        | "net.socket_set_no_delay"
13537        | "net.socket_set_keep_alive"
13538        | "net.socket_upgrade_tls"
13539        | "net.socket_get_tls_client_hello"
13540        | "net.socket_tls_query"
13541        | "net.server_poll"
13542        | "net.server_accept"
13543        | "net.server_connections"
13544        | "net.upgrade_socket_write"
13545        | "net.upgrade_socket_end"
13546        | "net.upgrade_socket_destroy"
13547        | "net.write"
13548        | "net.shutdown"
13549        | "net.destroy"
13550        | "net.server_close"
13551        | "tls.get_ciphers" => {
13552            service_javascript_net_sync_rpc(JavascriptNetSyncRpcServiceRequest {
13553                bridge,
13554                vm_id,
13555                dns,
13556                socket_paths,
13557                kernel,
13558                process,
13559                sync_request: request,
13560                resource_limits,
13561                network_counts,
13562            })
13563        }
13564        "dgram.createSocket"
13565        | "dgram.bind"
13566        | "dgram.send"
13567        | "dgram.poll"
13568        | "dgram.close"
13569        | "dgram.address"
13570        | "dgram.setBufferSize"
13571        | "dgram.getBufferSize" => {
13572            service_javascript_dgram_sync_rpc(JavascriptDgramSyncRpcServiceRequest {
13573                bridge,
13574                kernel,
13575                vm_id,
13576                dns,
13577                socket_paths,
13578                process,
13579                sync_request: request,
13580                resource_limits,
13581                network_counts,
13582            })
13583        }
13584        "sqlite.constants"
13585        | "sqlite.open"
13586        | "sqlite.close"
13587        | "sqlite.exec"
13588        | "sqlite.query"
13589        | "sqlite.prepare"
13590        | "sqlite.location"
13591        | "sqlite.checkpoint"
13592        | "sqlite.statement.run"
13593        | "sqlite.statement.get"
13594        | "sqlite.statement.all"
13595        | "sqlite.statement.iterate"
13596        | "sqlite.statement.columns"
13597        | "sqlite.statement.setReturnArrays"
13598        | "sqlite.statement.setReadBigInts"
13599        | "sqlite.statement.setAllowBareNamedParameters"
13600        | "sqlite.statement.setAllowUnknownNamedParameters"
13601        | "sqlite.statement.finalize" => {
13602            service_javascript_sqlite_sync_rpc(kernel, process, request)
13603        }
13604        "process.kill" => {
13605            let target_pid =
13606                javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?;
13607            let signal = javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?;
13608            let parsed_signal = parse_signal(signal)?;
13609            if parsed_signal == 0 {
13610                kernel
13611                    .signal_process(EXECUTION_DRIVER_NAME, target_pid, parsed_signal)
13612                    .map_err(kernel_error)?;
13613                return Ok(Value::Null);
13614            }
13615            let process_pid = i32::try_from(process.kernel_pid)
13616                .map_err(|_| SidecarError::InvalidState("process pid exceeds i32".into()))?;
13617            if target_pid != process_pid {
13618                return Err(SidecarError::InvalidState(format!(
13619                    "unknown process pid {target_pid}"
13620                )));
13621            }
13622            process.pending_self_signal_exit = None;
13623            if parsed_signal != 0
13624                && !matches!(
13625                    canonical_signal_name(parsed_signal),
13626                    Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
13627                )
13628            {
13629                process.pending_self_signal_exit = Some(parsed_signal);
13630            }
13631            Ok(json!({
13632                "self": true,
13633                "action": "default",
13634            }))
13635        }
13636        "process.umask" => {
13637            let new_mask = javascript_sync_rpc_arg_u32_optional(&request.args, 0, "process umask")?;
13638            kernel
13639                .umask(EXECUTION_DRIVER_NAME, process.kernel_pid, new_mask)
13640                .map(|mask| json!(mask))
13641                .map_err(kernel_error)
13642        }
13643        "fs.chmodSync" | "fs.promises.chmod" => {
13644            let response =
13645                service_javascript_fs_sync_rpc(kernel, process, process.kernel_pid, request)?;
13646            mirror_process_chmod_to_host(process, request)?;
13647            Ok(response)
13648        }
13649        _ => service_javascript_fs_sync_rpc(kernel, process, process.kernel_pid, request),
13650    }
13651}
13652
13653fn service_javascript_internal_bridge_sync_rpc(
13654    process: &ActiveProcess,
13655    request: &JavascriptSyncRpcRequest,
13656) -> Result<Value, SidecarError> {
13657    // Module resolution / loading / format now reads the kernel VFS via
13658    // `service_javascript_module_sync_rpc`. This host-context path only handles
13659    // polyfills, which are static guest expressions independent of the FS.
13660    let method = match request.method.as_str() {
13661        "_loadPolyfill" | "__load_polyfill" => "_loadPolyfill",
13662        other => {
13663            return Err(SidecarError::InvalidState(format!(
13664                "unsupported JavaScript internal bridge method {other}"
13665            )));
13666        }
13667    };
13668
13669    handle_internal_bridge_call_from_host_context(
13670        &process.host_cwd,
13671        &process.guest_cwd,
13672        &process.env,
13673        method,
13674        &request.args,
13675    )
13676    .ok_or_else(|| {
13677        SidecarError::InvalidState(format!(
13678            "JavaScript internal bridge method {method} returned no value"
13679        ))
13680    })
13681}
13682
13683fn mirror_process_chmod_to_host(
13684    process: &ActiveProcess,
13685    request: &JavascriptSyncRpcRequest,
13686) -> Result<(), SidecarError> {
13687    let guest_path = javascript_sync_rpc_arg_str(&request.args, 0, "filesystem chmod path")?;
13688    let mode = javascript_sync_rpc_arg_u32(&request.args, 1, "filesystem chmod mode")? & 0o7777;
13689    let Some(host_path) = resolve_process_guest_path_to_host(process, guest_path) else {
13690        return Ok(());
13691    };
13692    if !host_path.exists() {
13693        return Ok(());
13694    }
13695    fs::set_permissions(&host_path, fs::Permissions::from_mode(mode)).map_err(|error| {
13696        SidecarError::Io(format!(
13697            "failed to mirror chmod to host path {}: {error}",
13698            host_path.display()
13699        ))
13700    })
13701}
13702
13703fn resolve_process_guest_path_to_host(
13704    process: &ActiveProcess,
13705    guest_path: &str,
13706) -> Option<PathBuf> {
13707    let normalized_guest_path = if guest_path.starts_with('/') {
13708        normalize_path(guest_path)
13709    } else {
13710        normalize_path(&format!(
13711            "{}/{}",
13712            process.guest_cwd.trim_end_matches('/'),
13713            guest_path
13714        ))
13715    };
13716    if let Some(host_path) =
13717        host_path_from_runtime_guest_mappings(&process.env, &normalized_guest_path)
13718    {
13719        return Some(host_path);
13720    }
13721    let normalized_guest_cwd = normalize_path(&process.guest_cwd);
13722    let mut host_root = normalize_host_path(&process.host_cwd);
13723    for _ in normalized_guest_cwd
13724        .trim_start_matches('/')
13725        .split('/')
13726        .filter(|segment| !segment.is_empty())
13727    {
13728        host_root = host_root.parent()?.to_path_buf();
13729    }
13730    if normalized_guest_path == "/" {
13731        Some(host_root)
13732    } else {
13733        Some(host_root.join(normalized_guest_path.trim_start_matches('/')))
13734    }
13735}
13736
13737pub(crate) fn service_javascript_crypto_sync_rpc(
13738    process: &mut ActiveProcess,
13739    request: &JavascriptSyncRpcRequest,
13740) -> Result<Value, SidecarError> {
13741    match request.method.as_str() {
13742        "crypto.hashDigest" => {
13743            let algorithm = javascript_crypto_digest_algorithm(
13744                &request.args,
13745                0,
13746                "crypto.hashDigest algorithm",
13747            )?;
13748            let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.hashDigest data")?;
13749            Ok(Value::String(
13750                base64::engine::general_purpose::STANDARD.encode(algorithm.digest(&data)),
13751            ))
13752        }
13753        "crypto.hmacDigest" => {
13754            let algorithm = javascript_crypto_digest_algorithm(
13755                &request.args,
13756                0,
13757                "crypto.hmacDigest algorithm",
13758            )?;
13759            let key = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.hmacDigest key")?;
13760            let data = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.hmacDigest data")?;
13761            Ok(Value::String(
13762                base64::engine::general_purpose::STANDARD.encode(algorithm.hmac(&key, &data)?),
13763            ))
13764        }
13765        "crypto.pbkdf2" => {
13766            let password =
13767                javascript_sync_rpc_base64_arg(&request.args, 0, "crypto.pbkdf2 password")?;
13768            let salt = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.pbkdf2 salt")?;
13769            let iterations =
13770                javascript_sync_rpc_arg_u32(&request.args, 2, "crypto.pbkdf2 iterations")?;
13771            if iterations == 0 {
13772                return Err(SidecarError::InvalidState(String::from(
13773                    "crypto.pbkdf2 iterations must be greater than zero",
13774                )));
13775            }
13776            let key_len = usize::try_from(javascript_sync_rpc_arg_u64(
13777                &request.args,
13778                3,
13779                "crypto.pbkdf2 key length",
13780            )?)
13781            .map_err(|_| {
13782                SidecarError::InvalidState(String::from(
13783                    "crypto.pbkdf2 key length must fit within usize",
13784                ))
13785            })?;
13786            let algorithm =
13787                javascript_crypto_digest_algorithm(&request.args, 4, "crypto.pbkdf2 digest")?;
13788            let mut output = vec![0u8; key_len];
13789            algorithm.pbkdf2(&password, &salt, iterations, &mut output);
13790            Ok(Value::String(
13791                base64::engine::general_purpose::STANDARD.encode(output),
13792            ))
13793        }
13794        "crypto.scrypt" => {
13795            let password =
13796                javascript_sync_rpc_base64_arg(&request.args, 0, "crypto.scrypt password")?;
13797            let salt = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.scrypt salt")?;
13798            let key_len = usize::try_from(javascript_sync_rpc_arg_u64(
13799                &request.args,
13800                2,
13801                "crypto.scrypt key length",
13802            )?)
13803            .map_err(|_| {
13804                SidecarError::InvalidState(String::from(
13805                    "crypto.scrypt key length must fit within usize",
13806                ))
13807            })?;
13808            let options_json =
13809                javascript_sync_rpc_arg_str(&request.args, 3, "crypto.scrypt options")?;
13810            let options: JavascriptScryptOptions =
13811                serde_json::from_str(options_json).map_err(|error| {
13812                    SidecarError::InvalidState(format!(
13813                        "crypto.scrypt options must be valid JSON: {error}"
13814                    ))
13815                })?;
13816            let cost = options.cost.unwrap_or(DEFAULT_SCRYPT_COST);
13817            if cost == 0 || !cost.is_power_of_two() {
13818                return Err(SidecarError::InvalidState(String::from(
13819                    "crypto.scrypt cost must be a positive power of two",
13820                )));
13821            }
13822            let log_n = u8::try_from(cost.ilog2()).map_err(|_| {
13823                SidecarError::InvalidState(String::from(
13824                    "crypto.scrypt cost exceeds supported parameter range",
13825                ))
13826            })?;
13827            let params = ScryptParams::new(
13828                log_n,
13829                options.block_size.unwrap_or(DEFAULT_SCRYPT_BLOCK_SIZE),
13830                options
13831                    .parallelization
13832                    .unwrap_or(DEFAULT_SCRYPT_PARALLELIZATION),
13833                key_len,
13834            )
13835            .map_err(|error| {
13836                SidecarError::InvalidState(format!("crypto.scrypt options are invalid: {error}"))
13837            })?;
13838            let mut output = vec![0u8; key_len];
13839            scrypt(&password, &salt, &params, &mut output).map_err(|error| {
13840                SidecarError::Execution(format!("crypto.scrypt failed: {error}"))
13841            })?;
13842            Ok(Value::String(
13843                base64::engine::general_purpose::STANDARD.encode(output),
13844            ))
13845        }
13846        "crypto.cipheriv" => service_javascript_crypto_cipheriv_sync_rpc(request),
13847        "crypto.decipheriv" => service_javascript_crypto_decipheriv_sync_rpc(request),
13848        "crypto.cipherivCreate" => {
13849            service_javascript_crypto_cipheriv_create_sync_rpc(process, request)
13850        }
13851        "crypto.cipherivUpdate" => {
13852            service_javascript_crypto_cipheriv_update_sync_rpc(process, request)
13853        }
13854        "crypto.cipherivFinal" => {
13855            service_javascript_crypto_cipheriv_final_sync_rpc(process, request)
13856        }
13857        "crypto.sign" => service_javascript_crypto_sign_sync_rpc(request),
13858        "crypto.verify" => service_javascript_crypto_verify_sync_rpc(request),
13859        "crypto.asymmetricOp" => service_javascript_crypto_asymmetric_op_sync_rpc(request),
13860        "crypto.createKeyObject" => service_javascript_crypto_create_key_object_sync_rpc(request),
13861        "crypto.generateKeyPairSync" => {
13862            service_javascript_crypto_generate_key_pair_sync_rpc(request)
13863        }
13864        "crypto.generateKeySync" => service_javascript_crypto_generate_key_sync_rpc(request),
13865        "crypto.generatePrimeSync" => service_javascript_crypto_generate_prime_sync_rpc(request),
13866        "crypto.diffieHellman" => service_javascript_crypto_diffie_hellman_sync_rpc(request),
13867        "crypto.diffieHellmanGroup" => {
13868            service_javascript_crypto_diffie_hellman_group_sync_rpc(request)
13869        }
13870        "crypto.diffieHellmanSessionCreate" => {
13871            service_javascript_crypto_diffie_hellman_session_create_sync_rpc(process, request)
13872        }
13873        "crypto.diffieHellmanSessionCall" => {
13874            service_javascript_crypto_diffie_hellman_session_call_sync_rpc(process, request)
13875        }
13876        "crypto.diffieHellmanSessionDestroy" => {
13877            service_javascript_crypto_diffie_hellman_session_destroy_sync_rpc(process, request)
13878        }
13879        "crypto.subtle" => service_javascript_crypto_subtle_sync_rpc(request),
13880        _ => Err(SidecarError::InvalidState(format!(
13881            "unsupported JavaScript crypto sync RPC method {}",
13882            request.method
13883        ))),
13884    }
13885}
13886
13887fn javascript_crypto_digest_algorithm(
13888    args: &[Value],
13889    index: usize,
13890    label: &str,
13891) -> Result<JavascriptCryptoDigestAlgorithm, SidecarError> {
13892    JavascriptCryptoDigestAlgorithm::parse(javascript_sync_rpc_arg_str(args, index, label)?)
13893}
13894
13895impl JavascriptCryptoDigestAlgorithm {
13896    fn parse(value: &str) -> Result<Self, SidecarError> {
13897        match value.trim().to_ascii_lowercase().replace('-', "").as_str() {
13898            "md5" => Ok(Self::Md5),
13899            "sha1" => Ok(Self::Sha1),
13900            "sha256" => Ok(Self::Sha256),
13901            "sha512" => Ok(Self::Sha512),
13902            _ => Err(SidecarError::InvalidState(format!(
13903                "unsupported crypto digest algorithm {value}"
13904            ))),
13905        }
13906    }
13907
13908    fn digest(self, data: &[u8]) -> Vec<u8> {
13909        match self {
13910            Self::Md5 => Md5::digest(data).to_vec(),
13911            Self::Sha1 => Sha1::digest(data).to_vec(),
13912            Self::Sha256 => Sha256::digest(data).to_vec(),
13913            Self::Sha512 => Sha512::digest(data).to_vec(),
13914        }
13915    }
13916
13917    fn hmac(self, key: &[u8], data: &[u8]) -> Result<Vec<u8>, SidecarError> {
13918        match self {
13919            Self::Md5 => {
13920                let mut mac = Hmac::<Md5>::new_from_slice(key).map_err(|error| {
13921                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
13922                })?;
13923                mac.update(data);
13924                Ok(mac.finalize().into_bytes().to_vec())
13925            }
13926            Self::Sha1 => {
13927                let mut mac = Hmac::<Sha1>::new_from_slice(key).map_err(|error| {
13928                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
13929                })?;
13930                mac.update(data);
13931                Ok(mac.finalize().into_bytes().to_vec())
13932            }
13933            Self::Sha256 => {
13934                let mut mac = Hmac::<Sha256>::new_from_slice(key).map_err(|error| {
13935                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
13936                })?;
13937                mac.update(data);
13938                Ok(mac.finalize().into_bytes().to_vec())
13939            }
13940            Self::Sha512 => {
13941                let mut mac = Hmac::<Sha512>::new_from_slice(key).map_err(|error| {
13942                    SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
13943                })?;
13944                mac.update(data);
13945                Ok(mac.finalize().into_bytes().to_vec())
13946            }
13947        }
13948    }
13949
13950    fn pbkdf2(self, password: &[u8], salt: &[u8], iterations: u32, output: &mut [u8]) {
13951        match self {
13952            Self::Md5 => pbkdf2_hmac::<Md5>(password, salt, iterations, output),
13953            Self::Sha1 => pbkdf2_hmac::<Sha1>(password, salt, iterations, output),
13954            Self::Sha256 => pbkdf2_hmac::<Sha256>(password, salt, iterations, output),
13955            Self::Sha512 => pbkdf2_hmac::<Sha512>(password, salt, iterations, output),
13956        }
13957    }
13958}
13959
13960#[derive(Debug, Clone)]
13961enum JavascriptCryptoKeyMaterial {
13962    Private(PKey<Private>),
13963    Public(PKey<Public>),
13964    Secret(Vec<u8>),
13965}
13966
13967#[derive(Debug, Clone, Deserialize, Serialize)]
13968struct JavascriptSerializedSandboxKeyObject {
13969    #[serde(rename = "type")]
13970    kind: String,
13971    #[serde(skip_serializing_if = "Option::is_none")]
13972    pem: Option<String>,
13973    #[serde(skip_serializing_if = "Option::is_none")]
13974    raw: Option<String>,
13975    #[serde(skip_serializing_if = "Option::is_none", rename = "asymmetricKeyType")]
13976    asymmetric_key_type: Option<String>,
13977    #[serde(
13978        skip_serializing_if = "Option::is_none",
13979        rename = "asymmetricKeyDetails"
13980    )]
13981    asymmetric_key_details: Option<Map<String, Value>>,
13982    #[serde(skip_serializing_if = "Option::is_none")]
13983    jwk: Option<Value>,
13984}
13985
13986#[derive(Debug, Clone)]
13987struct JavascriptDirectKeyInput {
13988    key: JavascriptCryptoKeyMaterial,
13989    padding: Option<Padding>,
13990}
13991
13992fn service_javascript_crypto_cipheriv_sync_rpc(
13993    request: &JavascriptSyncRpcRequest,
13994) -> Result<Value, SidecarError> {
13995    service_javascript_crypto_cipheriv_inner(request, false)
13996}
13997
13998fn service_javascript_crypto_decipheriv_sync_rpc(
13999    request: &JavascriptSyncRpcRequest,
14000) -> Result<Value, SidecarError> {
14001    service_javascript_crypto_cipheriv_inner(request, true)
14002}
14003
14004fn service_javascript_crypto_cipheriv_create_sync_rpc(
14005    process: &mut ActiveProcess,
14006    request: &JavascriptSyncRpcRequest,
14007) -> Result<Value, SidecarError> {
14008    ensure_per_process_state_handle_capacity(process.cipher_sessions.len(), "cipher session")?;
14009    let mode = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.cipherivCreate mode")?;
14010    let decrypt = mode == "decipher";
14011    let algorithm =
14012        javascript_sync_rpc_arg_str(&request.args, 1, "crypto.cipherivCreate algorithm")?;
14013    let key = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.cipherivCreate key")?;
14014    let iv = javascript_sync_rpc_base64_arg_optional(&request.args, 3, "crypto.cipherivCreate iv")?;
14015    let options =
14016        javascript_sync_rpc_json_arg_optional(&request.args, 4, "crypto.cipherivCreate options")?;
14017    let auth_tag_len = javascript_crypto_requested_aead_tag_len(algorithm, options.as_ref())?;
14018    let context = javascript_crypto_build_cipher_context(
14019        algorithm,
14020        &key,
14021        iv.as_deref(),
14022        decrypt,
14023        options.as_ref(),
14024    )?;
14025    process.next_cipher_session_id += 1;
14026    let session_id = process.next_cipher_session_id;
14027    process.cipher_sessions.insert(
14028        session_id,
14029        ActiveCipherSession {
14030            algorithm: algorithm.to_string(),
14031            auth_tag_len,
14032            context,
14033        },
14034    );
14035    Ok(json!(session_id))
14036}
14037
14038fn service_javascript_crypto_cipheriv_update_sync_rpc(
14039    process: &mut ActiveProcess,
14040    request: &JavascriptSyncRpcRequest,
14041) -> Result<Value, SidecarError> {
14042    let session_id =
14043        javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.cipherivUpdate session id")?;
14044    let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.cipherivUpdate data")?;
14045    let session = process
14046        .cipher_sessions
14047        .get_mut(&session_id)
14048        .ok_or_else(|| {
14049            SidecarError::InvalidState(format!("Cipher session {session_id} not found"))
14050        })?;
14051    let result = javascript_crypto_cipher_update(&mut session.context, &data)?;
14052    Ok(Value::String(
14053        base64::engine::general_purpose::STANDARD.encode(result),
14054    ))
14055}
14056
14057fn service_javascript_crypto_cipheriv_final_sync_rpc(
14058    process: &mut ActiveProcess,
14059    request: &JavascriptSyncRpcRequest,
14060) -> Result<Value, SidecarError> {
14061    let session_id =
14062        javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.cipherivFinal session id")?;
14063    let mut session = process.cipher_sessions.remove(&session_id).ok_or_else(|| {
14064        SidecarError::InvalidState(format!("Cipher session {session_id} not found"))
14065    })?;
14066    let data = javascript_crypto_cipher_finalize(&mut session.context)?;
14067    let mut response = Map::new();
14068    response.insert(
14069        String::from("data"),
14070        Value::String(base64::engine::general_purpose::STANDARD.encode(data)),
14071    );
14072    if javascript_crypto_is_aead(&session.algorithm) {
14073        let mut auth_tag = vec![0_u8; session.auth_tag_len];
14074        session
14075            .context
14076            .get_tag(&mut auth_tag)
14077            .map_err(javascript_crypto_openssl_error)?;
14078        response.insert(
14079            String::from("authTag"),
14080            Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
14081        );
14082    }
14083    Ok(Value::String(serde_json::to_string(&response).map_err(
14084        |error| SidecarError::InvalidState(format!("serialize cipher final response: {error}")),
14085    )?))
14086}
14087
14088fn service_javascript_crypto_sign_sync_rpc(
14089    request: &JavascriptSyncRpcRequest,
14090) -> Result<Value, SidecarError> {
14091    let algorithm = request.args.first().and_then(Value::as_str);
14092    let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.sign data")?;
14093    let key_json = javascript_sync_rpc_arg_str(&request.args, 2, "crypto.sign key")?;
14094    let key_input =
14095        javascript_crypto_parse_direct_key_input(key_json, Some("private"), "crypto.sign key")?;
14096    let private_key = javascript_crypto_expect_private_key(key_input.key, "crypto.sign key")?;
14097    let mut signer = javascript_crypto_new_signer(algorithm, &private_key)?;
14098    if let Some(padding) = key_input.padding {
14099        signer
14100            .set_rsa_padding(padding)
14101            .map_err(javascript_crypto_openssl_error)?;
14102    }
14103    signer
14104        .update(&data)
14105        .map_err(javascript_crypto_openssl_error)?;
14106    Ok(Value::String(
14107        base64::engine::general_purpose::STANDARD.encode(
14108            signer
14109                .sign_to_vec()
14110                .map_err(javascript_crypto_openssl_error)?,
14111        ),
14112    ))
14113}
14114
14115fn service_javascript_crypto_verify_sync_rpc(
14116    request: &JavascriptSyncRpcRequest,
14117) -> Result<Value, SidecarError> {
14118    let algorithm = request.args.first().and_then(Value::as_str);
14119    let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.verify data")?;
14120    let key_json = javascript_sync_rpc_arg_str(&request.args, 2, "crypto.verify key")?;
14121    let signature = javascript_sync_rpc_base64_arg(&request.args, 3, "crypto.verify signature")?;
14122    let key_input =
14123        javascript_crypto_parse_direct_key_input(key_json, Some("public"), "crypto.verify key")?;
14124    let public_key = javascript_crypto_expect_public_key(key_input.key, "crypto.verify key")?;
14125    let mut verifier = javascript_crypto_new_verifier(algorithm, &public_key)?;
14126    if let Some(padding) = key_input.padding {
14127        verifier
14128            .set_rsa_padding(padding)
14129            .map_err(javascript_crypto_openssl_error)?;
14130    }
14131    verifier
14132        .update(&data)
14133        .map_err(javascript_crypto_openssl_error)?;
14134    Ok(json!(verifier
14135        .verify(&signature)
14136        .map_err(javascript_crypto_openssl_error)?))
14137}
14138
14139fn service_javascript_crypto_asymmetric_op_sync_rpc(
14140    request: &JavascriptSyncRpcRequest,
14141) -> Result<Value, SidecarError> {
14142    let operation = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.asymmetricOp operation")?;
14143    let key_json = javascript_sync_rpc_arg_str(&request.args, 1, "crypto.asymmetricOp key")?;
14144    let data = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.asymmetricOp data")?;
14145    let expect_kind = match operation {
14146        "publicEncrypt" | "publicDecrypt" => Some("public"),
14147        "privateEncrypt" | "privateDecrypt" => Some("private"),
14148        other => {
14149            return Err(SidecarError::InvalidState(format!(
14150                "Unsupported asymmetric crypto operation: {other}"
14151            )));
14152        }
14153    };
14154    let key_input =
14155        javascript_crypto_parse_direct_key_input(key_json, expect_kind, "crypto.asymmetricOp key")?;
14156    let padding = key_input.padding.unwrap_or(Padding::PKCS1);
14157    let mut output = vec![0_u8; javascript_crypto_rsa_output_size(&key_input.key)?];
14158    let written = match (operation, key_input.key) {
14159        ("publicEncrypt", JavascriptCryptoKeyMaterial::Public(key))
14160        | ("publicDecrypt", JavascriptCryptoKeyMaterial::Public(key)) => {
14161            let rsa = key.rsa().map_err(javascript_crypto_openssl_error)?;
14162            if operation == "publicEncrypt" {
14163                rsa.public_encrypt(&data, &mut output, padding)
14164                    .map_err(javascript_crypto_openssl_error)?
14165            } else {
14166                rsa.public_decrypt(&data, &mut output, padding)
14167                    .map_err(javascript_crypto_openssl_error)?
14168            }
14169        }
14170        ("privateEncrypt", JavascriptCryptoKeyMaterial::Private(key))
14171        | ("privateDecrypt", JavascriptCryptoKeyMaterial::Private(key)) => {
14172            let rsa = key.rsa().map_err(javascript_crypto_openssl_error)?;
14173            if operation == "privateEncrypt" {
14174                rsa.private_encrypt(&data, &mut output, padding)
14175                    .map_err(javascript_crypto_openssl_error)?
14176            } else {
14177                rsa.private_decrypt(&data, &mut output, padding)
14178                    .map_err(javascript_crypto_openssl_error)?
14179            }
14180        }
14181        _ => {
14182            return Err(SidecarError::InvalidState(format!(
14183                "{operation} requires an RSA {} key",
14184                expect_kind.unwrap_or("asymmetric")
14185            )));
14186        }
14187    };
14188    output.truncate(written);
14189    Ok(Value::String(
14190        base64::engine::general_purpose::STANDARD.encode(output),
14191    ))
14192}
14193
14194fn service_javascript_crypto_create_key_object_sync_rpc(
14195    request: &JavascriptSyncRpcRequest,
14196) -> Result<Value, SidecarError> {
14197    let operation =
14198        javascript_sync_rpc_arg_str(&request.args, 0, "crypto.createKeyObject operation")?;
14199    let key_json = javascript_sync_rpc_arg_str(&request.args, 1, "crypto.createKeyObject key")?;
14200    let expected = match operation {
14201        "createPrivateKey" => Some("private"),
14202        "createPublicKey" => Some("public"),
14203        other => {
14204            return Err(SidecarError::InvalidState(format!(
14205                "Unsupported key creation operation: {other}"
14206            )));
14207        }
14208    };
14209    let key_input =
14210        javascript_crypto_parse_direct_key_input(key_json, expected, "crypto.createKeyObject key")?;
14211    Ok(Value::String(
14212        serde_json::to_string(&javascript_crypto_serialize_sandbox_key_object(
14213            &key_input.key,
14214        )?)
14215        .map_err(|error| {
14216            SidecarError::InvalidState(format!("serialize crypto key object: {error}"))
14217        })?,
14218    ))
14219}
14220
14221fn service_javascript_crypto_generate_key_pair_sync_rpc(
14222    request: &JavascriptSyncRpcRequest,
14223) -> Result<Value, SidecarError> {
14224    let key_type =
14225        javascript_sync_rpc_arg_str(&request.args, 0, "crypto.generateKeyPairSync type")?;
14226    let options = javascript_crypto_parse_serialized_options_arg(
14227        &request.args,
14228        1,
14229        "crypto.generateKeyPairSync options",
14230    )?
14231    .unwrap_or(Value::Object(Map::new()));
14232    let public_encoding = options.get("publicKeyEncoding").cloned();
14233    let private_encoding = options.get("privateKeyEncoding").cloned();
14234
14235    let private_key = match key_type {
14236        "rsa" => {
14237            let bits = options
14238                .get("modulusLength")
14239                .and_then(Value::as_u64)
14240                .unwrap_or(2048) as u32;
14241            let exponent = options
14242                .get("publicExponent")
14243                .map(|value| javascript_crypto_u32_from_bridge_value(value, "rsa publicExponent"))
14244                .transpose()?
14245                .unwrap_or(65_537);
14246            let exponent = BigNum::from_u32(exponent).map_err(javascript_crypto_openssl_error)?;
14247            let rsa =
14248                Rsa::generate_with_e(bits, &exponent).map_err(javascript_crypto_openssl_error)?;
14249            PKey::from_rsa(rsa).map_err(javascript_crypto_openssl_error)?
14250        }
14251        "ec" => {
14252            let named_curve = options
14253                .get("namedCurve")
14254                .and_then(Value::as_str)
14255                .ok_or_else(|| {
14256                    SidecarError::InvalidState(String::from(
14257                        "crypto.generateKeyPairSync ec requires namedCurve",
14258                    ))
14259                })?;
14260            let group = EcGroup::from_curve_name(javascript_crypto_curve_nid(named_curve)?)
14261                .map_err(javascript_crypto_openssl_error)?;
14262            let key = EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?;
14263            PKey::from_ec_key(key).map_err(javascript_crypto_openssl_error)?
14264        }
14265        "ed25519" => PKey::generate_ed25519().map_err(javascript_crypto_openssl_error)?,
14266        "x25519" => PKey::generate_x25519().map_err(javascript_crypto_openssl_error)?,
14267        other => {
14268            return Err(SidecarError::InvalidState(format!(
14269                "unsupported crypto key pair type {other}"
14270            )));
14271        }
14272    };
14273    let public_key = PKey::public_key_from_pem(
14274        &private_key
14275            .public_key_to_pem()
14276            .map_err(javascript_crypto_openssl_error)?,
14277    )
14278    .map_err(javascript_crypto_openssl_error)?;
14279    let response = if public_encoding.is_some() || private_encoding.is_some() {
14280        json!({
14281            "publicKey": javascript_crypto_serialize_encoded_key_value_public(&public_key, public_encoding.as_ref())?,
14282            "privateKey": javascript_crypto_serialize_encoded_key_value_private(&private_key, private_encoding.as_ref())?,
14283        })
14284    } else {
14285        json!({
14286            "publicKey": javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Public(public_key))?,
14287            "privateKey": javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Private(private_key))?,
14288        })
14289    };
14290    Ok(Value::String(serde_json::to_string(&response).map_err(
14291        |error| SidecarError::InvalidState(format!("serialize generated key pair: {error}")),
14292    )?))
14293}
14294
14295fn service_javascript_crypto_generate_key_sync_rpc(
14296    request: &JavascriptSyncRpcRequest,
14297) -> Result<Value, SidecarError> {
14298    let key_type = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.generateKeySync type")?;
14299    let options = javascript_crypto_parse_serialized_options_arg(
14300        &request.args,
14301        1,
14302        "crypto.generateKeySync options",
14303    )?
14304    .unwrap_or(Value::Object(Map::new()));
14305    let bit_length = options
14306        .get("length")
14307        .and_then(Value::as_u64)
14308        .ok_or_else(|| {
14309            SidecarError::InvalidState(String::from(
14310                "crypto.generateKeySync options.length is required",
14311            ))
14312        })? as usize;
14313    let mut raw = vec![0_u8; bit_length.div_ceil(8)];
14314    rand_bytes(&mut raw).map_err(javascript_crypto_openssl_error)?;
14315    let serialized = match key_type {
14316        "hmac" => javascript_crypto_serialize_sandbox_key_object(
14317            &JavascriptCryptoKeyMaterial::Secret(raw),
14318        )?,
14319        "aes" => javascript_crypto_serialize_sandbox_key_object(
14320            &JavascriptCryptoKeyMaterial::Secret(raw),
14321        )?,
14322        other => {
14323            return Err(SidecarError::InvalidState(format!(
14324                "unsupported crypto.generateKeySync type {other}"
14325            )));
14326        }
14327    };
14328    Ok(Value::String(serde_json::to_string(&serialized).map_err(
14329        |error| SidecarError::InvalidState(format!("serialize generated key: {error}")),
14330    )?))
14331}
14332
14333fn service_javascript_crypto_generate_prime_sync_rpc(
14334    request: &JavascriptSyncRpcRequest,
14335) -> Result<Value, SidecarError> {
14336    let bits =
14337        javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.generatePrimeSync size")? as i32;
14338    let options = javascript_crypto_parse_serialized_options_arg(
14339        &request.args,
14340        1,
14341        "crypto.generatePrimeSync options",
14342    )?
14343    .unwrap_or(Value::Object(Map::new()));
14344    let safe = options
14345        .get("safe")
14346        .and_then(Value::as_bool)
14347        .unwrap_or(false);
14348    let add = options
14349        .get("add")
14350        .map(|value| javascript_crypto_bignum_from_bridge_value(value, "prime add"))
14351        .transpose()?;
14352    let rem = options
14353        .get("rem")
14354        .map(|value| javascript_crypto_bignum_from_bridge_value(value, "prime rem"))
14355        .transpose()?;
14356    let mut prime = BigNum::new().map_err(javascript_crypto_openssl_error)?;
14357    prime
14358        .generate_prime(bits, safe, add.as_deref(), rem.as_deref())
14359        .map_err(javascript_crypto_openssl_error)?;
14360    let payload = if options
14361        .get("bigint")
14362        .and_then(Value::as_bool)
14363        .unwrap_or(false)
14364    {
14365        json!({
14366            "__type": "bigint",
14367            "value": prime.to_dec_str().map_err(javascript_crypto_openssl_error)?.to_string(),
14368        })
14369    } else {
14370        json!({
14371            "__type": "buffer",
14372            "value": base64::engine::general_purpose::STANDARD.encode(prime.to_vec()),
14373        })
14374    };
14375    Ok(Value::String(serde_json::to_string(&payload).map_err(
14376        |error| SidecarError::InvalidState(format!("serialize generated prime: {error}")),
14377    )?))
14378}
14379
14380fn service_javascript_crypto_diffie_hellman_sync_rpc(
14381    request: &JavascriptSyncRpcRequest,
14382) -> Result<Value, SidecarError> {
14383    let options = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.diffieHellman options")?;
14384    let parsed: Value = serde_json::from_str(options).map_err(|error| {
14385        SidecarError::InvalidState(format!(
14386            "crypto.diffieHellman options must be valid JSON: {error}"
14387        ))
14388    })?;
14389    let private_key = javascript_crypto_parse_key_material_value(
14390        parsed.get("privateKey").ok_or_else(|| {
14391            SidecarError::InvalidState(String::from("crypto.diffieHellman missing privateKey"))
14392        })?,
14393        Some("private"),
14394        "crypto.diffieHellman privateKey",
14395    )?;
14396    let public_key = javascript_crypto_parse_key_material_value(
14397        parsed.get("publicKey").ok_or_else(|| {
14398            SidecarError::InvalidState(String::from("crypto.diffieHellman missing publicKey"))
14399        })?,
14400        Some("public"),
14401        "crypto.diffieHellman publicKey",
14402    )?;
14403    let private_key =
14404        javascript_crypto_expect_private_key(private_key, "crypto.diffieHellman privateKey")?;
14405    let public_key =
14406        javascript_crypto_expect_public_key(public_key, "crypto.diffieHellman publicKey")?;
14407    let mut deriver = Deriver::new(&private_key).map_err(javascript_crypto_openssl_error)?;
14408    deriver
14409        .set_peer(&public_key)
14410        .map_err(javascript_crypto_openssl_error)?;
14411    let secret = deriver
14412        .derive_to_vec()
14413        .map_err(javascript_crypto_openssl_error)?;
14414    Ok(Value::String(
14415        serde_json::to_string(&json!({
14416            "__type": "buffer",
14417            "value": base64::engine::general_purpose::STANDARD.encode(secret),
14418        }))
14419        .map_err(|error| {
14420            SidecarError::InvalidState(format!("serialize derived secret: {error}"))
14421        })?,
14422    ))
14423}
14424
14425fn service_javascript_crypto_diffie_hellman_group_sync_rpc(
14426    request: &JavascriptSyncRpcRequest,
14427) -> Result<Value, SidecarError> {
14428    let name = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.diffieHellmanGroup name")?;
14429    let params = javascript_crypto_named_dh_group(name)?;
14430    let response = json!({
14431        "prime": {
14432            "__type": "buffer",
14433            "value": base64::engine::general_purpose::STANDARD.encode(params.prime_p().to_vec()),
14434        },
14435        "generator": {
14436            "__type": "buffer",
14437            "value": base64::engine::general_purpose::STANDARD.encode(params.generator().to_vec()),
14438        },
14439    });
14440    Ok(Value::String(serde_json::to_string(&response).map_err(
14441        |error| {
14442            SidecarError::InvalidState(format!("serialize diffieHellmanGroup response: {error}"))
14443        },
14444    )?))
14445}
14446
14447fn service_javascript_crypto_diffie_hellman_session_create_sync_rpc(
14448    process: &mut ActiveProcess,
14449    request: &JavascriptSyncRpcRequest,
14450) -> Result<Value, SidecarError> {
14451    ensure_per_process_state_handle_capacity(
14452        process.diffie_hellman_sessions.len(),
14453        "diffie-hellman session",
14454    )?;
14455    let raw = javascript_sync_rpc_arg_str(
14456        &request.args,
14457        0,
14458        "crypto.diffieHellmanSessionCreate request",
14459    )?;
14460    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14461        SidecarError::InvalidState(format!(
14462            "crypto.diffieHellmanSessionCreate request must be valid JSON: {error}"
14463        ))
14464    })?;
14465    let session = match parsed.get("type").and_then(Value::as_str) {
14466        Some("group") => {
14467            let name = parsed.get("name").and_then(Value::as_str).ok_or_else(|| {
14468                SidecarError::InvalidState(String::from(
14469                    "crypto.diffieHellmanSessionCreate group requires name",
14470                ))
14471            })?;
14472            ActiveDiffieHellmanSession::Dh(ActiveDhSession {
14473                params: javascript_crypto_named_dh_group(name)?,
14474                key_pair: None,
14475            })
14476        }
14477        Some("dh") => {
14478            let args = parsed
14479                .get("args")
14480                .and_then(Value::as_array)
14481                .ok_or_else(|| {
14482                    SidecarError::InvalidState(String::from(
14483                        "crypto.diffieHellmanSessionCreate dh requires args",
14484                    ))
14485                })?;
14486            let params = javascript_crypto_build_dh_params(args)?;
14487            ActiveDiffieHellmanSession::Dh(ActiveDhSession {
14488                params,
14489                key_pair: None,
14490            })
14491        }
14492        Some("ecdh") => {
14493            let curve = parsed.get("name").and_then(Value::as_str).ok_or_else(|| {
14494                SidecarError::InvalidState(String::from(
14495                    "crypto.diffieHellmanSessionCreate ecdh requires name",
14496                ))
14497            })?;
14498            ActiveDiffieHellmanSession::Ecdh(ActiveEcdhSession {
14499                curve: curve.to_string(),
14500                key_pair: None,
14501            })
14502        }
14503        other => {
14504            return Err(SidecarError::InvalidState(format!(
14505                "Unsupported Diffie-Hellman session type: {}",
14506                other.unwrap_or("<missing>")
14507            )));
14508        }
14509    };
14510    process.next_diffie_hellman_session_id += 1;
14511    let session_id = process.next_diffie_hellman_session_id;
14512    process.diffie_hellman_sessions.insert(session_id, session);
14513    Ok(json!(session_id))
14514}
14515
14516fn service_javascript_crypto_diffie_hellman_session_call_sync_rpc(
14517    process: &mut ActiveProcess,
14518    request: &JavascriptSyncRpcRequest,
14519) -> Result<Value, SidecarError> {
14520    let session_id = javascript_sync_rpc_arg_u64(
14521        &request.args,
14522        0,
14523        "crypto.diffieHellmanSessionCall session id",
14524    )?;
14525    let raw =
14526        javascript_sync_rpc_arg_str(&request.args, 1, "crypto.diffieHellmanSessionCall request")?;
14527    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14528        SidecarError::InvalidState(format!(
14529            "crypto.diffieHellmanSessionCall request must be valid JSON: {error}"
14530        ))
14531    })?;
14532    let method = parsed
14533        .get("method")
14534        .and_then(Value::as_str)
14535        .ok_or_else(|| {
14536            SidecarError::InvalidState(String::from(
14537                "crypto.diffieHellmanSessionCall request missing method",
14538            ))
14539        })?;
14540    let args = parsed
14541        .get("args")
14542        .and_then(Value::as_array)
14543        .cloned()
14544        .unwrap_or_default();
14545    let session = process
14546        .diffie_hellman_sessions
14547        .get_mut(&session_id)
14548        .ok_or_else(|| {
14549            SidecarError::InvalidState(format!("Diffie-Hellman session {session_id} not found"))
14550        })?;
14551    let (result, has_result) = match session {
14552        ActiveDiffieHellmanSession::Dh(session) => {
14553            javascript_crypto_call_dh_session(session, method, &args)?
14554        }
14555        ActiveDiffieHellmanSession::Ecdh(session) => {
14556            javascript_crypto_call_ecdh_session(session, method, &args)?
14557        }
14558    };
14559    Ok(Value::String(
14560        serde_json::to_string(&json!({
14561            "result": result,
14562            "hasResult": has_result,
14563        }))
14564        .map_err(|error| {
14565            SidecarError::InvalidState(format!("serialize diffie session result: {error}"))
14566        })?,
14567    ))
14568}
14569
14570fn service_javascript_crypto_diffie_hellman_session_destroy_sync_rpc(
14571    process: &mut ActiveProcess,
14572    request: &JavascriptSyncRpcRequest,
14573) -> Result<Value, SidecarError> {
14574    let session_id = javascript_sync_rpc_arg_u64(
14575        &request.args,
14576        0,
14577        "crypto.diffieHellmanSessionDestroy session id",
14578    )?;
14579    process
14580        .diffie_hellman_sessions
14581        .remove(&session_id)
14582        .ok_or_else(|| {
14583            SidecarError::InvalidState(format!("Diffie-Hellman session {session_id} not found"))
14584        })?;
14585    Ok(Value::Null)
14586}
14587
14588fn service_javascript_crypto_subtle_sync_rpc(
14589    request: &JavascriptSyncRpcRequest,
14590) -> Result<Value, SidecarError> {
14591    let raw = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.subtle request")?;
14592    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14593        SidecarError::InvalidState(format!("crypto.subtle request must be valid JSON: {error}"))
14594    })?;
14595    let op = parsed.get("op").and_then(Value::as_str).ok_or_else(|| {
14596        SidecarError::InvalidState(String::from("crypto.subtle request missing op"))
14597    })?;
14598    match op {
14599        "digest" => {
14600            let algorithm = parsed
14601                .get("algorithm")
14602                .and_then(Value::as_str)
14603                .ok_or_else(|| {
14604                    SidecarError::InvalidState(String::from(
14605                        "crypto.subtle.digest missing algorithm",
14606                    ))
14607                })?;
14608            let data = parsed.get("data").and_then(Value::as_str).ok_or_else(|| {
14609                SidecarError::InvalidState(String::from("crypto.subtle.digest missing data"))
14610            })?;
14611            let bytes = base64::engine::general_purpose::STANDARD
14612                .decode(data)
14613                .map_err(|error| {
14614                    SidecarError::InvalidState(format!("crypto.subtle.digest data base64: {error}"))
14615                })?;
14616            let digest = JavascriptCryptoDigestAlgorithm::parse(algorithm)?.digest(&bytes);
14617            Ok(Value::String(
14618                serde_json::to_string(&json!({
14619                    "data": base64::engine::general_purpose::STANDARD.encode(digest),
14620                }))
14621                .map_err(|error| {
14622                    SidecarError::InvalidState(format!("serialize crypto.subtle digest: {error}"))
14623                })?,
14624            ))
14625        }
14626        "generateKey" => {
14627            let algorithm = parsed.get("algorithm").ok_or_else(|| {
14628                SidecarError::InvalidState(String::from(
14629                    "crypto.subtle.generateKey missing algorithm",
14630                ))
14631            })?;
14632            let name =
14633                javascript_crypto_subtle_algorithm_name(algorithm, "crypto.subtle.generateKey")?;
14634            if !matches!(name, "AES-GCM" | "AES-CBC" | "AES-CTR" | "AES-KW") {
14635                return Err(SidecarError::InvalidState(format!(
14636                    "Unsupported key algorithm: {name}"
14637                )));
14638            }
14639            let length_bits = algorithm
14640                .get("length")
14641                .and_then(Value::as_u64)
14642                .ok_or_else(|| {
14643                    SidecarError::InvalidState(String::from(
14644                        "crypto.subtle.generateKey AES algorithm requires length",
14645                    ))
14646                })?;
14647            if length_bits % 8 != 0 {
14648                return Err(SidecarError::InvalidState(String::from(
14649                    "crypto.subtle.generateKey length must be byte-aligned",
14650                )));
14651            }
14652            let length_bytes = usize::try_from(length_bits / 8).map_err(|_| {
14653                SidecarError::InvalidState(String::from(
14654                    "crypto.subtle.generateKey length is too large",
14655                ))
14656            })?;
14657            let mut raw = vec![0_u8; length_bytes];
14658            rand_bytes(&mut raw).map_err(javascript_crypto_openssl_error)?;
14659            let key = javascript_crypto_serialize_subtle_secret_key(
14660                &raw,
14661                javascript_crypto_normalize_subtle_secret_algorithm(algorithm.clone(), &raw)?,
14662                parsed
14663                    .get("extractable")
14664                    .and_then(Value::as_bool)
14665                    .unwrap_or(false),
14666                parsed.get("usages").cloned().unwrap_or_else(|| json!([])),
14667            )?;
14668            Ok(Value::String(
14669                serde_json::to_string(&json!({ "key": key })).map_err(|error| {
14670                    SidecarError::InvalidState(format!(
14671                        "serialize crypto.subtle generated key: {error}"
14672                    ))
14673                })?,
14674            ))
14675        }
14676        "importKey" => {
14677            let format = parsed
14678                .get("format")
14679                .and_then(Value::as_str)
14680                .ok_or_else(|| {
14681                    SidecarError::InvalidState(String::from(
14682                        "crypto.subtle.importKey missing format",
14683                    ))
14684                })?;
14685            if format != "raw" {
14686                return Err(SidecarError::InvalidState(format!(
14687                    "Unsupported import format: {format}"
14688                )));
14689            }
14690            let key_data = parsed
14691                .get("keyData")
14692                .and_then(Value::as_str)
14693                .ok_or_else(|| {
14694                    SidecarError::InvalidState(String::from(
14695                        "crypto.subtle.importKey missing keyData",
14696                    ))
14697                })?;
14698            let raw = base64::engine::general_purpose::STANDARD
14699                .decode(key_data)
14700                .map_err(|error| {
14701                    SidecarError::InvalidState(format!(
14702                        "crypto.subtle.importKey keyData base64: {error}"
14703                    ))
14704                })?;
14705            let algorithm = parsed.get("algorithm").ok_or_else(|| {
14706                SidecarError::InvalidState(String::from(
14707                    "crypto.subtle.importKey missing algorithm",
14708                ))
14709            })?;
14710            let key = javascript_crypto_serialize_subtle_secret_key(
14711                &raw,
14712                javascript_crypto_normalize_subtle_secret_algorithm(algorithm.clone(), &raw)?,
14713                parsed
14714                    .get("extractable")
14715                    .and_then(Value::as_bool)
14716                    .unwrap_or(false),
14717                parsed.get("usages").cloned().unwrap_or_else(|| json!([])),
14718            )?;
14719            Ok(Value::String(
14720                serde_json::to_string(&json!({ "key": key })).map_err(|error| {
14721                    SidecarError::InvalidState(format!(
14722                        "serialize crypto.subtle imported key: {error}"
14723                    ))
14724                })?,
14725            ))
14726        }
14727        "exportKey" => {
14728            let format = parsed
14729                .get("format")
14730                .and_then(Value::as_str)
14731                .ok_or_else(|| {
14732                    SidecarError::InvalidState(String::from(
14733                        "crypto.subtle.exportKey missing format",
14734                    ))
14735                })?;
14736            if format != "raw" {
14737                return Err(SidecarError::InvalidState(format!(
14738                    "Unsupported export format: {format}"
14739                )));
14740            }
14741            let raw = javascript_crypto_subtle_key_raw(
14742                parsed.get("key").ok_or_else(|| {
14743                    SidecarError::InvalidState(String::from("crypto.subtle.exportKey missing key"))
14744                })?,
14745                "crypto.subtle.exportKey key",
14746            )?;
14747            Ok(Value::String(
14748                serde_json::to_string(&json!({
14749                    "data": base64::engine::general_purpose::STANDARD.encode(raw),
14750                }))
14751                .map_err(|error| {
14752                    SidecarError::InvalidState(format!("serialize crypto.subtle export: {error}"))
14753                })?,
14754            ))
14755        }
14756        "encrypt" | "decrypt" => service_javascript_crypto_subtle_aes_crypt_sync_rpc(op, &parsed),
14757        _ => Err(SidecarError::InvalidState(format!(
14758            "Unsupported subtle operation: {op}"
14759        ))),
14760    }
14761}
14762
14763fn javascript_crypto_subtle_algorithm_name<'a>(
14764    algorithm: &'a Value,
14765    label: &str,
14766) -> Result<&'a str, SidecarError> {
14767    if let Some(name) = algorithm.as_str() {
14768        return Ok(name);
14769    }
14770    algorithm
14771        .get("name")
14772        .and_then(Value::as_str)
14773        .ok_or_else(|| SidecarError::InvalidState(format!("{label} algorithm missing name")))
14774}
14775
14776fn javascript_crypto_normalize_subtle_secret_algorithm(
14777    algorithm: Value,
14778    raw: &[u8],
14779) -> Result<Value, SidecarError> {
14780    let mut object = match algorithm {
14781        Value::String(name) => {
14782            let mut object = Map::new();
14783            object.insert(String::from("name"), Value::String(name));
14784            object
14785        }
14786        Value::Object(object) => object,
14787        _ => {
14788            return Err(SidecarError::InvalidState(String::from(
14789                "crypto.subtle secret algorithm must be a string or object",
14790            )));
14791        }
14792    };
14793    let name = object
14794        .get("name")
14795        .and_then(Value::as_str)
14796        .ok_or_else(|| {
14797            SidecarError::InvalidState(String::from("crypto.subtle secret algorithm missing name"))
14798        })?
14799        .to_string();
14800    if matches!(name.as_str(), "AES-GCM" | "AES-CBC" | "AES-CTR" | "AES-KW")
14801        && !object.contains_key("length")
14802    {
14803        object.insert(String::from("length"), json!(raw.len() * 8));
14804    }
14805    Ok(Value::Object(object))
14806}
14807
14808fn javascript_crypto_serialize_subtle_secret_key(
14809    raw: &[u8],
14810    algorithm: Value,
14811    extractable: bool,
14812    usages: Value,
14813) -> Result<Value, SidecarError> {
14814    let raw_base64 = base64::engine::general_purpose::STANDARD.encode(raw);
14815    let source_key_object_data = javascript_crypto_serialize_sandbox_key_object(
14816        &JavascriptCryptoKeyMaterial::Secret(raw.to_vec()),
14817    )?;
14818    Ok(json!({
14819        "type": "secret",
14820        "algorithm": algorithm,
14821        "extractable": extractable,
14822        "usages": usages,
14823        "_raw": raw_base64,
14824        "_sourceKeyObjectData": source_key_object_data,
14825    }))
14826}
14827
14828fn javascript_crypto_subtle_key_raw(key: &Value, label: &str) -> Result<Vec<u8>, SidecarError> {
14829    let raw = key.get("_raw").and_then(Value::as_str).ok_or_else(|| {
14830        SidecarError::InvalidState(format!("{label} must be a raw secret CryptoKey"))
14831    })?;
14832    base64::engine::general_purpose::STANDARD
14833        .decode(raw)
14834        .map_err(|error| SidecarError::InvalidState(format!("{label} raw base64: {error}")))
14835}
14836
14837fn service_javascript_crypto_subtle_aes_crypt_sync_rpc(
14838    op: &str,
14839    parsed: &Value,
14840) -> Result<Value, SidecarError> {
14841    let algorithm = parsed.get("algorithm").ok_or_else(|| {
14842        SidecarError::InvalidState(format!("crypto.subtle.{op} missing algorithm"))
14843    })?;
14844    let name = javascript_crypto_subtle_algorithm_name(algorithm, &format!("crypto.subtle.{op}"))?;
14845    if name != "AES-GCM" {
14846        return Err(SidecarError::InvalidState(format!(
14847            "Unsupported subtle AES operation algorithm: {name}"
14848        )));
14849    }
14850    let key = javascript_crypto_subtle_key_raw(
14851        parsed
14852            .get("key")
14853            .ok_or_else(|| SidecarError::InvalidState(format!("crypto.subtle.{op} missing key")))?,
14854        &format!("crypto.subtle.{op} key"),
14855    )?;
14856    let iv = algorithm.get("iv").and_then(Value::as_str).ok_or_else(|| {
14857        SidecarError::InvalidState(format!("crypto.subtle.{op} AES-GCM missing iv"))
14858    })?;
14859    let iv = base64::engine::general_purpose::STANDARD
14860        .decode(iv)
14861        .map_err(|error| {
14862            SidecarError::InvalidState(format!("crypto.subtle.{op} iv base64: {error}"))
14863        })?;
14864    let data = parsed
14865        .get("data")
14866        .and_then(Value::as_str)
14867        .ok_or_else(|| SidecarError::InvalidState(format!("crypto.subtle.{op} missing data")))?;
14868    let mut data = base64::engine::general_purpose::STANDARD
14869        .decode(data)
14870        .map_err(|error| {
14871            SidecarError::InvalidState(format!("crypto.subtle.{op} data base64: {error}"))
14872        })?;
14873    let tag_len = javascript_crypto_subtle_aes_gcm_tag_len(algorithm)?;
14874    let mut options = Map::new();
14875    options.insert(String::from("authTagLength"), json!(tag_len));
14876    if let Some(additional_data) = algorithm.get("additionalData").and_then(Value::as_str) {
14877        options.insert(
14878            String::from("aad"),
14879            Value::String(additional_data.to_string()),
14880        );
14881    }
14882    let decrypt = op == "decrypt";
14883    if decrypt {
14884        if data.len() < tag_len {
14885            return Err(SidecarError::InvalidState(String::from(
14886                "crypto.subtle.decrypt AES-GCM data shorter than auth tag",
14887            )));
14888        }
14889        let auth_tag = data.split_off(data.len() - tag_len);
14890        options.insert(
14891            String::from("authTag"),
14892            Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
14893        );
14894    }
14895    let cipher_name = format!("aes-{}-gcm", key.len() * 8);
14896    let mut context = javascript_crypto_build_cipher_context(
14897        &cipher_name,
14898        &key,
14899        Some(&iv),
14900        decrypt,
14901        Some(&Value::Object(options)),
14902    )?;
14903    let mut output = javascript_crypto_cipher_update(&mut context, &data)?;
14904    output.extend(javascript_crypto_cipher_finalize(&mut context)?);
14905    if !decrypt {
14906        let mut auth_tag = vec![0_u8; tag_len];
14907        context
14908            .get_tag(&mut auth_tag)
14909            .map_err(javascript_crypto_openssl_error)?;
14910        output.extend(auth_tag);
14911    }
14912    Ok(Value::String(
14913        serde_json::to_string(&json!({
14914            "data": base64::engine::general_purpose::STANDARD.encode(output),
14915        }))
14916        .map_err(|error| {
14917            SidecarError::InvalidState(format!("serialize crypto.subtle {op}: {error}"))
14918        })?,
14919    ))
14920}
14921
14922fn javascript_crypto_subtle_aes_gcm_tag_len(algorithm: &Value) -> Result<usize, SidecarError> {
14923    let tag_bits = algorithm
14924        .get("tagLength")
14925        .and_then(Value::as_u64)
14926        .unwrap_or(128);
14927    if !tag_bits.is_multiple_of(8) {
14928        return Err(SidecarError::InvalidState(String::from(
14929            "crypto.subtle AES-GCM tagLength must be byte-aligned",
14930        )));
14931    }
14932    usize::try_from(tag_bits / 8).map_err(|_| {
14933        SidecarError::InvalidState(String::from("crypto.subtle AES-GCM tagLength too large"))
14934    })
14935}
14936
14937fn service_javascript_crypto_cipheriv_inner(
14938    request: &JavascriptSyncRpcRequest,
14939    decrypt: bool,
14940) -> Result<Value, SidecarError> {
14941    let label = if decrypt {
14942        "crypto.decipheriv"
14943    } else {
14944        "crypto.cipheriv"
14945    };
14946    let algorithm = javascript_sync_rpc_arg_str(&request.args, 0, &format!("{label} algorithm"))?;
14947    let key = javascript_sync_rpc_base64_arg(&request.args, 1, &format!("{label} key"))?;
14948    let iv = javascript_sync_rpc_base64_arg_optional(&request.args, 2, &format!("{label} iv"))?;
14949    let data = javascript_sync_rpc_base64_arg(&request.args, 3, &format!("{label} data"))?;
14950    let options =
14951        javascript_sync_rpc_json_arg_optional(&request.args, 4, &format!("{label} options"))?;
14952    let auth_tag_len = javascript_crypto_requested_aead_tag_len(algorithm, options.as_ref())?;
14953    let mut context = javascript_crypto_build_cipher_context(
14954        algorithm,
14955        &key,
14956        iv.as_deref(),
14957        decrypt,
14958        options.as_ref(),
14959    )?;
14960    let payload = javascript_crypto_cipher_update(&mut context, &data)?;
14961    let final_bytes = javascript_crypto_cipher_finalize(&mut context)?;
14962    if decrypt {
14963        let mut output = payload;
14964        output.extend(final_bytes);
14965        return Ok(Value::String(
14966            base64::engine::general_purpose::STANDARD.encode(output),
14967        ));
14968    }
14969
14970    let mut response = Map::new();
14971    let mut encrypted = payload;
14972    encrypted.extend(final_bytes);
14973    response.insert(
14974        String::from("data"),
14975        Value::String(base64::engine::general_purpose::STANDARD.encode(encrypted)),
14976    );
14977    if javascript_crypto_is_aead(algorithm) {
14978        let mut auth_tag = vec![0_u8; auth_tag_len];
14979        context
14980            .get_tag(&mut auth_tag)
14981            .map_err(javascript_crypto_openssl_error)?;
14982        response.insert(
14983            String::from("authTag"),
14984            Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
14985        );
14986    }
14987    Ok(Value::String(serde_json::to_string(&response).map_err(
14988        |error| SidecarError::InvalidState(format!("serialize {label} response: {error}")),
14989    )?))
14990}
14991
14992fn javascript_sync_rpc_base64_arg_optional(
14993    args: &[Value],
14994    index: usize,
14995    label: &str,
14996) -> Result<Option<Vec<u8>>, SidecarError> {
14997    if args.get(index).is_none() || args[index].is_null() {
14998        return Ok(None);
14999    }
15000    javascript_sync_rpc_base64_arg(args, index, label).map(Some)
15001}
15002
15003fn javascript_sync_rpc_json_arg_optional(
15004    args: &[Value],
15005    index: usize,
15006    label: &str,
15007) -> Result<Option<Value>, SidecarError> {
15008    if args.get(index).is_none() || args[index].is_null() {
15009        return Ok(None);
15010    }
15011    let raw = javascript_sync_rpc_arg_str(args, index, label)?;
15012    serde_json::from_str(raw)
15013        .map(Some)
15014        .map_err(|error| SidecarError::InvalidState(format!("{label} must be valid JSON: {error}")))
15015}
15016
15017fn javascript_crypto_parse_direct_key_input(
15018    raw: &str,
15019    expected: Option<&str>,
15020    label: &str,
15021) -> Result<JavascriptDirectKeyInput, SidecarError> {
15022    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
15023        SidecarError::InvalidState(format!("{label} must be valid JSON: {error}"))
15024    })?;
15025    let padding = match parsed.as_object().and_then(|value| value.get("padding")) {
15026        Some(value) => javascript_crypto_padding_from_value(value)?,
15027        None => None,
15028    };
15029    Ok(JavascriptDirectKeyInput {
15030        key: javascript_crypto_parse_key_material_value(&parsed, expected, label)?,
15031        padding,
15032    })
15033}
15034
15035fn javascript_crypto_parse_key_material_value(
15036    value: &Value,
15037    expected: Option<&str>,
15038    label: &str,
15039) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15040    if let Some(object) = value.as_object() {
15041        if object.get("__type").and_then(Value::as_str) == Some("keyObject") {
15042            let serialized = object.get("value").ok_or_else(|| {
15043                SidecarError::InvalidState(format!("{label} keyObject is missing a value"))
15044            })?;
15045            return javascript_crypto_parse_serialized_key_object(serialized, expected, label);
15046        }
15047        if object.contains_key("type") && (object.contains_key("pem") || object.contains_key("raw"))
15048        {
15049            return javascript_crypto_parse_serialized_key_object(value, expected, label);
15050        }
15051        if let Some(source) = object.get("key") {
15052            return javascript_crypto_parse_key_source(
15053                source,
15054                object.get("format").and_then(Value::as_str),
15055                object.get("type").and_then(Value::as_str),
15056                expected,
15057                label,
15058            );
15059        }
15060    }
15061    javascript_crypto_parse_key_source(value, None, None, expected, label)
15062}
15063
15064fn javascript_crypto_parse_key_source(
15065    source: &Value,
15066    format: Option<&str>,
15067    kind: Option<&str>,
15068    expected: Option<&str>,
15069    label: &str,
15070) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15071    match source {
15072        Value::String(pem) => javascript_crypto_parse_key_from_pem(pem.as_bytes(), expected, label),
15073        Value::Object(object) if object.get("__type").and_then(Value::as_str) == Some("buffer") => {
15074            let data = javascript_crypto_decode_bridge_buffer(source, label)?;
15075            javascript_crypto_parse_key_from_bytes(&data, format, kind, expected, label)
15076        }
15077        Value::Object(_) => {
15078            if format == Some("jwk") {
15079                return Err(SidecarError::InvalidState(format!(
15080                    "{label} jwk inputs are not supported yet"
15081                )));
15082            }
15083            Err(SidecarError::InvalidState(format!(
15084                "{label} has an unsupported key shape"
15085            )))
15086        }
15087        _ => Err(SidecarError::InvalidState(format!(
15088            "{label} has an unsupported key value"
15089        ))),
15090    }
15091}
15092
15093fn javascript_crypto_parse_key_from_pem(
15094    pem: &[u8],
15095    expected: Option<&str>,
15096    label: &str,
15097) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15098    match expected {
15099        Some("private") => PKey::private_key_from_pem(pem)
15100            .map(JavascriptCryptoKeyMaterial::Private)
15101            .map_err(|error| {
15102                SidecarError::InvalidState(format!("{label} private key is invalid: {error}"))
15103            }),
15104        Some("public") => PKey::public_key_from_pem(pem)
15105            .map(JavascriptCryptoKeyMaterial::Public)
15106            .map_err(|error| {
15107                SidecarError::InvalidState(format!("{label} public key is invalid: {error}"))
15108            }),
15109        _ => PKey::private_key_from_pem(pem)
15110            .map(JavascriptCryptoKeyMaterial::Private)
15111            .or_else(|_| PKey::public_key_from_pem(pem).map(JavascriptCryptoKeyMaterial::Public))
15112            .map_err(|error| {
15113                SidecarError::InvalidState(format!("{label} PEM key is invalid: {error}"))
15114            }),
15115    }
15116}
15117
15118fn javascript_crypto_parse_key_from_bytes(
15119    der: &[u8],
15120    format: Option<&str>,
15121    kind: Option<&str>,
15122    expected: Option<&str>,
15123    label: &str,
15124) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15125    match (format.unwrap_or("der"), kind.or(expected)) {
15126        ("der", Some("pkcs8")) | ("der", Some("private")) => PKey::private_key_from_der(der)
15127            .map(JavascriptCryptoKeyMaterial::Private)
15128            .map_err(|error| {
15129                SidecarError::InvalidState(format!("{label} private key DER is invalid: {error}"))
15130            }),
15131        ("der", Some("spki")) | ("der", Some("public")) => PKey::public_key_from_der(der)
15132            .map(JavascriptCryptoKeyMaterial::Public)
15133            .map_err(|error| {
15134                SidecarError::InvalidState(format!("{label} public key DER is invalid: {error}"))
15135            }),
15136        _ => Err(SidecarError::InvalidState(format!(
15137            "{label} unsupported key bytes format"
15138        ))),
15139    }
15140}
15141
15142fn javascript_crypto_parse_serialized_key_object(
15143    value: &Value,
15144    expected: Option<&str>,
15145    label: &str,
15146) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15147    let serialized: JavascriptSerializedSandboxKeyObject = serde_json::from_value(value.clone())
15148        .map_err(|error| {
15149            SidecarError::InvalidState(format!("{label} keyObject is invalid: {error}"))
15150        })?;
15151    match serialized.kind.as_str() {
15152        "secret" => {
15153            if expected == Some("public") || expected == Some("private") {
15154                return Err(SidecarError::InvalidState(format!(
15155                    "{label} expected an asymmetric key"
15156                )));
15157            }
15158            Ok(JavascriptCryptoKeyMaterial::Secret(
15159                base64::engine::general_purpose::STANDARD
15160                    .decode(serialized.raw.unwrap_or_default())
15161                    .map_err(|error| {
15162                        SidecarError::InvalidState(format!(
15163                            "{label} secret key contains invalid base64: {error}"
15164                        ))
15165                    })?,
15166            ))
15167        }
15168        "private" => {
15169            let pem = serialized.pem.ok_or_else(|| {
15170                SidecarError::InvalidState(format!("{label} private keyObject is missing pem"))
15171            })?;
15172            javascript_crypto_parse_key_from_pem(pem.as_bytes(), Some("private"), label)
15173        }
15174        "public" => {
15175            let pem = serialized.pem.ok_or_else(|| {
15176                SidecarError::InvalidState(format!("{label} public keyObject is missing pem"))
15177            })?;
15178            javascript_crypto_parse_key_from_pem(pem.as_bytes(), Some("public"), label)
15179        }
15180        other => Err(SidecarError::InvalidState(format!(
15181            "{label} has unsupported keyObject type {other}"
15182        ))),
15183    }
15184}
15185
15186fn javascript_crypto_expect_private_key(
15187    key: JavascriptCryptoKeyMaterial,
15188    label: &str,
15189) -> Result<PKey<Private>, SidecarError> {
15190    match key {
15191        JavascriptCryptoKeyMaterial::Private(key) => Ok(key),
15192        _ => Err(SidecarError::InvalidState(format!(
15193            "{label} requires a private key"
15194        ))),
15195    }
15196}
15197
15198fn javascript_crypto_expect_public_key(
15199    key: JavascriptCryptoKeyMaterial,
15200    label: &str,
15201) -> Result<PKey<Public>, SidecarError> {
15202    match key {
15203        JavascriptCryptoKeyMaterial::Public(key) => Ok(key),
15204        JavascriptCryptoKeyMaterial::Private(key) => {
15205            let pem = key
15206                .public_key_to_pem()
15207                .map_err(javascript_crypto_openssl_error)?;
15208            PKey::public_key_from_pem(&pem).map_err(javascript_crypto_openssl_error)
15209        }
15210        _ => Err(SidecarError::InvalidState(format!(
15211            "{label} requires a public key"
15212        ))),
15213    }
15214}
15215
15216fn javascript_crypto_new_signer<'a>(
15217    algorithm: Option<&'a str>,
15218    key: &'a PKey<Private>,
15219) -> Result<Signer<'a>, SidecarError> {
15220    if matches!(key.id(), PKeyId::ED25519 | PKeyId::ED448) || algorithm.is_none() {
15221        return Signer::new_without_digest(key).map_err(javascript_crypto_openssl_error);
15222    }
15223    Signer::new(
15224        javascript_crypto_message_digest_from_name(algorithm.ok_or_else(|| {
15225            SidecarError::InvalidState(String::from("crypto.sign requires a digest algorithm"))
15226        })?)?,
15227        key,
15228    )
15229    .map_err(javascript_crypto_openssl_error)
15230}
15231
15232fn javascript_crypto_new_verifier<'a>(
15233    algorithm: Option<&'a str>,
15234    key: &'a PKey<Public>,
15235) -> Result<Verifier<'a>, SidecarError> {
15236    if matches!(key.id(), PKeyId::ED25519 | PKeyId::ED448) || algorithm.is_none() {
15237        return Verifier::new_without_digest(key).map_err(javascript_crypto_openssl_error);
15238    }
15239    Verifier::new(
15240        javascript_crypto_message_digest_from_name(algorithm.ok_or_else(|| {
15241            SidecarError::InvalidState(String::from("crypto.verify requires a digest algorithm"))
15242        })?)?,
15243        key,
15244    )
15245    .map_err(javascript_crypto_openssl_error)
15246}
15247
15248fn javascript_crypto_message_digest_from_name(name: &str) -> Result<MessageDigest, SidecarError> {
15249    match name.trim().to_ascii_lowercase().replace('-', "").as_str() {
15250        "md5" => Ok(MessageDigest::md5()),
15251        "sha1" => Ok(MessageDigest::sha1()),
15252        "sha256" => Ok(MessageDigest::sha256()),
15253        "sha384" => Ok(MessageDigest::sha384()),
15254        "sha512" => Ok(MessageDigest::sha512()),
15255        other => Err(SidecarError::InvalidState(format!(
15256            "unsupported crypto digest algorithm {other}"
15257        ))),
15258    }
15259}
15260
15261fn javascript_crypto_padding_from_value(value: &Value) -> Result<Option<Padding>, SidecarError> {
15262    let Some(number) = value.as_i64() else {
15263        return Ok(None);
15264    };
15265    let padding = match number {
15266        1 => Padding::PKCS1,
15267        3 => Padding::NONE,
15268        4 => Padding::PKCS1_OAEP,
15269        6 => Padding::PKCS1_PSS,
15270        other => {
15271            return Err(SidecarError::InvalidState(format!(
15272                "unsupported RSA padding constant {other}"
15273            )));
15274        }
15275    };
15276    Ok(Some(padding))
15277}
15278
15279fn javascript_crypto_decode_bridge_buffer(
15280    value: &Value,
15281    label: &str,
15282) -> Result<Vec<u8>, SidecarError> {
15283    let base64_value = value
15284        .as_object()
15285        .filter(|object| object.get("__type").and_then(Value::as_str) == Some("buffer"))
15286        .and_then(|object| object.get("value"))
15287        .and_then(Value::as_str)
15288        .ok_or_else(|| {
15289            SidecarError::InvalidState(format!("{label} must be a serialized bridge buffer"))
15290        })?;
15291    base64::engine::general_purpose::STANDARD
15292        .decode(base64_value)
15293        .map_err(|error| {
15294            SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
15295        })
15296}
15297
15298fn javascript_crypto_serialize_sandbox_key_object(
15299    key: &JavascriptCryptoKeyMaterial,
15300) -> Result<Value, SidecarError> {
15301    let serialized = match key {
15302        JavascriptCryptoKeyMaterial::Private(key) => JavascriptSerializedSandboxKeyObject {
15303            kind: String::from("private"),
15304            pem: Some(
15305                String::from_utf8(
15306                    key.private_key_to_pem_pkcs8()
15307                        .map_err(javascript_crypto_openssl_error)?,
15308                )
15309                .map_err(|error| {
15310                    SidecarError::InvalidState(format!("private key PEM is not utf8: {error}"))
15311                })?,
15312            ),
15313            raw: None,
15314            asymmetric_key_type: javascript_crypto_pkey_type_name(key.id()),
15315            asymmetric_key_details: None,
15316            jwk: None,
15317        },
15318        JavascriptCryptoKeyMaterial::Public(key) => JavascriptSerializedSandboxKeyObject {
15319            kind: String::from("public"),
15320            pem: Some(
15321                String::from_utf8(
15322                    key.public_key_to_pem()
15323                        .map_err(javascript_crypto_openssl_error)?,
15324                )
15325                .map_err(|error| {
15326                    SidecarError::InvalidState(format!("public key PEM is not utf8: {error}"))
15327                })?,
15328            ),
15329            raw: None,
15330            asymmetric_key_type: javascript_crypto_pkey_type_name(key.id()),
15331            asymmetric_key_details: None,
15332            jwk: None,
15333        },
15334        JavascriptCryptoKeyMaterial::Secret(raw) => JavascriptSerializedSandboxKeyObject {
15335            kind: String::from("secret"),
15336            pem: None,
15337            raw: Some(base64::engine::general_purpose::STANDARD.encode(raw)),
15338            asymmetric_key_type: None,
15339            asymmetric_key_details: None,
15340            jwk: None,
15341        },
15342    };
15343    serde_json::to_value(serialized)
15344        .map_err(|error| SidecarError::InvalidState(format!("serialize key object: {error}")))
15345}
15346
15347fn javascript_crypto_pkey_type_name(id: PKeyId) -> Option<String> {
15348    match id {
15349        PKeyId::RSA => Some(String::from("rsa")),
15350        PKeyId::EC => Some(String::from("ec")),
15351        PKeyId::ED25519 => Some(String::from("ed25519")),
15352        PKeyId::ED448 => Some(String::from("ed448")),
15353        PKeyId::X25519 => Some(String::from("x25519")),
15354        PKeyId::X448 => Some(String::from("x448")),
15355        PKeyId::DH => Some(String::from("dh")),
15356        _ => None,
15357    }
15358}
15359
15360fn javascript_crypto_rsa_output_size(
15361    key: &JavascriptCryptoKeyMaterial,
15362) -> Result<usize, SidecarError> {
15363    match key {
15364        JavascriptCryptoKeyMaterial::Private(key) => key
15365            .rsa()
15366            .map(|rsa| rsa.size() as usize)
15367            .map_err(javascript_crypto_openssl_error),
15368        JavascriptCryptoKeyMaterial::Public(key) => key
15369            .rsa()
15370            .map(|rsa| rsa.size() as usize)
15371            .map_err(javascript_crypto_openssl_error),
15372        JavascriptCryptoKeyMaterial::Secret(_) => Err(SidecarError::InvalidState(String::from(
15373            "RSA operations require an asymmetric key",
15374        ))),
15375    }
15376}
15377
15378fn javascript_crypto_parse_serialized_options_arg(
15379    args: &[Value],
15380    index: usize,
15381    label: &str,
15382) -> Result<Option<Value>, SidecarError> {
15383    let Some(raw) = args.get(index).and_then(Value::as_str) else {
15384        return Ok(None);
15385    };
15386    let parsed: Value = serde_json::from_str(raw).map_err(|error| {
15387        SidecarError::InvalidState(format!("{label} must be valid JSON: {error}"))
15388    })?;
15389    if parsed.get("hasOptions").and_then(Value::as_bool) == Some(true) {
15390        Ok(parsed.get("options").cloned())
15391    } else {
15392        Ok(None)
15393    }
15394}
15395
15396fn javascript_crypto_u32_from_bridge_value(
15397    value: &Value,
15398    label: &str,
15399) -> Result<u32, SidecarError> {
15400    if let Some(number) = value.as_u64() {
15401        return u32::try_from(number)
15402            .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")));
15403    }
15404    let bytes = javascript_crypto_decode_bridge_buffer(value, label)?;
15405    if bytes.len() > 4 {
15406        return Err(SidecarError::InvalidState(format!(
15407            "{label} buffer is too large for u32"
15408        )));
15409    }
15410    Ok(bytes
15411        .into_iter()
15412        .fold(0_u32, |acc, byte| (acc << 8) | u32::from(byte)))
15413}
15414
15415fn javascript_crypto_bignum_from_bridge_value(
15416    value: &Value,
15417    label: &str,
15418) -> Result<BigNum, SidecarError> {
15419    if let Some(object) = value.as_object() {
15420        if object.get("__type").and_then(Value::as_str) == Some("bigint") {
15421            let decimal = object.get("value").and_then(Value::as_str).ok_or_else(|| {
15422                SidecarError::InvalidState(format!("{label} bigint is missing a value"))
15423            })?;
15424            return BigNum::from_dec_str(decimal).map_err(javascript_crypto_openssl_error);
15425        }
15426    }
15427    let bytes = javascript_crypto_decode_bridge_buffer(value, label)?;
15428    BigNum::from_slice(&bytes).map_err(javascript_crypto_openssl_error)
15429}
15430
15431fn javascript_crypto_curve_nid(name: &str) -> Result<Nid, SidecarError> {
15432    match name {
15433        "prime256v1" | "P-256" => Ok(Nid::X9_62_PRIME256V1),
15434        "secp384r1" | "P-384" => Ok(Nid::SECP384R1),
15435        "secp521r1" | "P-521" => Ok(Nid::SECP521R1),
15436        "secp256k1" => Ok(Nid::SECP256K1),
15437        other => Err(SidecarError::InvalidState(format!(
15438            "unsupported EC curve {other}"
15439        ))),
15440    }
15441}
15442
15443fn javascript_crypto_named_dh_group(name: &str) -> Result<Dh<Params>, SidecarError> {
15444    match name {
15445        "modp2" => Dh::get_1024_160().map_err(javascript_crypto_openssl_error),
15446        "modp14" | "modp15" | "modp16" | "modp17" | "modp18" => {
15447            Dh::get_2048_256().map_err(javascript_crypto_openssl_error)
15448        }
15449        other => Err(SidecarError::InvalidState(format!(
15450            "unsupported Diffie-Hellman group {other}"
15451        ))),
15452    }
15453}
15454
15455fn javascript_crypto_clone_dh_params(params: &Dh<Params>) -> Result<Dh<Params>, SidecarError> {
15456    Dh::from_pqg(
15457        params
15458            .prime_p()
15459            .to_owned()
15460            .map_err(javascript_crypto_openssl_error)?,
15461        params
15462            .prime_q()
15463            .map(|value| value.to_owned().map_err(javascript_crypto_openssl_error))
15464            .transpose()?,
15465        params
15466            .generator()
15467            .to_owned()
15468            .map_err(javascript_crypto_openssl_error)?,
15469    )
15470    .map_err(javascript_crypto_openssl_error)
15471}
15472
15473fn javascript_crypto_build_dh_params(args: &[Value]) -> Result<Dh<Params>, SidecarError> {
15474    let Some(first) = args.first() else {
15475        return Err(SidecarError::InvalidState(String::from(
15476            "Diffie-Hellman session args are required",
15477        )));
15478    };
15479    if let Some(bits) = first.as_u64() {
15480        let generator = args
15481            .get(1)
15482            .map(|value| javascript_crypto_u32_from_bridge_value(value, "Diffie-Hellman generator"))
15483            .transpose()?
15484            .unwrap_or(2);
15485        return Dh::generate_params(bits as u32, generator)
15486            .map_err(javascript_crypto_openssl_error);
15487    }
15488    let prime = javascript_crypto_bignum_from_bridge_value(first, "Diffie-Hellman prime")?;
15489    let generator = args
15490        .get(1)
15491        .map(|value| javascript_crypto_bignum_from_bridge_value(value, "Diffie-Hellman generator"))
15492        .transpose()?
15493        .unwrap_or(BigNum::from_u32(2).map_err(javascript_crypto_openssl_error)?);
15494    Dh::from_pqg(prime, None, generator).map_err(javascript_crypto_openssl_error)
15495}
15496
15497fn javascript_crypto_call_dh_session(
15498    session: &mut ActiveDhSession,
15499    method: &str,
15500    args: &[Value],
15501) -> Result<(Value, bool), SidecarError> {
15502    match method {
15503        "verifyError" => Ok((Value::Null, false)),
15504        "generateKeys" => {
15505            if session.key_pair.is_none() {
15506                session.key_pair = Some(
15507                    javascript_crypto_clone_dh_params(&session.params)?
15508                        .generate_key()
15509                        .map_err(javascript_crypto_openssl_error)?,
15510                );
15511            }
15512            let public = session
15513                .key_pair
15514                .as_ref()
15515                .expect("dh key pair")
15516                .public_key()
15517                .to_vec();
15518            Ok((javascript_crypto_bridge_buffer_value(&public), true))
15519        }
15520        "computeSecret" => {
15521            if session.key_pair.is_none() {
15522                session.key_pair = Some(
15523                    javascript_crypto_clone_dh_params(&session.params)?
15524                        .generate_key()
15525                        .map_err(javascript_crypto_openssl_error)?,
15526                );
15527            }
15528            let peer = javascript_crypto_bignum_from_bridge_value(
15529                args.first().ok_or_else(|| {
15530                    SidecarError::InvalidState(String::from(
15531                        "computeSecret requires peer public key",
15532                    ))
15533                })?,
15534                "Diffie-Hellman peer public key",
15535            )?;
15536            let secret = session
15537                .key_pair
15538                .as_ref()
15539                .expect("dh key pair")
15540                .compute_key(&peer)
15541                .map_err(javascript_crypto_openssl_error)?;
15542            Ok((javascript_crypto_bridge_buffer_value(&secret), true))
15543        }
15544        "getPrime" => Ok((
15545            javascript_crypto_bridge_buffer_value(&session.params.prime_p().to_vec()),
15546            true,
15547        )),
15548        "getGenerator" => Ok((
15549            javascript_crypto_bridge_buffer_value(&session.params.generator().to_vec()),
15550            true,
15551        )),
15552        "getPublicKey" => {
15553            if session.key_pair.is_none() {
15554                session.key_pair = Some(
15555                    javascript_crypto_clone_dh_params(&session.params)?
15556                        .generate_key()
15557                        .map_err(javascript_crypto_openssl_error)?,
15558                );
15559            }
15560            Ok((
15561                javascript_crypto_bridge_buffer_value(
15562                    &session
15563                        .key_pair
15564                        .as_ref()
15565                        .expect("dh key pair")
15566                        .public_key()
15567                        .to_vec(),
15568                ),
15569                true,
15570            ))
15571        }
15572        "getPrivateKey" => {
15573            if session.key_pair.is_none() {
15574                session.key_pair = Some(
15575                    javascript_crypto_clone_dh_params(&session.params)?
15576                        .generate_key()
15577                        .map_err(javascript_crypto_openssl_error)?,
15578                );
15579            }
15580            Ok((
15581                javascript_crypto_bridge_buffer_value(
15582                    &session
15583                        .key_pair
15584                        .as_ref()
15585                        .expect("dh key pair")
15586                        .private_key()
15587                        .to_vec(),
15588                ),
15589                true,
15590            ))
15591        }
15592        other => Err(SidecarError::InvalidState(format!(
15593            "Unsupported Diffie-Hellman method: {other}"
15594        ))),
15595    }
15596}
15597
15598fn javascript_crypto_call_ecdh_session(
15599    session: &mut ActiveEcdhSession,
15600    method: &str,
15601    args: &[Value],
15602) -> Result<(Value, bool), SidecarError> {
15603    let nid = javascript_crypto_curve_nid(&session.curve)?;
15604    let group = EcGroup::from_curve_name(nid).map_err(javascript_crypto_openssl_error)?;
15605    match method {
15606        "verifyError" => Ok((Value::Null, false)),
15607        "generateKeys" => {
15608            if session.key_pair.is_none() {
15609                session.key_pair =
15610                    Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15611            }
15612            let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
15613            let bytes = session
15614                .key_pair
15615                .as_ref()
15616                .expect("ecdh key pair")
15617                .public_key()
15618                .to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx)
15619                .map_err(javascript_crypto_openssl_error)?;
15620            Ok((javascript_crypto_bridge_buffer_value(&bytes), true))
15621        }
15622        "computeSecret" => {
15623            if session.key_pair.is_none() {
15624                session.key_pair =
15625                    Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15626            }
15627            let peer_bytes = javascript_crypto_decode_bridge_buffer(
15628                args.first().ok_or_else(|| {
15629                    SidecarError::InvalidState(String::from(
15630                        "computeSecret requires peer public key",
15631                    ))
15632                })?,
15633                "ECDH peer public key",
15634            )?;
15635            let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
15636            let peer_point = EcPoint::from_bytes(&group, &peer_bytes, &mut ctx)
15637                .map_err(javascript_crypto_openssl_error)?;
15638            let peer_key = EcKey::from_public_key(&group, &peer_point)
15639                .map_err(javascript_crypto_openssl_error)?;
15640            let private =
15641                PKey::from_ec_key(session.key_pair.as_ref().expect("ecdh key pair").to_owned())
15642                    .map_err(javascript_crypto_openssl_error)?;
15643            let peer = PKey::from_ec_key(peer_key).map_err(javascript_crypto_openssl_error)?;
15644            let mut deriver = Deriver::new(&private).map_err(javascript_crypto_openssl_error)?;
15645            deriver
15646                .set_peer(&peer)
15647                .map_err(javascript_crypto_openssl_error)?;
15648            let secret = deriver
15649                .derive_to_vec()
15650                .map_err(javascript_crypto_openssl_error)?;
15651            Ok((javascript_crypto_bridge_buffer_value(&secret), true))
15652        }
15653        "getPublicKey" => {
15654            if session.key_pair.is_none() {
15655                session.key_pair =
15656                    Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15657            }
15658            let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
15659            let bytes = session
15660                .key_pair
15661                .as_ref()
15662                .expect("ecdh key pair")
15663                .public_key()
15664                .to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx)
15665                .map_err(javascript_crypto_openssl_error)?;
15666            Ok((javascript_crypto_bridge_buffer_value(&bytes), true))
15667        }
15668        "getPrivateKey" => {
15669            if session.key_pair.is_none() {
15670                session.key_pair =
15671                    Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15672            }
15673            Ok((
15674                javascript_crypto_bridge_buffer_value(
15675                    &session
15676                        .key_pair
15677                        .as_ref()
15678                        .expect("ecdh key pair")
15679                        .private_key()
15680                        .to_vec(),
15681                ),
15682                true,
15683            ))
15684        }
15685        other => Err(SidecarError::InvalidState(format!(
15686            "Unsupported Diffie-Hellman method: {other}"
15687        ))),
15688    }
15689}
15690
15691fn javascript_crypto_serialize_encoded_key_value_public(
15692    key: &PKey<Public>,
15693    encoding: Option<&Value>,
15694) -> Result<Value, SidecarError> {
15695    if let Some(encoding) = encoding {
15696        let format = encoding
15697            .get("format")
15698            .and_then(Value::as_str)
15699            .unwrap_or("pem");
15700        return Ok(match format {
15701            "der" => json!({
15702                "kind": "buffer",
15703                "value": base64::engine::general_purpose::STANDARD
15704                    .encode(key.public_key_to_der().map_err(javascript_crypto_openssl_error)?),
15705            }),
15706            _ => json!({
15707                "kind": "string",
15708                "value": String::from_utf8(
15709                    key.public_key_to_pem().map_err(javascript_crypto_openssl_error)?,
15710                )
15711                .map_err(|error| SidecarError::InvalidState(format!("public key PEM utf8: {error}")))?,
15712            }),
15713        });
15714    }
15715    javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Public(
15716        key.to_owned(),
15717    ))
15718}
15719
15720fn javascript_crypto_serialize_encoded_key_value_private(
15721    key: &PKey<Private>,
15722    encoding: Option<&Value>,
15723) -> Result<Value, SidecarError> {
15724    if let Some(encoding) = encoding {
15725        let format = encoding
15726            .get("format")
15727            .and_then(Value::as_str)
15728            .unwrap_or("pem");
15729        return Ok(match format {
15730            "der" => json!({
15731                "kind": "buffer",
15732                "value": base64::engine::general_purpose::STANDARD
15733                    .encode(key.private_key_to_der().map_err(javascript_crypto_openssl_error)?),
15734            }),
15735            _ => json!({
15736                "kind": "string",
15737                "value": String::from_utf8(
15738                    key.private_key_to_pem_pkcs8().map_err(javascript_crypto_openssl_error)?,
15739                )
15740                .map_err(|error| SidecarError::InvalidState(format!("private key PEM utf8: {error}")))?,
15741            }),
15742        });
15743    }
15744    javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Private(
15745        key.to_owned(),
15746    ))
15747}
15748
15749fn javascript_crypto_bridge_buffer_value(bytes: &[u8]) -> Value {
15750    json!({
15751        "__type": "buffer",
15752        "value": base64::engine::general_purpose::STANDARD.encode(bytes),
15753    })
15754}
15755
15756fn javascript_crypto_build_cipher_context(
15757    algorithm: &str,
15758    key: &[u8],
15759    iv: Option<&[u8]>,
15760    decrypt: bool,
15761    options: Option<&Value>,
15762) -> Result<Crypter, SidecarError> {
15763    let cipher = javascript_crypto_cipher_from_name(algorithm)?;
15764    let mode = if decrypt {
15765        Mode::Decrypt
15766    } else {
15767        Mode::Encrypt
15768    };
15769    let mut context =
15770        Crypter::new(cipher, mode, key, iv).map_err(javascript_crypto_openssl_error)?;
15771    if let Some(auto_padding) = options
15772        .and_then(|value| value.get("autoPadding"))
15773        .and_then(Value::as_bool)
15774    {
15775        context.pad(auto_padding);
15776    }
15777    if javascript_crypto_is_aead(algorithm) {
15778        if let Some(aad) = options
15779            .and_then(|value| value.get("aad"))
15780            .and_then(Value::as_str)
15781        {
15782            context
15783                .aad_update(
15784                    &base64::engine::general_purpose::STANDARD
15785                        .decode(aad)
15786                        .map_err(|error| {
15787                            SidecarError::InvalidState(format!(
15788                                "cipher aad contains invalid base64: {error}"
15789                            ))
15790                        })?,
15791                )
15792                .map_err(javascript_crypto_openssl_error)?;
15793        }
15794        if decrypt {
15795            if let Some(auth_tag) = options
15796                .and_then(|value| value.get("authTag"))
15797                .and_then(Value::as_str)
15798            {
15799                let decoded = base64::engine::general_purpose::STANDARD
15800                    .decode(auth_tag)
15801                    .map_err(|error| {
15802                        SidecarError::InvalidState(format!(
15803                            "cipher authTag contains invalid base64: {error}"
15804                        ))
15805                    })?;
15806                context
15807                    .set_tag(&decoded)
15808                    .map_err(javascript_crypto_openssl_error)?;
15809            }
15810        }
15811    }
15812    Ok(context)
15813}
15814
15815fn javascript_crypto_requested_aead_tag_len(
15816    algorithm: &str,
15817    options: Option<&Value>,
15818) -> Result<usize, SidecarError> {
15819    if !javascript_crypto_is_aead(algorithm) {
15820        return Ok(0);
15821    }
15822    let requested = options
15823        .and_then(|value| value.get("authTagLength"))
15824        .and_then(Value::as_u64)
15825        .unwrap_or(javascript_crypto_aead_tag_len(algorithm) as u64);
15826    usize::try_from(requested).map_err(|_| {
15827        SidecarError::InvalidState(String::from("cipher authTagLength must fit within usize"))
15828    })
15829}
15830
15831fn javascript_crypto_cipher_update(
15832    context: &mut Crypter,
15833    data: &[u8],
15834) -> Result<Vec<u8>, SidecarError> {
15835    let mut output = vec![0_u8; data.len() + 32];
15836    let written = context
15837        .update(data, &mut output)
15838        .map_err(javascript_crypto_openssl_error)?;
15839    output.truncate(written);
15840    Ok(output)
15841}
15842
15843fn javascript_crypto_cipher_finalize(context: &mut Crypter) -> Result<Vec<u8>, SidecarError> {
15844    let mut output = vec![0_u8; 32];
15845    let written = context
15846        .finalize(&mut output)
15847        .map_err(javascript_crypto_openssl_error)?;
15848    output.truncate(written);
15849    Ok(output)
15850}
15851
15852fn javascript_crypto_cipher_from_name(name: &str) -> Result<Cipher, SidecarError> {
15853    match name.to_ascii_lowercase().as_str() {
15854        "aes-128-cbc" => Ok(Cipher::aes_128_cbc()),
15855        "aes-192-cbc" => Ok(Cipher::aes_192_cbc()),
15856        "aes-256-cbc" => Ok(Cipher::aes_256_cbc()),
15857        "aes-128-ctr" => Ok(Cipher::aes_128_ctr()),
15858        "aes-192-ctr" => Ok(Cipher::aes_192_ctr()),
15859        "aes-256-ctr" => Ok(Cipher::aes_256_ctr()),
15860        "aes-128-gcm" => Ok(Cipher::aes_128_gcm()),
15861        "aes-192-gcm" => Ok(Cipher::aes_192_gcm()),
15862        "aes-256-gcm" => Ok(Cipher::aes_256_gcm()),
15863        other => Err(SidecarError::InvalidState(format!(
15864            "unsupported crypto cipher algorithm {other}"
15865        ))),
15866    }
15867}
15868
15869fn javascript_crypto_is_aead(algorithm: &str) -> bool {
15870    algorithm.to_ascii_lowercase().ends_with("-gcm")
15871}
15872
15873fn javascript_crypto_aead_tag_len(_algorithm: &str) -> usize {
15874    16
15875}
15876
15877fn javascript_crypto_openssl_error(error: openssl::error::ErrorStack) -> SidecarError {
15878    SidecarError::Execution(format!("crypto operation failed: {error}"))
15879}
15880
15881fn service_javascript_kernel_stdin_sync_rpc(
15882    kernel: &mut SidecarKernel,
15883    process: &mut ActiveProcess,
15884    request: &JavascriptSyncRpcRequest,
15885) -> Result<Value, SidecarError> {
15886    let max_bytes =
15887        javascript_sync_rpc_arg_u64_optional(&request.args, 0, "__kernel_stdin_read max bytes")?
15888            .map(|value| value.clamp(1, DEFAULT_KERNEL_STDIN_READ_MAX_BYTES as u64) as usize)
15889            .unwrap_or(DEFAULT_KERNEL_STDIN_READ_MAX_BYTES);
15890    let timeout_ms =
15891        javascript_sync_rpc_arg_u64_optional(&request.args, 1, "__kernel_stdin_read timeout ms")?
15892            .unwrap_or(DEFAULT_KERNEL_STDIN_READ_TIMEOUT_MS);
15893
15894    match kernel
15895        .fd_read_with_timeout_result(
15896            EXECUTION_DRIVER_NAME,
15897            process.kernel_pid,
15898            0,
15899            max_bytes,
15900            Some(Duration::from_millis(timeout_ms)),
15901        )
15902        .map_err(kernel_error)
15903    {
15904        Ok(Some(chunk)) if !chunk.is_empty() => Ok(json!({
15905            "dataBase64": base64::engine::general_purpose::STANDARD.encode(chunk),
15906        })),
15907        Ok(Some(_)) => Ok(Value::Null),
15908        Ok(None) => Ok(json!({
15909            "done": true,
15910        })),
15911        Err(SidecarError::Kernel(error)) if error.starts_with("EAGAIN:") => Ok(Value::Null),
15912        Err(error) => Err(error),
15913    }
15914}
15915
15916fn service_javascript_pty_set_raw_mode_sync_rpc(
15917    kernel: &mut SidecarKernel,
15918    process: &mut ActiveProcess,
15919    request: &JavascriptSyncRpcRequest,
15920) -> Result<Value, SidecarError> {
15921    let enabled = javascript_sync_rpc_arg_bool(&request.args, 0, "__pty_set_raw_mode enabled")?;
15922    kernel
15923        .pty_set_discipline(
15924            EXECUTION_DRIVER_NAME,
15925            process.kernel_pid,
15926            0,
15927            LineDisciplineConfig {
15928                canonical: Some(!enabled),
15929                echo: Some(!enabled),
15930                isig: Some(!enabled),
15931            },
15932        )
15933        .map_err(kernel_error)?;
15934    Ok(Value::Null)
15935}
15936
15937fn service_javascript_kernel_stdio_write_sync_rpc(
15938    kernel: &mut SidecarKernel,
15939    process: &mut ActiveProcess,
15940    request: &JavascriptSyncRpcRequest,
15941) -> Result<Value, SidecarError> {
15942    let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_stdio_write fd")?;
15943    let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "__kernel_stdio_write chunk")?;
15944
15945    let written = match fd {
15946        1 => kernel
15947            .write_process_stdout(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
15948            .map_err(kernel_error)?,
15949        2 => kernel
15950            .write_process_stderr(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
15951            .map_err(kernel_error)?,
15952        other => {
15953            return Err(SidecarError::InvalidState(format!(
15954                "__kernel_stdio_write only supports fd 1/2, got {other}"
15955            )));
15956        }
15957    };
15958
15959    let event = if fd == 1 {
15960        ActiveExecutionEvent::Stdout(chunk)
15961    } else {
15962        ActiveExecutionEvent::Stderr(chunk)
15963    };
15964    process.queue_pending_execution_event(event)?;
15965
15966    Ok(json!(written))
15967}
15968
15969fn service_javascript_kernel_poll_sync_rpc(
15970    kernel: &mut SidecarKernel,
15971    process: &ActiveProcess,
15972    request: &JavascriptSyncRpcRequest,
15973) -> Result<Value, SidecarError> {
15974    let fd_requests: Vec<KernelPollFdRequest> = serde_json::from_value(
15975        request
15976            .args
15977            .first()
15978            .cloned()
15979            .unwrap_or_else(|| Value::Array(Vec::new())),
15980    )
15981    .map_err(|error| {
15982        SidecarError::InvalidState(format!(
15983            "__kernel_poll fd list must be a JSON array of {{ fd, events }} objects: {error}"
15984        ))
15985    })?;
15986    let timeout_ms =
15987        javascript_sync_rpc_arg_u64_optional(&request.args, 1, "__kernel_poll timeout ms")?
15988            .unwrap_or_default();
15989    let timeout_ms = i32::try_from(timeout_ms).map_err(|_| {
15990        SidecarError::InvalidState(String::from("__kernel_poll timeout ms must fit within i32"))
15991    })?;
15992
15993    let poll_fds = fd_requests
15994        .iter()
15995        .map(|entry| PollFd {
15996            fd: entry.fd,
15997            events: PollEvents::from_bits(entry.events),
15998            revents: PollEvents::empty(),
15999        })
16000        .collect::<Vec<_>>();
16001    let result = kernel
16002        .poll_fds(
16003            EXECUTION_DRIVER_NAME,
16004            process.kernel_pid,
16005            poll_fds,
16006            timeout_ms,
16007        )
16008        .map_err(kernel_error)?;
16009
16010    Ok(json!({
16011        "readyCount": result.ready_count,
16012        "fds": result
16013            .fds
16014            .into_iter()
16015            .map(|entry| KernelPollFdResponse {
16016                fd: entry.fd,
16017                events: entry.events.bits(),
16018                revents: entry.revents.bits(),
16019            })
16020            .collect::<Vec<_>>(),
16021    }))
16022}
16023
16024fn install_kernel_stdin_pipe(kernel: &mut SidecarKernel, pid: u32) -> Result<u32, SidecarError> {
16025    let (read_fd, write_fd) = kernel
16026        .open_pipe(EXECUTION_DRIVER_NAME, pid)
16027        .map_err(kernel_error)?;
16028    kernel
16029        .fd_dup2(EXECUTION_DRIVER_NAME, pid, read_fd, 0)
16030        .map_err(kernel_error)?;
16031    kernel
16032        .fd_close(EXECUTION_DRIVER_NAME, pid, read_fd)
16033        .map_err(kernel_error)?;
16034    Ok(write_fd)
16035}
16036
16037fn javascript_child_process_stdin_mode(request: &JavascriptChildProcessSpawnRequest) -> &str {
16038    request
16039        .options
16040        .stdio
16041        .first()
16042        .map(String::as_str)
16043        .unwrap_or("pipe")
16044}
16045
16046pub(crate) fn write_kernel_process_stdin(
16047    kernel: &mut SidecarKernel,
16048    process: &mut ActiveProcess,
16049    chunk: &[u8],
16050) -> Result<(), SidecarError> {
16051    if process.runtime == GuestRuntimeKind::JavaScript {
16052        return Ok(());
16053    }
16054    let Some(writer_fd) = process.kernel_stdin_writer_fd else {
16055        return Ok(());
16056    };
16057    kernel
16058        .fd_write(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd, chunk)
16059        .map(|_| ())
16060        .map_err(kernel_error)
16061}
16062
16063pub(crate) fn close_kernel_process_stdin(
16064    kernel: &mut SidecarKernel,
16065    process: &mut ActiveProcess,
16066) -> Result<(), SidecarError> {
16067    let Some(writer_fd) = process.kernel_stdin_writer_fd.take() else {
16068        return Ok(());
16069    };
16070    kernel
16071        .fd_close(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd)
16072        .map_err(kernel_error)
16073}
16074
16075fn parse_http_header_collection(
16076    headers: &BTreeMap<String, Value>,
16077    label: &str,
16078) -> Result<HttpHeaderCollection, SidecarError> {
16079    let mut normalized = BTreeMap::<String, Vec<String>>::new();
16080    let mut raw_pairs = Vec::new();
16081
16082    for (raw_name, value) in headers {
16083        let normalized_name = raw_name.to_ascii_lowercase();
16084        let values = match value {
16085            Value::String(text) => vec![text.clone()],
16086            Value::Array(values) => values
16087                .iter()
16088                .map(|entry| {
16089                    entry.as_str().map(str::to_owned).ok_or_else(|| {
16090                        SidecarError::InvalidState(format!(
16091                            "{label} header {raw_name} must contain only strings"
16092                        ))
16093                    })
16094                })
16095                .collect::<Result<Vec<_>, _>>()?,
16096            other => {
16097                return Err(SidecarError::InvalidState(format!(
16098                    "{label} header {raw_name} must be a string or string array, received {other}"
16099                )));
16100            }
16101        };
16102        raw_pairs.extend(
16103            values
16104                .iter()
16105                .cloned()
16106                .map(|entry| (raw_name.clone(), entry)),
16107        );
16108        normalized
16109            .entry(normalized_name)
16110            .or_default()
16111            .extend(values);
16112    }
16113
16114    Ok(HttpHeaderCollection {
16115        normalized,
16116        raw_pairs,
16117    })
16118}
16119
16120fn http_headers_json(headers: &HttpHeaderCollection) -> Value {
16121    let map = headers
16122        .normalized
16123        .iter()
16124        .map(|(name, values)| {
16125            let value = if values.len() == 1 {
16126                Value::String(values[0].clone())
16127            } else {
16128                Value::Array(values.iter().cloned().map(Value::String).collect())
16129            };
16130            (name.clone(), value)
16131        })
16132        .collect::<Map<String, Value>>();
16133    Value::Object(map)
16134}
16135
16136fn http_raw_headers_json(headers: &HttpHeaderCollection) -> Value {
16137    Value::Array(
16138        headers
16139            .raw_pairs
16140            .iter()
16141            .flat_map(|(name, value)| [Value::String(name.clone()), Value::String(value.clone())])
16142            .collect(),
16143    )
16144}
16145
16146fn is_loopback_request_host(host: &str) -> bool {
16147    let bare = host
16148        .strip_prefix('[')
16149        .and_then(|value| value.strip_suffix(']'))
16150        .unwrap_or(host);
16151    matches!(bare, "localhost" | "127.0.0.1" | "::1")
16152}
16153
16154fn serialize_http_loopback_request(
16155    url: &Url,
16156    options: &JavascriptHttpRequestOptions,
16157    headers: &HttpHeaderCollection,
16158) -> Result<String, SidecarError> {
16159    let body_base64 = options
16160        .body
16161        .as_ref()
16162        .map(|body| base64::engine::general_purpose::STANDARD.encode(body.as_bytes()));
16163    serde_json::to_string(&json!({
16164        "method": options.method.clone().unwrap_or_else(|| String::from("GET")),
16165        "url": http_request_target(url),
16166        "headers": http_headers_json(headers),
16167        "rawHeaders": http_raw_headers_json(headers),
16168        "bodyBase64": body_base64,
16169    }))
16170    .map_err(|error| SidecarError::Execution(format!("ERR_AGENT_OS_NODE_SYNC_RPC: {error}")))
16171}
16172
16173fn http_request_target(url: &Url) -> String {
16174    let path = if url.path().is_empty() {
16175        "/"
16176    } else {
16177        url.path()
16178    };
16179    format!(
16180        "{path}{}",
16181        url.query()
16182            .map(|query| format!("?{query}"))
16183            .unwrap_or_default()
16184    )
16185}
16186
16187fn outbound_http_response_json(url: &Url, response: ureq::Response) -> Result<Value, SidecarError> {
16188    let status = response.status();
16189    let status_text = response.status_text().to_owned();
16190    let mut header_pairs = Vec::new();
16191    let mut raw_headers = Vec::new();
16192    for raw_name in response.headers_names() {
16193        for value in response.all(&raw_name) {
16194            header_pairs.push(json!([raw_name.to_ascii_lowercase(), value]));
16195            raw_headers.push(Value::String(raw_name.clone()));
16196            raw_headers.push(Value::String(value.to_owned()));
16197        }
16198    }
16199    let mut reader = response.into_reader();
16200    let mut body = Vec::new();
16201    reader.read_to_end(&mut body).map_err(|error| {
16202        SidecarError::Execution(format!("failed to read HTTP response: {error}"))
16203    })?;
16204    serde_json::to_string(&json!({
16205        "status": status,
16206        "statusText": status_text,
16207        "headers": header_pairs,
16208        "rawHeaders": raw_headers,
16209        "body": base64::engine::general_purpose::STANDARD.encode(body),
16210        "bodyEncoding": "base64",
16211        "url": url.as_str(),
16212    }))
16213    .map(Value::String)
16214    .map_err(|error| SidecarError::Execution(format!("ERR_AGENT_OS_NODE_SYNC_RPC: {error}")))
16215}
16216
16217/// Split a ureq resolver `netloc` (`host:port`, with optional `[..]` IPv6
16218/// brackets) into its host and port components. Returns `None` if the port is
16219/// missing or unparseable.
16220fn split_netloc(netloc: &str) -> Option<(&str, u16)> {
16221    let (host, port) = netloc.rsplit_once(':')?;
16222    let port: u16 = port.parse().ok()?;
16223    let host = host
16224        .strip_prefix('[')
16225        .and_then(|rest| rest.strip_suffix(']'))
16226        .unwrap_or(host);
16227    Some((host, port))
16228}
16229
16230fn issue_outbound_http_request(
16231    url: &Url,
16232    options: &JavascriptHttpRequestOptions,
16233    headers: &HttpHeaderCollection,
16234    pinned_addresses: &[IpAddr],
16235) -> Result<Value, SidecarError> {
16236    let method = options.method.as_deref().unwrap_or("GET");
16237    // Pin the underlying resolver to the egress-vetted addresses. ureq performs
16238    // its own DNS resolution for the TCP/TLS connect; without this override an
16239    // https:// request would re-resolve the hostname through the host resolver
16240    // (a rebinding DNS server could then return a private/metadata IP that the
16241    // earlier range check would have rejected). The pinned resolver returns only
16242    // the vetted addresses and refuses any host it was not vetted for, while the
16243    // request URL keeps the original hostname so TLS SNI and the Host header stay
16244    // correct.
16245    let pinned_host = url.host_str().map(str::to_owned);
16246    let pinned: Vec<IpAddr> = pinned_addresses.to_vec();
16247    let resolver = move |netloc: &str| -> std::io::Result<Vec<SocketAddr>> {
16248        let (host, port) = split_netloc(netloc).ok_or_else(|| {
16249            std::io::Error::new(
16250                std::io::ErrorKind::InvalidInput,
16251                format!("invalid network location: {netloc}"),
16252            )
16253        })?;
16254        let expected_host = pinned_host.as_deref();
16255        if expected_host != Some(host) {
16256            return Err(std::io::Error::new(
16257                std::io::ErrorKind::PermissionDenied,
16258                format!(
16259                    "EACCES: outbound HTTP resolver pinned to {expected_host:?}, refusing {host}"
16260                ),
16261            ));
16262        }
16263        if pinned.is_empty() {
16264            return Err(std::io::Error::new(
16265                std::io::ErrorKind::PermissionDenied,
16266                "EACCES: no egress-vetted address available for outbound HTTP request",
16267            ));
16268        }
16269        Ok(pinned.iter().map(|ip| SocketAddr::new(*ip, port)).collect())
16270    };
16271    let mut agent_builder = ureq::AgentBuilder::new()
16272        .resolver(resolver)
16273        .timeout_connect(Duration::from_secs(5))
16274        .timeout_read(Duration::from_secs(15))
16275        .timeout_write(Duration::from_secs(15));
16276    if url.scheme() == "https" {
16277        let tls_options = JavascriptTlsBridgeOptions {
16278            is_server: false,
16279            servername: url.host_str().map(str::to_owned),
16280            alpn_protocols: Some(vec![String::from("http/1.1")]),
16281            reject_unauthorized: options.reject_unauthorized,
16282            ..JavascriptTlsBridgeOptions::default()
16283        };
16284        agent_builder = agent_builder.tls_config(Arc::new(build_client_tls_config(&tls_options)?));
16285    }
16286    let agent = agent_builder.build();
16287    let mut request = agent.request_url(method, url);
16288    for (name, values) in &headers.normalized {
16289        if name == "host" {
16290            continue;
16291        }
16292        let header_value = values.join(", ");
16293        request = request.set(name, &header_value);
16294    }
16295    let response = match options.body.as_deref() {
16296        Some(body) => request.send_string(body),
16297        None => request.call(),
16298    };
16299
16300    match response {
16301        Ok(response) => outbound_http_response_json(url, response),
16302        Err(ureq::Error::Status(_, response)) => outbound_http_response_json(url, response),
16303        Err(ureq::Error::Transport(error)) => Err(SidecarError::Execution(format!(
16304            "ERR_HTTP_REQUEST_FAILED: {error}"
16305        ))),
16306    }
16307}
16308
16309fn wait_for_loopback_http_response<B>(
16310    request: LoopbackHttpResponseWaitRequest<'_, B>,
16311) -> Result<String, SidecarError>
16312where
16313    B: NativeSidecarBridge + Send + 'static,
16314    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16315{
16316    let LoopbackHttpResponseWaitRequest {
16317        bridge,
16318        vm_id,
16319        dns,
16320        socket_paths,
16321        kernel,
16322        process,
16323        resource_limits,
16324        request_key,
16325    } = request;
16326    let deadline = Instant::now() + HTTP_LOOPBACK_REQUEST_TIMEOUT;
16327    loop {
16328        if let Some(response) = process
16329            .pending_http_requests
16330            .get(&request_key)
16331            .and_then(|response| response.clone())
16332        {
16333            process.pending_http_requests.remove(&request_key);
16334            return Ok(response);
16335        }
16336
16337        if Instant::now() >= deadline {
16338            process.pending_http_requests.remove(&request_key);
16339            return Err(SidecarError::Execution(String::from(
16340                "HTTP loopback request timed out waiting for net.http_respond",
16341            )));
16342        }
16343
16344        let Some(event) = process
16345            .execution
16346            .poll_event_blocking(Duration::from_millis(10))
16347            .map_err(|error| SidecarError::Execution(error.to_string()))?
16348        else {
16349            continue;
16350        };
16351
16352        match event {
16353            ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
16354                let network_counts = process.network_resource_counts();
16355                let response = service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
16356                    bridge,
16357                    vm_id,
16358                    dns,
16359                    socket_paths,
16360                    kernel,
16361                    process,
16362                    sync_request: &request,
16363                    resource_limits,
16364                    network_counts,
16365                });
16366                match response {
16367                    Ok(result) => process
16368                        .execution
16369                        .respond_javascript_sync_rpc_success(request.id, result)
16370                        .or_else(ignore_stale_javascript_sync_rpc_response)?,
16371                    Err(error) => process
16372                        .execution
16373                        .respond_javascript_sync_rpc_error(
16374                            request.id,
16375                            javascript_sync_rpc_error_code(&error),
16376                            error.to_string(),
16377                        )
16378                        .or_else(ignore_stale_javascript_sync_rpc_response)?,
16379                }
16380            }
16381            ActiveExecutionEvent::Exited(code) => {
16382                process.pending_http_requests.remove(&request_key);
16383                return Err(SidecarError::Execution(format!(
16384                    "HTTP loopback server exited before responding (exit code {code})"
16385                )));
16386            }
16387            ActiveExecutionEvent::Stdout(_)
16388            | ActiveExecutionEvent::Stderr(_)
16389            | ActiveExecutionEvent::PythonVfsRpcRequest(_)
16390            | ActiveExecutionEvent::SignalState { .. } => {}
16391        }
16392    }
16393}
16394
16395fn ensure_vm_fetch_response_within_limit(
16396    response_json: &str,
16397    operation: &str,
16398) -> Result<(), SidecarError> {
16399    let size = response_json.len();
16400    if size > VM_FETCH_BUFFER_LIMIT_BYTES {
16401        return Err(SidecarError::Execution(format!(
16402            "{operation} payload is {size} bytes, limit is {VM_FETCH_BUFFER_LIMIT_BYTES}"
16403        )));
16404    }
16405    Ok(())
16406}
16407
16408pub(crate) fn ensure_vm_fetch_response_frame_within_limit(
16409    response: &ResponseFrame,
16410    max_frame_bytes: usize,
16411) -> Result<(), SidecarError> {
16412    let max_frame_bytes = max_frame_bytes.min(VM_FETCH_BUFFER_LIMIT_BYTES);
16413    let frame = crate::protocol::to_generated_protocol_frame(
16414        &crate::protocol::ProtocolFrame::Response(response.clone()),
16415    )
16416    .map_err(|error| SidecarError::FrameTooLarge(error.to_string()))?;
16417    let WireProtocolFrame::ResponseFrame(_) = &frame else {
16418        return Err(SidecarError::FrameTooLarge(String::from(
16419            "vm fetch response converted to non-response wire frame",
16420        )));
16421    };
16422    WireFrameCodec::new(max_frame_bytes)
16423        .encode(&frame)
16424        .map(|_| ())
16425        .map_err(|error| SidecarError::FrameTooLarge(error.to_string()))
16426}
16427
16428fn service_javascript_dns_sync_rpc<B>(
16429    bridge: &SharedBridge<B>,
16430    kernel: &SidecarKernel,
16431    vm_id: &str,
16432    dns: &VmDnsConfig,
16433    request: &JavascriptSyncRpcRequest,
16434) -> Result<Value, SidecarError>
16435where
16436    B: NativeSidecarBridge + Send + 'static,
16437    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16438{
16439    match request.method.as_str() {
16440        "dns.lookup" => {
16441            let payload = request
16442                .args
16443                .first()
16444                .cloned()
16445                .ok_or_else(|| {
16446                    SidecarError::InvalidState(String::from(
16447                        "dns.lookup requires a request payload",
16448                    ))
16449                })
16450                .and_then(|value| {
16451                    serde_json::from_value::<JavascriptDnsLookupRequest>(value).map_err(|error| {
16452                        SidecarError::InvalidState(format!("invalid dns.lookup payload: {error}"))
16453                    })
16454                })?;
16455            let addresses = filter_dns_ip_addrs(
16456                resolve_dns_ip_addrs(
16457                    bridge,
16458                    kernel,
16459                    vm_id,
16460                    dns,
16461                    &payload.hostname,
16462                    DnsLookupPolicy::CheckPermissions,
16463                )?,
16464                payload.family,
16465            )?;
16466            let addresses = filter_dns_safe_ip_addrs(addresses, &payload.hostname)?;
16467            Ok(Value::Array(
16468                addresses
16469                    .into_iter()
16470                    .map(|ip| {
16471                        json!({
16472                            "address": ip.to_string(),
16473                            "family": if ip.is_ipv6() { 6 } else { 4 },
16474                        })
16475                    })
16476                    .collect(),
16477            ))
16478        }
16479        "dns.resolve" | "dns.resolve4" | "dns.resolve6" => {
16480            let payload = request
16481                .args
16482                .first()
16483                .cloned()
16484                .ok_or_else(|| {
16485                    SidecarError::InvalidState(String::from(
16486                        "dns.resolve requires a request payload",
16487                    ))
16488                })
16489                .and_then(|value| {
16490                    serde_json::from_value::<JavascriptDnsResolveRequest>(value).map_err(|error| {
16491                        SidecarError::InvalidState(format!("invalid dns.resolve payload: {error}"))
16492                    })
16493                })?;
16494            let requested_type = match request.method.as_str() {
16495                "dns.resolve4" => String::from("A"),
16496                "dns.resolve6" => String::from("AAAA"),
16497                _ => payload
16498                    .rrtype
16499                    .as_deref()
16500                    .unwrap_or("A")
16501                    .to_ascii_uppercase(),
16502            };
16503            let record_type = parse_dns_record_type(&requested_type)?;
16504            let resolution = resolve_dns_records(
16505                bridge,
16506                kernel,
16507                vm_id,
16508                dns,
16509                &payload.hostname,
16510                record_type,
16511                DnsLookupPolicy::CheckPermissions,
16512            )?;
16513            dns_resolution_to_node_value(&resolution, &requested_type)
16514        }
16515        other => Err(SidecarError::InvalidState(format!(
16516            "unsupported JavaScript dns sync RPC method {other}"
16517        ))),
16518    }
16519}
16520
16521fn service_javascript_dgram_sync_rpc<B>(
16522    request: JavascriptDgramSyncRpcServiceRequest<'_, B>,
16523) -> Result<Value, SidecarError>
16524where
16525    B: NativeSidecarBridge + Send + 'static,
16526    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16527{
16528    let JavascriptDgramSyncRpcServiceRequest {
16529        bridge,
16530        kernel,
16531        vm_id,
16532        dns,
16533        socket_paths,
16534        process,
16535        sync_request: request,
16536        resource_limits,
16537        network_counts,
16538    } = request;
16539    match request.method.as_str() {
16540        "dgram.createSocket" => {
16541            check_network_resource_limit(
16542                resource_limits.max_sockets,
16543                network_counts.sockets,
16544                1,
16545                "socket",
16546            )?;
16547            let payload = request
16548                .args
16549                .first()
16550                .cloned()
16551                .ok_or_else(|| {
16552                    SidecarError::InvalidState(String::from(
16553                        "dgram.createSocket requires a request payload",
16554                    ))
16555                })
16556                .and_then(|value| {
16557                    serde_json::from_value::<JavascriptDgramCreateSocketRequest>(value).map_err(
16558                        |error| {
16559                            SidecarError::InvalidState(format!(
16560                                "invalid dgram.createSocket payload: {error}"
16561                            ))
16562                        },
16563                    )
16564                })?;
16565            let family = JavascriptUdpFamily::from_socket_type(&payload.socket_type)?;
16566            let socket_id = process.allocate_udp_socket_id();
16567            process.udp_sockets.insert(
16568                socket_id.clone(),
16569                ActiveUdpSocket::new(kernel, process.kernel_pid, family)?,
16570            );
16571            Ok(json!({
16572                "socketId": socket_id,
16573                "type": family.socket_type(),
16574            }))
16575        }
16576        "dgram.bind" => {
16577            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.bind socket id")?;
16578            let payload = request
16579                .args
16580                .get(1)
16581                .cloned()
16582                .ok_or_else(|| {
16583                    SidecarError::InvalidState(String::from(
16584                        "dgram.bind requires a request payload",
16585                    ))
16586                })
16587                .and_then(|value| {
16588                    serde_json::from_value::<JavascriptDgramBindRequest>(value).map_err(|error| {
16589                        SidecarError::InvalidState(format!("invalid dgram.bind payload: {error}"))
16590                    })
16591                })?;
16592            let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
16593                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
16594            })?;
16595            let local_addr = socket.bind(
16596                kernel,
16597                process.kernel_pid,
16598                payload.address.as_deref(),
16599                payload.port,
16600                socket_paths,
16601            )?;
16602            Ok(json!({
16603                "localAddress": local_addr.ip().to_string(),
16604                "localPort": local_addr.port(),
16605                "family": socket_addr_family(&local_addr),
16606            }))
16607        }
16608        "dgram.send" => {
16609            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.send socket id")?;
16610            let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "dgram.send payload")?;
16611            let payload = request
16612                .args
16613                .get(2)
16614                .cloned()
16615                .ok_or_else(|| {
16616                    SidecarError::InvalidState(String::from(
16617                        "dgram.send requires a request payload",
16618                    ))
16619                })
16620                .and_then(|value| {
16621                    serde_json::from_value::<JavascriptDgramSendRequest>(value).map_err(|error| {
16622                        SidecarError::InvalidState(format!("invalid dgram.send payload: {error}"))
16623                    })
16624                })?;
16625            let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
16626                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
16627            })?;
16628            let (written, local_addr) = socket.send_to(ActiveUdpSendToRequest {
16629                bridge,
16630                kernel,
16631                kernel_pid: process.kernel_pid,
16632                vm_id,
16633                dns,
16634                host: payload.address.as_deref().unwrap_or("localhost"),
16635                port: payload.port,
16636                context: socket_paths,
16637                contents: &chunk,
16638            })?;
16639            Ok(json!({
16640                "bytes": written,
16641                "localAddress": local_addr.ip().to_string(),
16642                "localPort": local_addr.port(),
16643                "family": socket_addr_family(&local_addr),
16644            }))
16645        }
16646        "dgram.poll" => {
16647            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.poll socket id")?;
16648            let wait_ms =
16649                javascript_sync_rpc_arg_u64_optional(&request.args, 1, "dgram.poll wait ms")?
16650                    .unwrap_or_default();
16651            let event = {
16652                let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
16653                    SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
16654                })?;
16655                socket.poll(kernel, process.kernel_pid, Duration::from_millis(wait_ms))?
16656            };
16657
16658            match event {
16659                Some(JavascriptUdpSocketEvent::Message { data, remote_addr }) => {
16660                    let family = JavascriptSocketFamily::from_ip(remote_addr.ip());
16661                    let guest_remote_port = if is_loopback_ip(remote_addr.ip()) {
16662                        socket_paths
16663                            .guest_udp_port_for_host_port(family, remote_addr.port())
16664                            .unwrap_or(remote_addr.port())
16665                    } else {
16666                        remote_addr.port()
16667                    };
16668                    Ok(json!({
16669                    "type": "message",
16670                    "data": javascript_sync_rpc_bytes_value(&data),
16671                    "remoteAddress": remote_addr.ip().to_string(),
16672                    "remotePort": guest_remote_port,
16673                    "remoteFamily": socket_addr_family(&remote_addr),
16674                    }))
16675                }
16676                Some(JavascriptUdpSocketEvent::Error { code, message }) => Ok(json!({
16677                    "type": "error",
16678                    "code": code,
16679                    "message": message,
16680                })),
16681                None => Ok(Value::Null),
16682            }
16683        }
16684        "dgram.close" => {
16685            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.close socket id")?;
16686            let mut socket = process.udp_sockets.remove(socket_id).ok_or_else(|| {
16687                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
16688            })?;
16689            socket.close(kernel, process.kernel_pid);
16690            Ok(Value::Null)
16691        }
16692        "dgram.address" => {
16693            let socket_id =
16694                javascript_sync_rpc_arg_str(&request.args, 0, "dgram.address socket id")?;
16695            let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
16696                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
16697            })?;
16698            let local_addr = socket.local_addr().ok_or_else(|| {
16699                SidecarError::Execution(String::from("EBADF: bad file descriptor"))
16700            })?;
16701            javascript_net_json_string(
16702                json!({
16703                    "address": local_addr.ip().to_string(),
16704                    "port": local_addr.port(),
16705                    "family": socket_addr_family(&local_addr),
16706                }),
16707                "dgram.address",
16708            )
16709        }
16710        "dgram.setBufferSize" => {
16711            let socket_id =
16712                javascript_sync_rpc_arg_str(&request.args, 0, "dgram.setBufferSize socket id")?;
16713            let which =
16714                javascript_sync_rpc_arg_str(&request.args, 1, "dgram.setBufferSize buffer kind")?;
16715            let size = javascript_sync_rpc_arg_u64(&request.args, 2, "dgram.setBufferSize size")?;
16716            let size = usize::try_from(size).map_err(|_| {
16717                SidecarError::InvalidState(String::from(
16718                    "dgram.setBufferSize size must fit within usize",
16719                ))
16720            })?;
16721            let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
16722                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
16723            })?;
16724            socket.set_buffer_size(which, size)?;
16725            Ok(Value::Null)
16726        }
16727        "dgram.getBufferSize" => {
16728            let socket_id =
16729                javascript_sync_rpc_arg_str(&request.args, 0, "dgram.getBufferSize socket id")?;
16730            let which =
16731                javascript_sync_rpc_arg_str(&request.args, 1, "dgram.getBufferSize buffer kind")?;
16732            let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
16733                SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
16734            })?;
16735            let size = socket.get_buffer_size(which)?;
16736            Ok(json!(size))
16737        }
16738        other => Err(SidecarError::InvalidState(format!(
16739            "unsupported JavaScript dgram sync RPC method {other}"
16740        ))),
16741    }
16742}
16743
16744#[derive(Debug)]
16745struct ClientHttp2StreamState {
16746    send_stream: Option<h2::SendStream<Bytes>>,
16747}
16748
16749#[derive(Debug)]
16750struct ServerHttp2StreamState {
16751    send_response: Option<ServerHttp2Responder>,
16752    send_stream: Option<h2::SendStream<Bytes>>,
16753}
16754
16755#[derive(Debug)]
16756enum ServerHttp2Responder {
16757    Regular(server::SendResponse<Bytes>),
16758    Pushed(server::SendPushedResponse<Bytes>),
16759}
16760
16761const HTTP2_DEFAULT_WINDOW_SIZE: u32 = 65_535;
16762const HTTP2_POLL_DELAY: Duration = Duration::from_millis(10);
16763
16764fn http2_runtime_snapshot() -> Http2RuntimeSnapshot {
16765    Http2RuntimeSnapshot {
16766        effective_local_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
16767        local_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
16768        remote_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
16769        next_stream_id: 1,
16770        outbound_queue_size: 1,
16771        deflate_dynamic_table_size: 0,
16772        inflate_dynamic_table_size: 0,
16773    }
16774}
16775
16776fn http2_snapshot_json(snapshot: &Http2SessionSnapshot) -> Result<String, SidecarError> {
16777    serde_json::to_string(snapshot)
16778        .map_err(|error| SidecarError::Execution(format!("ERR_AGENT_OS_NODE_SYNC_RPC: {error}")))
16779}
16780
16781fn http2_event_value(event: &Http2BridgeEvent) -> Result<Value, SidecarError> {
16782    serde_json::to_string(event)
16783        .map(Value::String)
16784        .map_err(|error| SidecarError::Execution(format!("ERR_AGENT_OS_NODE_SYNC_RPC: {error}")))
16785}
16786
16787fn push_http2_server_event(
16788    shared: &Arc<Mutex<crate::state::Http2SharedState>>,
16789    server_id: u64,
16790    event: Http2BridgeEvent,
16791) {
16792    if let Ok(mut state) = shared.lock() {
16793        state
16794            .server_events
16795            .entry(server_id)
16796            .or_default()
16797            .push_back(event);
16798    }
16799}
16800
16801fn push_http2_session_event(
16802    shared: &Arc<Mutex<crate::state::Http2SharedState>>,
16803    session_id: u64,
16804    event: Http2BridgeEvent,
16805) {
16806    if let Ok(mut state) = shared.lock() {
16807        state
16808            .session_events
16809            .entry(session_id)
16810            .or_default()
16811            .push_back(event);
16812    }
16813}
16814
16815fn pop_http2_event(
16816    queue: &mut BTreeMap<u64, VecDeque<Http2BridgeEvent>>,
16817    id: u64,
16818) -> Option<Http2BridgeEvent> {
16819    queue.get_mut(&id).and_then(VecDeque::pop_front)
16820}
16821
16822fn wait_for_http2_event(
16823    shared: &Arc<Mutex<crate::state::Http2SharedState>>,
16824    id: u64,
16825    is_server: bool,
16826    wait_ms: u64,
16827) -> Option<Http2BridgeEvent> {
16828    let deadline = Instant::now() + Duration::from_millis(wait_ms);
16829    loop {
16830        if let Ok(mut state) = shared.lock() {
16831            let queue = if is_server {
16832                &mut state.server_events
16833            } else {
16834                &mut state.session_events
16835            };
16836            if let Some(event) = pop_http2_event(queue, id) {
16837                return Some(event);
16838            }
16839        }
16840        if wait_ms == 0 || Instant::now() >= deadline {
16841            return None;
16842        }
16843        thread::sleep(HTTP2_POLL_DELAY);
16844    }
16845}
16846
16847fn next_http2_session_id(shared: &mut crate::state::Http2SharedState) -> u64 {
16848    shared.next_session_id += 1;
16849    shared.next_session_id
16850}
16851
16852fn next_http2_stream_id(shared: &mut crate::state::Http2SharedState) -> u64 {
16853    shared.next_stream_id += 1;
16854    shared.next_stream_id
16855}
16856
16857fn http2_reason(code: Option<u32>) -> Reason {
16858    code.unwrap_or(Reason::NO_ERROR.into()).into()
16859}
16860
16861fn http2_error_payload(message: impl Into<String>) -> String {
16862    serde_json::to_string(&json!({
16863        "name": "Error",
16864        "code": "ERR_HTTP2_ERROR",
16865        "message": message.into(),
16866    }))
16867    .unwrap_or_else(|_| {
16868        String::from(
16869            "{\"name\":\"Error\",\"code\":\"ERR_HTTP2_ERROR\",\"message\":\"HTTP/2 bridge error\"}",
16870        )
16871    })
16872}
16873
16874fn http2_socket_snapshot(local_addr: SocketAddr, remote_addr: SocketAddr) -> Http2SocketSnapshot {
16875    Http2SocketSnapshot {
16876        encrypted: false,
16877        allow_half_open: false,
16878        local_address: Some(local_addr.ip().to_string()),
16879        local_port: Some(local_addr.port()),
16880        local_family: Some(socket_addr_family(&local_addr).to_string()),
16881        remote_address: Some(remote_addr.ip().to_string()),
16882        remote_port: Some(remote_addr.port()),
16883        remote_family: Some(socket_addr_family(&remote_addr).to_string()),
16884        servername: None,
16885        alpn_protocol: Some(String::from("h2c")),
16886    }
16887}
16888
16889fn http2_wait_result(kind: &str, id: u64) -> Value {
16890    json!({
16891        "kind": kind,
16892        "id": id,
16893    })
16894}
16895
16896fn is_http2_terminal_event(event: &Http2BridgeEvent, is_server: bool, id: u64) -> bool {
16897    if is_server {
16898        event.kind == "serverClose" && event.id == id
16899    } else {
16900        event.kind == "sessionClose" && event.id == id
16901    }
16902}
16903
16904fn dispatch_http2_wait_loop(
16905    process: &ActiveProcess,
16906    id: u64,
16907    is_server: bool,
16908) -> Result<Value, SidecarError> {
16909    loop {
16910        if let Some(event) = wait_for_http2_event(&process.http2.shared, id, is_server, 50) {
16911            let payload = serde_json::to_value(&event).map_err(|error| {
16912                SidecarError::Execution(format!("ERR_AGENT_OS_NODE_SYNC_RPC: {error}"))
16913            })?;
16914            process
16915                .execution
16916                .send_javascript_stream_event("http2", payload.clone())?;
16917            if is_http2_terminal_event(&event, is_server, id) {
16918                return Ok(payload);
16919            }
16920            continue;
16921        }
16922
16923        let exists = process
16924            .http2
16925            .shared
16926            .lock()
16927            .map(|state| {
16928                if is_server {
16929                    state.servers.contains_key(&id)
16930                } else {
16931                    state.sessions.contains_key(&id)
16932                }
16933            })
16934            .unwrap_or(false);
16935        if !exists {
16936            return Ok(if is_server {
16937                http2_wait_result("serverClose", id)
16938            } else {
16939                http2_wait_result("sessionClose", id)
16940            });
16941        }
16942    }
16943}
16944
16945fn dispatch_http_wait_loop(process: &ActiveProcess, server_id: u64) -> Result<Value, SidecarError> {
16946    loop {
16947        if !process.http_servers.contains_key(&server_id) {
16948            return Ok(json!({
16949                "kind": "serverClose",
16950                "id": server_id,
16951            }));
16952        }
16953        thread::sleep(Duration::from_millis(25));
16954    }
16955}
16956
16957fn http2_settings_from_value(settings: &BTreeMap<String, Value>) -> BTreeMap<String, Value> {
16958    settings.clone()
16959}
16960
16961fn parse_http2_headers_json(
16962    headers_json: &str,
16963    label: &str,
16964) -> Result<BTreeMap<String, Value>, SidecarError> {
16965    serde_json::from_str::<BTreeMap<String, Value>>(headers_json)
16966        .map_err(|error| SidecarError::InvalidState(format!("{label} must be valid JSON: {error}")))
16967}
16968
16969fn apply_http2_header_values(
16970    header_map: &mut HeaderMap,
16971    name: &str,
16972    value: &Value,
16973) -> Result<(), SidecarError> {
16974    let header_name = HeaderName::from_bytes(name.as_bytes()).map_err(|error| {
16975        SidecarError::InvalidState(format!("invalid HTTP/2 header name {name:?}: {error}"))
16976    })?;
16977    match value {
16978        Value::Array(values) => {
16979            for value in values {
16980                apply_http2_header_values(header_map, name, value)?;
16981            }
16982        }
16983        Value::String(text) => {
16984            let value = HeaderValue::from_str(text).map_err(|error| {
16985                SidecarError::InvalidState(format!(
16986                    "invalid HTTP/2 header value for {name}: {error}"
16987                ))
16988            })?;
16989            header_map.append(header_name.clone(), value);
16990        }
16991        Value::Number(number) => {
16992            let value = HeaderValue::from_str(&number.to_string()).map_err(|error| {
16993                SidecarError::InvalidState(format!(
16994                    "invalid HTTP/2 numeric header value for {name}: {error}"
16995                ))
16996            })?;
16997            header_map.append(header_name.clone(), value);
16998        }
16999        Value::Bool(boolean) => {
17000            let value = HeaderValue::from_str(if *boolean { "true" } else { "false" }).map_err(
17001                |error| {
17002                    SidecarError::InvalidState(format!(
17003                        "invalid HTTP/2 boolean header value for {name}: {error}"
17004                    ))
17005                },
17006            )?;
17007            header_map.append(header_name.clone(), value);
17008        }
17009        Value::Null => {}
17010        Value::Object(_) => {
17011            return Err(SidecarError::InvalidState(format!(
17012                "unsupported HTTP/2 header object value for {name}"
17013            )));
17014        }
17015    }
17016    Ok(())
17017}
17018
17019fn build_http2_request(headers_json: &str) -> Result<Request<()>, SidecarError> {
17020    let headers = parse_http2_headers_json(headers_json, "HTTP/2 request headers")?;
17021    let method = headers
17022        .get(":method")
17023        .and_then(Value::as_str)
17024        .unwrap_or("GET");
17025    let path = headers.get(":path").and_then(Value::as_str).unwrap_or("/");
17026    let mut builder = Request::builder()
17027        .method(Method::from_bytes(method.as_bytes()).map_err(|error| {
17028            SidecarError::InvalidState(format!("invalid HTTP/2 method {method:?}: {error}"))
17029        })?)
17030        .uri(path.parse::<Uri>().map_err(|error| {
17031            SidecarError::InvalidState(format!("invalid HTTP/2 path {path:?}: {error}"))
17032        })?);
17033    {
17034        let header_map = builder.headers_mut().expect("request header map");
17035        for (name, value) in &headers {
17036            if name.starts_with(':') {
17037                continue;
17038            }
17039            apply_http2_header_values(header_map, name, value)?;
17040        }
17041    }
17042    builder
17043        .body(())
17044        .map_err(|error| SidecarError::InvalidState(format!("invalid HTTP/2 request: {error}")))
17045}
17046
17047fn build_http2_response(headers_json: &str) -> Result<Response<()>, SidecarError> {
17048    let headers = parse_http2_headers_json(headers_json, "HTTP/2 response headers")?;
17049    let status = headers
17050        .get(":status")
17051        .and_then(Value::as_u64)
17052        .or_else(|| {
17053            headers
17054                .get(":status")
17055                .and_then(Value::as_str)
17056                .and_then(|value| value.parse::<u16>().ok().map(u64::from))
17057        })
17058        .unwrap_or(200);
17059    let mut builder = Response::builder().status(status as u16);
17060    {
17061        let header_map = builder.headers_mut().expect("response header map");
17062        for (name, value) in &headers {
17063            if name.starts_with(':') {
17064                continue;
17065            }
17066            apply_http2_header_values(header_map, name, value)?;
17067        }
17068    }
17069    builder.body(()).map_err(|error| {
17070        SidecarError::InvalidState(format!("invalid HTTP/2 response headers: {error}"))
17071    })
17072}
17073
17074fn serialize_http2_headers_map(
17075    pseudo: BTreeMap<String, Value>,
17076    headers: &HeaderMap,
17077) -> Result<String, SidecarError> {
17078    let mut serialized = pseudo;
17079    for (name, value) in headers {
17080        let name = name.as_str().to_string();
17081        let value = Value::String(
17082            value
17083                .to_str()
17084                .map_err(|error| {
17085                    SidecarError::Execution(format!("invalid HTTP/2 header value: {error}"))
17086                })?
17087                .to_owned(),
17088        );
17089        match serialized.get_mut(&name) {
17090            Some(Value::Array(values)) => values.push(value),
17091            Some(existing) => {
17092                let first = existing.clone();
17093                *existing = Value::Array(vec![first, value]);
17094            }
17095            None => {
17096                serialized.insert(name, value);
17097            }
17098        }
17099    }
17100    serde_json::to_string(&serialized)
17101        .map_err(|error| SidecarError::Execution(format!("ERR_AGENT_OS_NODE_SYNC_RPC: {error}")))
17102}
17103
17104fn serialize_http2_request_headers(
17105    request: &Request<h2::RecvStream>,
17106) -> Result<String, SidecarError> {
17107    let mut pseudo = BTreeMap::new();
17108    pseudo.insert(
17109        String::from(":method"),
17110        Value::String(request.method().as_str().to_string()),
17111    );
17112    pseudo.insert(
17113        String::from(":path"),
17114        Value::String(
17115            request
17116                .uri()
17117                .path_and_query()
17118                .map(|value| value.as_str().to_string())
17119                .unwrap_or_else(|| String::from("/")),
17120        ),
17121    );
17122    serialize_http2_headers_map(pseudo, request.headers())
17123}
17124
17125fn serialize_http2_response_headers(
17126    response: &Response<h2::RecvStream>,
17127) -> Result<String, SidecarError> {
17128    let mut pseudo = BTreeMap::new();
17129    pseudo.insert(
17130        String::from(":status"),
17131        Value::Number(serde_json::Number::from(response.status().as_u16())),
17132    );
17133    serialize_http2_headers_map(pseudo, response.headers())
17134}
17135
17136fn remove_http2_session_resources(
17137    shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17138    session_id: u64,
17139) {
17140    if let Ok(mut state) = shared.lock() {
17141        state.sessions.remove(&session_id);
17142        state.session_events.remove(&session_id);
17143        let stream_ids = state
17144            .streams
17145            .iter()
17146            .filter_map(|(stream_id, stream)| {
17147                (stream.session_id == session_id).then_some(*stream_id)
17148            })
17149            .collect::<Vec<_>>();
17150        for stream_id in stream_ids {
17151            state.streams.remove(&stream_id);
17152        }
17153    }
17154}
17155
17156fn spawn_http2_client_session(
17157    shared: Arc<Mutex<crate::state::Http2SharedState>>,
17158    session_id: u64,
17159    remote_addr: SocketAddr,
17160    tls: Option<JavascriptTlsBridgeOptions>,
17161    snapshot: Arc<Mutex<Http2SessionSnapshot>>,
17162    mut command_rx: UnboundedReceiver<Http2SessionCommand>,
17163) {
17164    thread::spawn(move || {
17165        let runtime = match TokioRuntimeBuilder::new_current_thread()
17166            .enable_all()
17167            .build()
17168        {
17169            Ok(runtime) => runtime,
17170            Err(error) => {
17171                push_http2_session_event(
17172                    &shared,
17173                    session_id,
17174                    Http2BridgeEvent {
17175                        kind: String::from("sessionError"),
17176                        id: session_id,
17177                        data: Some(http2_error_payload(error.to_string())),
17178                        ..Http2BridgeEvent::default()
17179                    },
17180                );
17181                remove_http2_session_resources(&shared, session_id);
17182                return;
17183            }
17184        };
17185
17186        runtime.block_on(async move {
17187            let stream = match tokio::net::TcpStream::connect(remote_addr).await {
17188                Ok(stream) => stream,
17189                Err(error) => {
17190                    push_http2_session_event(
17191                        &shared,
17192                        session_id,
17193                        Http2BridgeEvent {
17194                            kind: String::from("sessionError"),
17195                            id: session_id,
17196                            data: Some(http2_error_payload(error.to_string())),
17197                            ..Http2BridgeEvent::default()
17198                        },
17199                    );
17200                    remove_http2_session_resources(&shared, session_id);
17201                    return;
17202                }
17203            };
17204
17205            let local_addr = match stream.local_addr() {
17206                Ok(addr) => addr,
17207                Err(error) => {
17208                    push_http2_session_event(
17209                        &shared,
17210                        session_id,
17211                        Http2BridgeEvent {
17212                            kind: String::from("sessionError"),
17213                            id: session_id,
17214                            data: Some(http2_error_payload(error.to_string())),
17215                            ..Http2BridgeEvent::default()
17216                        },
17217                    );
17218                    remove_http2_session_resources(&shared, session_id);
17219                    return;
17220                }
17221            };
17222
17223            {
17224                let mut snapshot_guard = snapshot.lock().expect("http2 snapshot lock");
17225                snapshot_guard.socket = http2_socket_snapshot(local_addr, remote_addr);
17226                if let Some(options) = tls.as_ref() {
17227                    snapshot_guard.encrypted = true;
17228                    snapshot_guard.alpn_protocol = Some(String::from("h2"));
17229                    snapshot_guard.socket.encrypted = true;
17230                    snapshot_guard.socket.servername = options.servername.clone();
17231                    snapshot_guard.socket.alpn_protocol = Some(String::from("h2"));
17232                }
17233                snapshot_guard.state = http2_runtime_snapshot();
17234            }
17235            if let Ok(snapshot_json) =
17236                http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())
17237            {
17238                push_http2_session_event(
17239                    &shared,
17240                    session_id,
17241                    Http2BridgeEvent {
17242                        kind: String::from("sessionConnect"),
17243                        id: session_id,
17244                        data: Some(snapshot_json),
17245                        ..Http2BridgeEvent::default()
17246                    },
17247                );
17248            }
17249
17250            let io: Pin<Box<dyn Http2AsyncIo>> = if let Some(options) = tls.as_ref() {
17251                let server_name = match ServerName::try_from(
17252                    options
17253                        .servername
17254                        .clone()
17255                        .unwrap_or_else(|| String::from("localhost")),
17256                ) {
17257                    Ok(server_name) => server_name,
17258                    Err(_) => {
17259                        push_http2_session_event(
17260                            &shared,
17261                            session_id,
17262                            Http2BridgeEvent {
17263                                kind: String::from("sessionError"),
17264                                id: session_id,
17265                                data: Some(http2_error_payload("invalid TLS servername")),
17266                                ..Http2BridgeEvent::default()
17267                            },
17268                        );
17269                        remove_http2_session_resources(&shared, session_id);
17270                        return;
17271                    }
17272                };
17273                let connector = match build_client_tls_config(options) {
17274                    Ok(config) => TlsConnector::from(Arc::new(config)),
17275                    Err(error) => {
17276                        push_http2_session_event(
17277                            &shared,
17278                            session_id,
17279                            Http2BridgeEvent {
17280                                kind: String::from("sessionError"),
17281                                id: session_id,
17282                                data: Some(http2_error_payload(error.to_string())),
17283                                ..Http2BridgeEvent::default()
17284                            },
17285                        );
17286                        remove_http2_session_resources(&shared, session_id);
17287                        return;
17288                    }
17289                };
17290                match connector.connect(server_name, stream).await {
17291                    Ok(tls_stream) => Box::pin(tls_stream),
17292                    Err(error) => {
17293                        push_http2_session_event(
17294                            &shared,
17295                            session_id,
17296                            Http2BridgeEvent {
17297                                kind: String::from("sessionError"),
17298                                id: session_id,
17299                                data: Some(http2_error_payload(error.to_string())),
17300                                ..Http2BridgeEvent::default()
17301                            },
17302                        );
17303                        remove_http2_session_resources(&shared, session_id);
17304                        return;
17305                    }
17306                }
17307            } else {
17308                Box::pin(stream)
17309            };
17310
17311            let (mut sender, connection) = match client::handshake(io).await {
17312                Ok(parts) => parts,
17313                Err(error) => {
17314                    push_http2_session_event(
17315                        &shared,
17316                        session_id,
17317                        Http2BridgeEvent {
17318                            kind: String::from("sessionError"),
17319                            id: session_id,
17320                            data: Some(http2_error_payload(error.to_string())),
17321                            ..Http2BridgeEvent::default()
17322                        },
17323                    );
17324                    remove_http2_session_resources(&shared, session_id);
17325                    return;
17326                }
17327            };
17328
17329            let (status_tx, mut status_rx) = unbounded_channel::<Result<(), String>>();
17330            tokio::spawn(async move {
17331                let _ = status_tx.send(connection.await.map_err(|error| error.to_string()));
17332            });
17333
17334            let streams: Arc<Mutex<BTreeMap<u64, ClientHttp2StreamState>>> =
17335                Arc::new(Mutex::new(BTreeMap::new()));
17336
17337            loop {
17338                tokio::select! {
17339                    Some(result) = status_rx.recv() => {
17340                        if let Err(message) = result {
17341                            push_http2_session_event(
17342                                &shared,
17343                                session_id,
17344                                Http2BridgeEvent {
17345                                    kind: String::from("sessionError"),
17346                                    id: session_id,
17347                                    data: Some(http2_error_payload(message)),
17348                                    ..Http2BridgeEvent::default()
17349                                },
17350                            );
17351                        }
17352                        push_http2_session_event(
17353                            &shared,
17354                            session_id,
17355                            Http2BridgeEvent {
17356                                kind: String::from("sessionClose"),
17357                                id: session_id,
17358                                ..Http2BridgeEvent::default()
17359                            },
17360                        );
17361                        remove_http2_session_resources(&shared, session_id);
17362                        break;
17363                    }
17364                    Some(command) = command_rx.recv() => {
17365                        match command {
17366                            Http2SessionCommand::Request { headers_json, options_json, respond_to } => {
17367                                let request = match build_http2_request(&headers_json) {
17368                                    Ok(request) => request,
17369                                    Err(error) => {
17370                                        let _ = respond_to.send(Err(error.to_string()));
17371                                        continue;
17372                                    }
17373                                };
17374                                let options: JavascriptHttp2RequestOptions =
17375                                    serde_json::from_str(&options_json).unwrap_or_default();
17376                                let stream_id = {
17377                                    let mut state = shared.lock().expect("http2 shared state");
17378                                    let stream_id = next_http2_stream_id(&mut state);
17379                                    state.streams.insert(
17380                                        stream_id,
17381                                        ActiveHttp2Stream {
17382                                            session_id,
17383                                            paused: Arc::new(AtomicBool::new(false)),
17384                                        },
17385                                    );
17386                                    stream_id
17387                                };
17388                                match sender.send_request(request, options.end_stream) {
17389                                    Ok((response_future, send_stream)) => {
17390                                        if !options.end_stream {
17391                                            streams
17392                                                .lock()
17393                                                .expect("http2 client streams")
17394                                                .insert(stream_id, ClientHttp2StreamState { send_stream: Some(send_stream) });
17395                                        }
17396                                        let shared_clone = Arc::clone(&shared);
17397                                        let snapshot_clone = Arc::clone(&snapshot);
17398                                        tokio::spawn(async move {
17399                                            match response_future.await {
17400                                                Ok(response) => {
17401                                                    if let Ok(headers_json) = serialize_http2_response_headers(&response) {
17402                                                        push_http2_session_event(
17403                                                            &shared_clone,
17404                                                            session_id,
17405                                                            Http2BridgeEvent {
17406                                                                kind: String::from("clientResponseHeaders"),
17407                                                                id: stream_id,
17408                                                                data: Some(headers_json),
17409                                                                ..Http2BridgeEvent::default()
17410                                                            },
17411                                                        );
17412                                                    }
17413                                                    let mut body = response.into_body();
17414                                                    while let Some(chunk) = body.data().await {
17415                                                        match chunk {
17416                                                            Ok(bytes) => {
17417                                                                let paused = {
17418                                                                    let state = shared_clone.lock().expect("http2 shared state");
17419                                                                    state.streams.get(&stream_id).map(|stream| Arc::clone(&stream.paused))
17420                                                                };
17421                                                                if let Some(paused) = paused {
17422                                                                    while paused.load(Ordering::SeqCst) {
17423                                                                        tokio::time::sleep(HTTP2_POLL_DELAY).await;
17424                                                                    }
17425                                                                }
17426                                                                let _ = body.flow_control().release_capacity(bytes.len());
17427                                                                push_http2_session_event(
17428                                                                    &shared_clone,
17429                                                                    session_id,
17430                                                                    Http2BridgeEvent {
17431                                                                        kind: String::from("clientData"),
17432                                                                        id: stream_id,
17433                                                                        data: Some(base64::engine::general_purpose::STANDARD.encode(bytes)),
17434                                                                        ..Http2BridgeEvent::default()
17435                                                                    },
17436                                                                );
17437                                                            }
17438                                                            Err(error) => {
17439                                                                push_http2_session_event(
17440                                                                    &shared_clone,
17441                                                                    session_id,
17442                                                                    Http2BridgeEvent {
17443                                                                        kind: String::from("clientError"),
17444                                                                        id: stream_id,
17445                                                                        data: Some(http2_error_payload(error.to_string())),
17446                                                                        ..Http2BridgeEvent::default()
17447                                                                    },
17448                                                                );
17449                                                                break;
17450                                                            }
17451                                                        }
17452                                                    }
17453                                                    {
17454                                                        let mut snapshot = snapshot_clone.lock().expect("http2 snapshot lock");
17455                                                        snapshot.state.next_stream_id =
17456                                                            snapshot.state.next_stream_id.saturating_add(2);
17457                                                    }
17458                                                    push_http2_session_event(
17459                                                        &shared_clone,
17460                                                        session_id,
17461                                                        Http2BridgeEvent {
17462                                                            kind: String::from("clientEnd"),
17463                                                            id: stream_id,
17464                                                            ..Http2BridgeEvent::default()
17465                                                        },
17466                                                    );
17467                                                    push_http2_session_event(
17468                                                        &shared_clone,
17469                                                        session_id,
17470                                                        Http2BridgeEvent {
17471                                                            kind: String::from("clientClose"),
17472                                                            id: stream_id,
17473                                                            extra_number: Some(0),
17474                                                            ..Http2BridgeEvent::default()
17475                                                        },
17476                                                    );
17477                                                    if let Ok(mut state) = shared_clone.lock() {
17478                                                        state.streams.remove(&stream_id);
17479                                                    }
17480                                                }
17481                                                Err(error) => {
17482                                                    push_http2_session_event(
17483                                                        &shared_clone,
17484                                                        session_id,
17485                                                        Http2BridgeEvent {
17486                                                            kind: String::from("clientError"),
17487                                                            id: stream_id,
17488                                                            data: Some(http2_error_payload(error.to_string())),
17489                                                            ..Http2BridgeEvent::default()
17490                                                        },
17491                                                    );
17492                                                    push_http2_session_event(
17493                                                        &shared_clone,
17494                                                        session_id,
17495                                                        Http2BridgeEvent {
17496                                                            kind: String::from("clientClose"),
17497                                                            id: stream_id,
17498                                                            extra_number: Some(u32::from(Reason::INTERNAL_ERROR) as u64),
17499                                                            ..Http2BridgeEvent::default()
17500                                                        },
17501                                                    );
17502                                                    if let Ok(mut state) = shared_clone.lock() {
17503                                                        state.streams.remove(&stream_id);
17504                                                    }
17505                                                }
17506                                            }
17507                                        });
17508                                        let _ = respond_to.send(Ok(json!(stream_id)));
17509                                    }
17510                                    Err(error) => {
17511                                        if let Ok(mut state) = shared.lock() {
17512                                            state.streams.remove(&stream_id);
17513                                        }
17514                                        let _ = respond_to.send(Err(error.to_string()));
17515                                    }
17516                                }
17517                            }
17518                            Http2SessionCommand::Settings { settings_json, respond_to } => {
17519                                let settings = serde_json::from_str::<BTreeMap<String, Value>>(&settings_json)
17520                                    .unwrap_or_default();
17521                                {
17522                                    let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
17523                                    snapshot.local_settings = http2_settings_from_value(&settings);
17524                                }
17525                                if let Ok(headers_json) = serde_json::to_string(&settings) {
17526                                    push_http2_session_event(
17527                                        &shared,
17528                                        session_id,
17529                                        Http2BridgeEvent {
17530                                            kind: String::from("sessionLocalSettings"),
17531                                            id: session_id,
17532                                            data: Some(headers_json.clone()),
17533                                            ..Http2BridgeEvent::default()
17534                                        },
17535                                    );
17536                                    push_http2_session_event(
17537                                        &shared,
17538                                        session_id,
17539                                        Http2BridgeEvent {
17540                                            kind: String::from("sessionSettingsAck"),
17541                                            id: session_id,
17542                                            ..Http2BridgeEvent::default()
17543                                        },
17544                                    );
17545                                }
17546                                let _ = respond_to.send(Ok(Value::Null));
17547                            }
17548                            Http2SessionCommand::SetLocalWindowSize { size, respond_to } => {
17549                                {
17550                                    let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
17551                                    snapshot.state.local_window_size = size;
17552                                    snapshot.state.effective_local_window_size = size;
17553                                }
17554                                let value = snapshot
17555                                    .lock()
17556                                    .ok()
17557                                    .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok())
17558                                    .map(Value::String)
17559                                    .unwrap_or(Value::Null);
17560                                let _ = respond_to.send(Ok(value));
17561                            }
17562                            Http2SessionCommand::Goaway { error_code, last_stream_id, opaque_data, respond_to } => {
17563                                push_http2_session_event(
17564                                    &shared,
17565                                    session_id,
17566                                    Http2BridgeEvent {
17567                                        kind: String::from("sessionGoaway"),
17568                                        id: session_id,
17569                                        data: opaque_data.map(|value| {
17570                                            base64::engine::general_purpose::STANDARD.encode(value)
17571                                        }),
17572                                        extra_number: Some(error_code as u64),
17573                                        flags: Some(last_stream_id as u64),
17574                                        ..Http2BridgeEvent::default()
17575                                    },
17576                                );
17577                                let _ = respond_to.send(Ok(Value::Null));
17578                            }
17579                            Http2SessionCommand::Close { respond_to, .. } => {
17580                                let _ = respond_to.send(Ok(Value::Null));
17581                                push_http2_session_event(
17582                                    &shared,
17583                                    session_id,
17584                                    Http2BridgeEvent {
17585                                        kind: String::from("sessionClose"),
17586                                        id: session_id,
17587                                        ..Http2BridgeEvent::default()
17588                                    },
17589                                );
17590                                remove_http2_session_resources(&shared, session_id);
17591                                break;
17592                            }
17593                            Http2SessionCommand::StreamWrite { stream_id, chunk, end_stream, respond_to } => {
17594                                let result = streams
17595                                    .lock()
17596                                    .expect("http2 client streams")
17597                                    .get_mut(&stream_id)
17598                                    .and_then(|stream| stream.send_stream.as_mut())
17599                                    .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 client stream {stream_id}")))
17600                                    .and_then(|stream| stream.send_data(Bytes::from(chunk), end_stream).map_err(|error| SidecarError::Execution(error.to_string())));
17601                                match result {
17602                                    Ok(()) => {
17603                                        if end_stream {
17604                                            streams.lock().expect("http2 client streams").remove(&stream_id);
17605                                        }
17606                                        let _ = respond_to.send(Ok(Value::Bool(true)));
17607                                    }
17608                                    Err(error) => {
17609                                        let _ = respond_to.send(Err(error.to_string()));
17610                                    }
17611                                }
17612                            }
17613                            Http2SessionCommand::StreamClose { stream_id, error_code, respond_to } => {
17614                                let mut streams = streams.lock().expect("http2 client streams");
17615                                let Some(mut state) = streams.remove(&stream_id) else {
17616                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 client stream {stream_id}")));
17617                                    continue;
17618                                };
17619                                if let Some(stream) = state.send_stream.as_mut() {
17620                                    stream.send_reset(http2_reason(error_code));
17621                                }
17622                                if let Ok(mut state) = shared.lock() {
17623                                    state.streams.remove(&stream_id);
17624                                }
17625                                push_http2_session_event(
17626                                    &shared,
17627                                    session_id,
17628                                    Http2BridgeEvent {
17629                                        kind: String::from("clientClose"),
17630                                        id: stream_id,
17631                                        extra_number: Some(u32::from(http2_reason(error_code)) as u64),
17632                                        ..Http2BridgeEvent::default()
17633                                    },
17634                                );
17635                                let _ = respond_to.send(Ok(Value::Null));
17636                            }
17637                            Http2SessionCommand::StreamRespond { respond_to, .. }
17638                            | Http2SessionCommand::StreamPush { respond_to, .. }
17639                            | Http2SessionCommand::StreamRespondWithFile { respond_to, .. } => {
17640                                let _ = respond_to.send(Err(String::from("HTTP/2 client streams cannot send server responses")));
17641                            }
17642                        }
17643                    }
17644                    else => break,
17645                }
17646            }
17647        });
17648    });
17649}
17650
17651fn spawn_http2_server_session(
17652    shared: Arc<Mutex<crate::state::Http2SharedState>>,
17653    server_id: u64,
17654    session_id: u64,
17655    stream: TcpStream,
17656    tls: Option<JavascriptTlsBridgeOptions>,
17657    snapshot: Arc<Mutex<Http2SessionSnapshot>>,
17658    mut command_rx: UnboundedReceiver<Http2SessionCommand>,
17659) {
17660    thread::spawn(move || {
17661        let runtime = match TokioRuntimeBuilder::new_current_thread()
17662            .enable_all()
17663            .build()
17664        {
17665            Ok(runtime) => runtime,
17666            Err(error) => {
17667                push_http2_server_event(
17668                    &shared,
17669                    server_id,
17670                    Http2BridgeEvent {
17671                        kind: String::from("serverStreamError"),
17672                        id: session_id,
17673                        data: Some(http2_error_payload(error.to_string())),
17674                        ..Http2BridgeEvent::default()
17675                    },
17676                );
17677                remove_http2_session_resources(&shared, session_id);
17678                return;
17679            }
17680        };
17681
17682        runtime.block_on(async move {
17683            if let Err(error) = stream.set_nonblocking(true) {
17684                push_http2_server_event(
17685                    &shared,
17686                    server_id,
17687                    Http2BridgeEvent {
17688                        kind: String::from("serverStreamError"),
17689                        id: session_id,
17690                        data: Some(http2_error_payload(error.to_string())),
17691                        ..Http2BridgeEvent::default()
17692                    },
17693                );
17694                remove_http2_session_resources(&shared, session_id);
17695                return;
17696            }
17697            let stream = match tokio::net::TcpStream::from_std(stream) {
17698                Ok(stream) => stream,
17699                Err(error) => {
17700                    push_http2_server_event(
17701                        &shared,
17702                        server_id,
17703                        Http2BridgeEvent {
17704                            kind: String::from("serverStreamError"),
17705                            id: session_id,
17706                            data: Some(http2_error_payload(error.to_string())),
17707                            ..Http2BridgeEvent::default()
17708                        },
17709                    );
17710                    remove_http2_session_resources(&shared, session_id);
17711                    return;
17712                }
17713            };
17714            let local_addr = match stream.local_addr() {
17715                Ok(addr) => addr,
17716                Err(error) => {
17717                    push_http2_server_event(
17718                        &shared,
17719                        server_id,
17720                        Http2BridgeEvent {
17721                            kind: String::from("serverStreamError"),
17722                            id: session_id,
17723                            data: Some(http2_error_payload(error.to_string())),
17724                            ..Http2BridgeEvent::default()
17725                        },
17726                    );
17727                    remove_http2_session_resources(&shared, session_id);
17728                    return;
17729                }
17730            };
17731            let remote_addr = match stream.peer_addr() {
17732                Ok(addr) => addr,
17733                Err(error) => {
17734                    push_http2_server_event(
17735                        &shared,
17736                        server_id,
17737                        Http2BridgeEvent {
17738                            kind: String::from("serverStreamError"),
17739                            id: session_id,
17740                            data: Some(http2_error_payload(error.to_string())),
17741                            ..Http2BridgeEvent::default()
17742                        },
17743                    );
17744                    remove_http2_session_resources(&shared, session_id);
17745                    return;
17746                }
17747            };
17748            {
17749                let mut snapshot_guard = snapshot.lock().expect("http2 snapshot lock");
17750                snapshot_guard.socket = http2_socket_snapshot(local_addr, remote_addr);
17751                if tls.is_some() {
17752                    snapshot_guard.encrypted = true;
17753                    snapshot_guard.alpn_protocol = Some(String::from("h2"));
17754                    snapshot_guard.socket.encrypted = true;
17755                    snapshot_guard.socket.alpn_protocol = Some(String::from("h2"));
17756                }
17757                snapshot_guard.state = http2_runtime_snapshot();
17758            }
17759            if let Ok(snapshot_json) =
17760                http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())
17761            {
17762                push_http2_server_event(
17763                    &shared,
17764                    server_id,
17765                    Http2BridgeEvent {
17766                        kind: String::from(if tls.is_some() {
17767                            "serverSecureConnection"
17768                        } else {
17769                            "serverConnection"
17770                        }),
17771                        id: server_id,
17772                        data: Some(serde_json::to_string(&http2_socket_snapshot(local_addr, remote_addr)).unwrap_or_default()),
17773                        ..Http2BridgeEvent::default()
17774                    },
17775                );
17776                push_http2_server_event(
17777                    &shared,
17778                    server_id,
17779                    Http2BridgeEvent {
17780                        kind: String::from("serverSession"),
17781                        id: server_id,
17782                        data: Some(snapshot_json),
17783                        extra_number: Some(session_id),
17784                        ..Http2BridgeEvent::default()
17785                    },
17786                );
17787            }
17788
17789            let io: Pin<Box<dyn Http2AsyncIo>> = if let Some(options) = tls.as_ref() {
17790                let acceptor = match build_server_tls_config(options) {
17791                    Ok(config) => TlsAcceptor::from(Arc::new(config)),
17792                    Err(error) => {
17793                        push_http2_server_event(
17794                            &shared,
17795                            server_id,
17796                            Http2BridgeEvent {
17797                                kind: String::from("serverStreamError"),
17798                                id: session_id,
17799                                data: Some(http2_error_payload(error.to_string())),
17800                                ..Http2BridgeEvent::default()
17801                            },
17802                        );
17803                        remove_http2_session_resources(&shared, session_id);
17804                        return;
17805                    }
17806                };
17807                match acceptor.accept(stream).await {
17808                    Ok(tls_stream) => Box::pin(tls_stream),
17809                    Err(error) => {
17810                        push_http2_server_event(
17811                            &shared,
17812                            server_id,
17813                            Http2BridgeEvent {
17814                                kind: String::from("serverStreamError"),
17815                                id: session_id,
17816                                data: Some(http2_error_payload(error.to_string())),
17817                                ..Http2BridgeEvent::default()
17818                            },
17819                        );
17820                        remove_http2_session_resources(&shared, session_id);
17821                        return;
17822                    }
17823                }
17824            } else {
17825                Box::pin(stream)
17826            };
17827
17828            let mut connection = match server::handshake(io).await {
17829                Ok(connection) => connection,
17830                Err(error) => {
17831                    push_http2_server_event(
17832                        &shared,
17833                        server_id,
17834                        Http2BridgeEvent {
17835                            kind: String::from("serverStreamError"),
17836                            id: session_id,
17837                            data: Some(http2_error_payload(error.to_string())),
17838                            ..Http2BridgeEvent::default()
17839                        },
17840                    );
17841                    remove_http2_session_resources(&shared, session_id);
17842                    return;
17843                }
17844            };
17845
17846            let streams: Arc<Mutex<BTreeMap<u64, ServerHttp2StreamState>>> =
17847                Arc::new(Mutex::new(BTreeMap::new()));
17848
17849            loop {
17850                tokio::select! {
17851                    incoming = connection.accept() => {
17852                        match incoming {
17853                            Some(Ok((request, respond))) => {
17854                                let headers_json = match serialize_http2_request_headers(&request) {
17855                                    Ok(headers) => headers,
17856                                    Err(error) => {
17857                                        push_http2_server_event(
17858                                            &shared,
17859                                            server_id,
17860                                            Http2BridgeEvent {
17861                                                kind: String::from("serverStreamError"),
17862                                                id: server_id,
17863                                                data: Some(http2_error_payload(error.to_string())),
17864                                                ..Http2BridgeEvent::default()
17865                                            },
17866                                        );
17867                                        continue;
17868                                    }
17869                                };
17870                                let stream_id = {
17871                                    let mut state = shared.lock().expect("http2 shared state");
17872                                    let stream_id = next_http2_stream_id(&mut state);
17873                                    state.streams.insert(
17874                                        stream_id,
17875                                        ActiveHttp2Stream {
17876                                            session_id,
17877                                            paused: Arc::new(AtomicBool::new(false)),
17878                                        },
17879                                    );
17880                                    stream_id
17881                                };
17882                                streams.lock().expect("http2 server streams").insert(
17883                                    stream_id,
17884                                    ServerHttp2StreamState {
17885                                        send_response: Some(ServerHttp2Responder::Regular(respond)),
17886                                        send_stream: None,
17887                                    },
17888                                );
17889                                let snapshot_json = snapshot
17890                                    .lock()
17891                                    .ok()
17892                                    .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok());
17893                                push_http2_server_event(
17894                                    &shared,
17895                                    server_id,
17896                                    Http2BridgeEvent {
17897                                        kind: String::from("serverStream"),
17898                                        id: server_id,
17899                                        data: Some(stream_id.to_string()),
17900                                        extra: snapshot_json,
17901                                        extra_number: Some(session_id),
17902                                        extra_headers: Some(headers_json),
17903                                        flags: Some(0),
17904                                    },
17905                                );
17906                                let shared_clone = Arc::clone(&shared);
17907                                tokio::spawn(async move {
17908                                    let mut body = request.into_body();
17909                                    while let Some(chunk) = body.data().await {
17910                                        match chunk {
17911                                            Ok(bytes) => {
17912                                                let paused = {
17913                                                    let state = shared_clone.lock().expect("http2 shared state");
17914                                                    state.streams.get(&stream_id).map(|stream| Arc::clone(&stream.paused))
17915                                                };
17916                                                if let Some(paused) = paused {
17917                                                    while paused.load(Ordering::SeqCst) {
17918                                                        tokio::time::sleep(HTTP2_POLL_DELAY).await;
17919                                                    }
17920                                                }
17921                                                let _ = body.flow_control().release_capacity(bytes.len());
17922                                                push_http2_server_event(
17923                                                    &shared_clone,
17924                                                    server_id,
17925                                                    Http2BridgeEvent {
17926                                                        kind: String::from("serverStreamData"),
17927                                                        id: stream_id,
17928                                                        data: Some(base64::engine::general_purpose::STANDARD.encode(bytes)),
17929                                                        ..Http2BridgeEvent::default()
17930                                                    },
17931                                                );
17932                                            }
17933                                            Err(error) => {
17934                                                push_http2_server_event(
17935                                                    &shared_clone,
17936                                                    server_id,
17937                                                    Http2BridgeEvent {
17938                                                        kind: String::from("serverStreamError"),
17939                                                        id: stream_id,
17940                                                        data: Some(http2_error_payload(error.to_string())),
17941                                                        ..Http2BridgeEvent::default()
17942                                                    },
17943                                                );
17944                                                break;
17945                                            }
17946                                        }
17947                                    }
17948                                    push_http2_server_event(
17949                                        &shared_clone,
17950                                        server_id,
17951                                        Http2BridgeEvent {
17952                                            kind: String::from("serverStreamEnd"),
17953                                            id: stream_id,
17954                                            ..Http2BridgeEvent::default()
17955                                        },
17956                                    );
17957                                });
17958                            }
17959                            Some(Err(error)) => {
17960                                push_http2_server_event(
17961                                    &shared,
17962                                    server_id,
17963                                    Http2BridgeEvent {
17964                                        kind: String::from("serverStreamError"),
17965                                        id: server_id,
17966                                        data: Some(http2_error_payload(error.to_string())),
17967                                        ..Http2BridgeEvent::default()
17968                                    },
17969                                );
17970                                break;
17971                            }
17972                            None => {
17973                                push_http2_server_event(
17974                                    &shared,
17975                                    server_id,
17976                                    Http2BridgeEvent {
17977                                        kind: String::from("sessionClose"),
17978                                        id: session_id,
17979                                        ..Http2BridgeEvent::default()
17980                                    },
17981                                );
17982                                remove_http2_session_resources(&shared, session_id);
17983                                break;
17984                            }
17985                        }
17986                    }
17987                    Some(command) = command_rx.recv() => {
17988                        match command {
17989                            Http2SessionCommand::Settings { settings_json, respond_to } => {
17990                                let settings = serde_json::from_str::<BTreeMap<String, Value>>(&settings_json)
17991                                    .unwrap_or_default();
17992                                if let Some(initial_window_size) = settings
17993                                    .get("initialWindowSize")
17994                                    .and_then(Value::as_u64)
17995                                {
17996                                    let _ = connection.set_initial_window_size(initial_window_size as u32);
17997                                }
17998                                {
17999                                    let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18000                                    snapshot.local_settings = http2_settings_from_value(&settings);
18001                                }
18002                                if let Ok(headers_json) = serde_json::to_string(&settings) {
18003                                    push_http2_session_event(
18004                                        &shared,
18005                                        session_id,
18006                                        Http2BridgeEvent {
18007                                            kind: String::from("sessionLocalSettings"),
18008                                            id: session_id,
18009                                            data: Some(headers_json),
18010                                            ..Http2BridgeEvent::default()
18011                                        },
18012                                    );
18013                                }
18014                                let _ = respond_to.send(Ok(Value::Null));
18015                            }
18016                            Http2SessionCommand::SetLocalWindowSize { size, respond_to } => {
18017                                connection.set_target_window_size(size);
18018                                {
18019                                    let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18020                                    snapshot.state.local_window_size = size;
18021                                    snapshot.state.effective_local_window_size = size;
18022                                }
18023                                let value = snapshot
18024                                    .lock()
18025                                    .ok()
18026                                    .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok())
18027                                    .map(Value::String)
18028                                    .unwrap_or(Value::Null);
18029                                let _ = respond_to.send(Ok(value));
18030                            }
18031                            Http2SessionCommand::Goaway { error_code, last_stream_id, opaque_data, respond_to } => {
18032                                connection.abrupt_shutdown(http2_reason(Some(error_code)));
18033                                push_http2_session_event(
18034                                    &shared,
18035                                    session_id,
18036                                    Http2BridgeEvent {
18037                                        kind: String::from("sessionGoaway"),
18038                                        id: session_id,
18039                                        data: opaque_data.map(|value| {
18040                                            base64::engine::general_purpose::STANDARD.encode(value)
18041                                        }),
18042                                        extra_number: Some(error_code as u64),
18043                                        flags: Some(last_stream_id as u64),
18044                                        ..Http2BridgeEvent::default()
18045                                    },
18046                                );
18047                                let _ = respond_to.send(Ok(Value::Null));
18048                            }
18049                            Http2SessionCommand::Close { abrupt, respond_to } => {
18050                                if abrupt {
18051                                    connection.abrupt_shutdown(Reason::NO_ERROR);
18052                                } else {
18053                                    connection.graceful_shutdown();
18054                                }
18055                                let _ = respond_to.send(Ok(Value::Null));
18056                                push_http2_session_event(
18057                                    &shared,
18058                                    session_id,
18059                                    Http2BridgeEvent {
18060                                        kind: String::from("sessionClose"),
18061                                        id: session_id,
18062                                        ..Http2BridgeEvent::default()
18063                                    },
18064                                );
18065                                remove_http2_session_resources(&shared, session_id);
18066                                break;
18067                            }
18068                            Http2SessionCommand::StreamRespond { stream_id, headers_json, respond_to } => {
18069                                let response = match build_http2_response(&headers_json) {
18070                                    Ok(response) => response,
18071                                    Err(error) => {
18072                                        let _ = respond_to.send(Err(error.to_string()));
18073                                        continue;
18074                                    }
18075                                };
18076                                let mut streams = streams.lock().expect("http2 server streams");
18077                                let Some(state) = streams.get_mut(&stream_id) else {
18078                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
18079                                    continue;
18080                                };
18081                                let Some(send_response) = state.send_response.as_mut() else {
18082                                    let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} already responded")));
18083                                    continue;
18084                                };
18085                                match match send_response {
18086                                    ServerHttp2Responder::Regular(send_response) => {
18087                                        send_response.send_response(response, false)
18088                                    }
18089                                    ServerHttp2Responder::Pushed(send_response) => {
18090                                        send_response.send_response(response, false)
18091                                    }
18092                                } {
18093                                    Ok(send_stream) => {
18094                                        state.send_stream = Some(send_stream);
18095                                        state.send_response = None;
18096                                        let _ = respond_to.send(Ok(Value::Null));
18097                                    }
18098                                    Err(error) => {
18099                                        let _ = respond_to.send(Err(error.to_string()));
18100                                    }
18101                                }
18102                            }
18103                            Http2SessionCommand::StreamPush { stream_id, headers_json, respond_to } => {
18104                                let request = match build_http2_request(&headers_json) {
18105                                    Ok(request) => request,
18106                                    Err(error) => {
18107                                        let _ = respond_to.send(Err(error.to_string()));
18108                                        continue;
18109                                    }
18110                                };
18111                                let mut streams_guard = streams.lock().expect("http2 server streams");
18112                                let Some(state) = streams_guard.get_mut(&stream_id) else {
18113                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
18114                                    continue;
18115                                };
18116                                let Some(send_response) = state.send_response.as_mut() else {
18117                                    let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} cannot push after responding")));
18118                                    continue;
18119                                };
18120                                let ServerHttp2Responder::Regular(send_response) = send_response else {
18121                                    let _ = respond_to.send(Err(format!("HTTP/2 pushed stream {stream_id} cannot create nested push promises")));
18122                                    continue;
18123                                };
18124                                match send_response.push_request(request) {
18125                                    Ok(pushed) => {
18126                                        let pushed_stream_id = {
18127                                            let mut state = shared.lock().expect("http2 shared state");
18128                                            let pushed_stream_id = next_http2_stream_id(&mut state);
18129                                            state.streams.insert(
18130                                                pushed_stream_id,
18131                                                ActiveHttp2Stream {
18132                                                    session_id,
18133                                                    paused: Arc::new(AtomicBool::new(false)),
18134                                                },
18135                                            );
18136                                            pushed_stream_id
18137                                        };
18138                                        streams_guard.insert(
18139                                            pushed_stream_id,
18140                                            ServerHttp2StreamState {
18141                                                send_response: Some(ServerHttp2Responder::Pushed(pushed)),
18142                                                send_stream: None,
18143                                            },
18144                                        );
18145                                        let _ = respond_to.send(Ok(json!({
18146                                            "streamId": pushed_stream_id,
18147                                            "headers": headers_json,
18148                                        }).to_string().into()));
18149                                    }
18150                                    Err(error) => {
18151                                        let _ = respond_to.send(Err(error.to_string()));
18152                                    }
18153                                }
18154                            }
18155                            Http2SessionCommand::StreamWrite { stream_id, chunk, end_stream, respond_to } => {
18156                                let mut streams = streams.lock().expect("http2 server streams");
18157                                let Some(state) = streams.get_mut(&stream_id) else {
18158                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
18159                                    continue;
18160                                };
18161                                let Some(send_stream) = state.send_stream.as_mut() else {
18162                                    let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} has not sent response headers")));
18163                                    continue;
18164                                };
18165                                match send_stream.send_data(Bytes::from(chunk), end_stream) {
18166                                    Ok(()) => {
18167                                        if end_stream {
18168                                            streams.remove(&stream_id);
18169                                            if let Ok(mut state) = shared.lock() {
18170                                                state.streams.remove(&stream_id);
18171                                            }
18172                                            push_http2_server_event(
18173                                                &shared,
18174                                                server_id,
18175                                                Http2BridgeEvent {
18176                                                    kind: String::from("serverStreamClose"),
18177                                                    id: stream_id,
18178                                                    extra_number: Some(0),
18179                                                    ..Http2BridgeEvent::default()
18180                                                },
18181                                            );
18182                                        }
18183                                        let _ = respond_to.send(Ok(Value::Bool(true)));
18184                                    }
18185                                    Err(error) => {
18186                                        let _ = respond_to.send(Err(error.to_string()));
18187                                    }
18188                                }
18189                            }
18190                            Http2SessionCommand::StreamClose { stream_id, error_code, respond_to } => {
18191                                let mut streams_guard = streams.lock().expect("http2 server streams");
18192                                let Some(mut state) = streams_guard.remove(&stream_id) else {
18193                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
18194                                    continue;
18195                                };
18196                                let reason = http2_reason(error_code);
18197                                if let Some(send_stream) = state.send_stream.as_mut() {
18198                                    send_stream.send_reset(reason);
18199                                }
18200                                if let Some(send_response) = state.send_response.as_mut() {
18201                                    match send_response {
18202                                        ServerHttp2Responder::Regular(send_response) => {
18203                                            send_response.send_reset(reason)
18204                                        }
18205                                        ServerHttp2Responder::Pushed(send_response) => {
18206                                            send_response.send_reset(reason)
18207                                        }
18208                                    }
18209                                }
18210                                if let Ok(mut shared_guard) = shared.lock() {
18211                                    shared_guard.streams.remove(&stream_id);
18212                                }
18213                                push_http2_server_event(
18214                                    &shared,
18215                                    server_id,
18216                                    Http2BridgeEvent {
18217                                        kind: String::from("serverStreamClose"),
18218                                        id: stream_id,
18219                                        extra_number: Some(u32::from(reason) as u64),
18220                                        ..Http2BridgeEvent::default()
18221                                    },
18222                                );
18223                                let _ = respond_to.send(Ok(Value::Null));
18224                            }
18225                            Http2SessionCommand::StreamRespondWithFile { stream_id, body, headers_json, options_json, respond_to } => {
18226                                let options: JavascriptHttp2FileResponseOptions =
18227                                    serde_json::from_str(&options_json).unwrap_or_default();
18228                                let response = match build_http2_response(&headers_json) {
18229                                    Ok(response) => response,
18230                                    Err(error) => {
18231                                        let _ = respond_to.send(Err(error.to_string()));
18232                                        continue;
18233                                    }
18234                                };
18235                                let offset = usize::try_from(options.offset.unwrap_or_default()).unwrap_or(0);
18236                                let body = if offset >= body.len() {
18237                                    Vec::new()
18238                                } else {
18239                                    let body = &body[offset..];
18240                                    match options.length {
18241                                        Some(length) if length >= 0 => {
18242                                            body[..body.len().min(length as usize)].to_vec()
18243                                        }
18244                                        _ => body.to_vec(),
18245                                    }
18246                                };
18247                                let mut streams_guard = streams.lock().expect("http2 server streams");
18248                                let Some(state) = streams_guard.get_mut(&stream_id) else {
18249                                    let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
18250                                    continue;
18251                                };
18252                                let Some(send_response) = state.send_response.as_mut() else {
18253                                    let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} already responded")));
18254                                    continue;
18255                                };
18256                                match match send_response {
18257                                    ServerHttp2Responder::Regular(send_response) => {
18258                                        send_response.send_response(response, body.is_empty())
18259                                    }
18260                                    ServerHttp2Responder::Pushed(send_response) => {
18261                                        send_response.send_response(response, body.is_empty())
18262                                    }
18263                                } {
18264                                    Ok(mut send_stream) => {
18265                                        state.send_response = None;
18266                                        if body.is_empty() {
18267                                            streams_guard.remove(&stream_id);
18268                                            if let Ok(mut shared_guard) = shared.lock() {
18269                                                shared_guard.streams.remove(&stream_id);
18270                                            }
18271                                        } else {
18272                                            if let Err(error) = send_stream.send_data(Bytes::from(body), true) {
18273                                                let _ = respond_to.send(Err(error.to_string()));
18274                                                continue;
18275                                            }
18276                                            streams_guard.remove(&stream_id);
18277                                            if let Ok(mut shared_guard) = shared.lock() {
18278                                                shared_guard.streams.remove(&stream_id);
18279                                            }
18280                                        }
18281                                        push_http2_server_event(
18282                                            &shared,
18283                                            server_id,
18284                                            Http2BridgeEvent {
18285                                                kind: String::from("serverStreamClose"),
18286                                                id: stream_id,
18287                                                extra_number: Some(0),
18288                                                ..Http2BridgeEvent::default()
18289                                            },
18290                                        );
18291                                        let _ = respond_to.send(Ok(Value::Null));
18292                                    }
18293                                    Err(error) => {
18294                                        let _ = respond_to.send(Err(error.to_string()));
18295                                    }
18296                                }
18297                            }
18298                            Http2SessionCommand::Request { respond_to, .. } => {
18299                                let _ = respond_to.send(Err(String::from("HTTP/2 server sessions cannot initiate client requests")));
18300                            }
18301                        }
18302                    }
18303                    else => break,
18304                }
18305            }
18306        });
18307    });
18308}
18309
18310fn spawn_http2_server_accept_loop(
18311    shared: Arc<Mutex<crate::state::Http2SharedState>>,
18312    server_id: u64,
18313    listener: TcpListener,
18314) {
18315    thread::spawn(move || {
18316        let listener = listener;
18317        loop {
18318            let closed = shared
18319                .lock()
18320                .ok()
18321                .and_then(|state| {
18322                    state
18323                        .servers
18324                        .get(&server_id)
18325                        .map(|server| server.closed.load(Ordering::SeqCst))
18326                })
18327                .unwrap_or(true);
18328            if closed {
18329                break;
18330            }
18331            match listener.accept() {
18332                Ok((stream, _)) => {
18333                    let (command_tx, command_rx) = unbounded_channel();
18334                    let (guest_local_addr, secure, tls) = {
18335                        let state = shared.lock().expect("http2 shared state");
18336                        let server = state.servers.get(&server_id).expect("http2 server state");
18337                        (server.guest_local_addr, server.secure, server.tls.clone())
18338                    };
18339                    let (local_addr, remote_addr) = match (stream.local_addr(), stream.peer_addr())
18340                    {
18341                        (Ok(local_addr), Ok(remote_addr)) => (local_addr, remote_addr),
18342                        _ => continue,
18343                    };
18344                    let session_snapshot = Arc::new(Mutex::new(Http2SessionSnapshot {
18345                        encrypted: secure,
18346                        alpn_protocol: Some(if secure {
18347                            String::from("h2")
18348                        } else {
18349                            String::from("h2c")
18350                        }),
18351                        local_settings: BTreeMap::new(),
18352                        remote_settings: BTreeMap::new(),
18353                        state: http2_runtime_snapshot(),
18354                        socket: Http2SocketSnapshot {
18355                            local_address: Some(guest_local_addr.ip().to_string()),
18356                            local_port: Some(guest_local_addr.port()),
18357                            local_family: Some(socket_addr_family(&guest_local_addr).to_string()),
18358                            remote_address: Some(remote_addr.ip().to_string()),
18359                            remote_port: Some(remote_addr.port()),
18360                            remote_family: Some(socket_addr_family(&remote_addr).to_string()),
18361                            ..http2_socket_snapshot(local_addr, remote_addr)
18362                        },
18363                        ..Http2SessionSnapshot::default()
18364                    }));
18365                    let session_id = {
18366                        let mut state = shared.lock().expect("http2 shared state");
18367                        let session_id = next_http2_session_id(&mut state);
18368                        state
18369                            .sessions
18370                            .insert(session_id, ActiveHttp2Session { command_tx });
18371                        session_id
18372                    };
18373                    spawn_http2_server_session(
18374                        Arc::clone(&shared),
18375                        server_id,
18376                        session_id,
18377                        stream,
18378                        tls,
18379                        session_snapshot,
18380                        command_rx,
18381                    );
18382                }
18383                Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
18384                    thread::sleep(HTTP2_POLL_DELAY);
18385                }
18386                Err(error) => {
18387                    push_http2_server_event(
18388                        &shared,
18389                        server_id,
18390                        Http2BridgeEvent {
18391                            kind: String::from("serverStreamError"),
18392                            id: server_id,
18393                            data: Some(http2_error_payload(error.to_string())),
18394                            ..Http2BridgeEvent::default()
18395                        },
18396                    );
18397                    thread::sleep(HTTP2_POLL_DELAY);
18398                }
18399            }
18400        }
18401    });
18402}
18403
18404fn send_http2_command(
18405    session: &ActiveHttp2Session,
18406    command: impl FnOnce(Sender<Result<Value, String>>) -> Http2SessionCommand,
18407) -> Result<Value, SidecarError> {
18408    let (respond_to, response_rx) = mpsc::channel();
18409    session.command_tx.send(command(respond_to)).map_err(|_| {
18410        SidecarError::InvalidState(String::from("HTTP/2 session command channel closed"))
18411    })?;
18412    response_rx
18413        .recv_timeout(Duration::from_secs(30))
18414        .map_err(|_| {
18415            SidecarError::Execution(String::from("timed out waiting for HTTP/2 session command"))
18416        })?
18417        .map_err(SidecarError::Execution)
18418}
18419
18420fn parse_http2_server_listen_payload(
18421    request: &JavascriptSyncRpcRequest,
18422) -> Result<JavascriptHttp2ServerListenRequest, SidecarError> {
18423    let payload_json =
18424        javascript_sync_rpc_arg_str(&request.args, 0, "net.http2_server_listen payload")?;
18425    serde_json::from_str(payload_json).map_err(|error| {
18426        SidecarError::InvalidState(format!(
18427            "net.http2_server_listen payload must be valid JSON: {error}"
18428        ))
18429    })
18430}
18431
18432fn parse_http2_connect_payload(
18433    request: &JavascriptSyncRpcRequest,
18434) -> Result<JavascriptHttp2SessionConnectRequest, SidecarError> {
18435    let payload_json =
18436        javascript_sync_rpc_arg_str(&request.args, 0, "net.http2_session_connect payload")?;
18437    serde_json::from_str(payload_json).map_err(|error| {
18438        SidecarError::InvalidState(format!(
18439            "net.http2_session_connect payload must be valid JSON: {error}"
18440        ))
18441    })
18442}
18443
18444fn http2_session_for_id(
18445    process: &ActiveProcess,
18446    session_id: u64,
18447) -> Result<ActiveHttp2Session, SidecarError> {
18448    let shared = process
18449        .http2
18450        .shared
18451        .lock()
18452        .map_err(|_| SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned")))?;
18453    shared
18454        .sessions
18455        .get(&session_id)
18456        .cloned()
18457        .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 session {session_id}")))
18458}
18459
18460fn http2_stream_for_id(
18461    process: &ActiveProcess,
18462    stream_id: u64,
18463) -> Result<ActiveHttp2Stream, SidecarError> {
18464    let shared = process
18465        .http2
18466        .shared
18467        .lock()
18468        .map_err(|_| SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned")))?;
18469    shared
18470        .streams
18471        .get(&stream_id)
18472        .cloned()
18473        .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 stream {stream_id}")))
18474}
18475
18476fn service_javascript_http2_sync_rpc<B>(
18477    request: JavascriptHttp2SyncRpcServiceRequest<'_, B>,
18478) -> Result<Value, SidecarError>
18479where
18480    B: NativeSidecarBridge + Send + 'static,
18481    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
18482{
18483    let JavascriptHttp2SyncRpcServiceRequest {
18484        bridge,
18485        kernel,
18486        vm_id,
18487        dns,
18488        socket_paths,
18489        process,
18490        sync_request: request,
18491        resource_limits,
18492        network_counts,
18493    } = request;
18494    match request.method.as_str() {
18495        "net.http2_server_listen" => {
18496            check_network_resource_limit(
18497                resource_limits.max_sockets,
18498                network_counts.sockets,
18499                1,
18500                "socket",
18501            )?;
18502            let payload = parse_http2_server_listen_payload(request)?;
18503            let (family, bind_host, guest_host) =
18504                normalize_tcp_listen_host(payload.host.as_deref())?;
18505            let requested_port = payload.port.unwrap_or(0);
18506            bridge.require_network_access(
18507                vm_id,
18508                NetworkOperation::Listen,
18509                format_tcp_resource(bind_host, requested_port),
18510            )?;
18511            let port = allocate_guest_listen_port(
18512                requested_port,
18513                family,
18514                &socket_paths.used_tcp_guest_ports,
18515                socket_paths.listen_policy,
18516            )?;
18517            let mut listener =
18518                ActiveTcpListener::bind(bind_host, guest_host, port, payload.backlog)?;
18519            let guest_local_addr = listener.guest_local_addr();
18520            let closed = Arc::new(AtomicBool::new(false));
18521            {
18522                let mut state = process.http2.shared.lock().map_err(|_| {
18523                    SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
18524                })?;
18525                state.servers.insert(
18526                    payload.server_id,
18527                    ActiveHttp2Server {
18528                        actual_local_addr: listener.local_addr(),
18529                        guest_local_addr,
18530                        secure: payload.secure,
18531                        tls: payload.tls.clone().map(|mut tls| {
18532                            tls.is_server = payload.secure;
18533                            if payload.secure && tls.alpn_protocols.is_none() {
18534                                tls.alpn_protocols = Some(vec![String::from("h2")]);
18535                            }
18536                            tls
18537                        }),
18538                        closed: Arc::clone(&closed),
18539                    },
18540                );
18541                state.server_events.entry(payload.server_id).or_default();
18542            }
18543            spawn_http2_server_accept_loop(
18544                Arc::clone(&process.http2.shared),
18545                payload.server_id,
18546                listener.listener.take().ok_or_else(|| {
18547                    SidecarError::InvalidState(String::from(
18548                        "HTTP/2 listener missing host TCP socket",
18549                    ))
18550                })?,
18551            );
18552            javascript_net_json_string(
18553                json!({
18554                    "address": {
18555                        "address": guest_local_addr.ip().to_string(),
18556                        "family": socket_addr_family(&guest_local_addr),
18557                        "port": guest_local_addr.port(),
18558                    }
18559                }),
18560                "net.http2_server_listen",
18561            )
18562        }
18563        "net.http2_server_poll" => {
18564            let server_id =
18565                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_poll server id")?;
18566            let wait_ms = javascript_sync_rpc_arg_u64_optional(
18567                &request.args,
18568                1,
18569                "net.http2_server_poll wait ms",
18570            )?
18571            .unwrap_or_default();
18572            match wait_for_http2_event(&process.http2.shared, server_id, true, wait_ms) {
18573                Some(event) => http2_event_value(&event),
18574                None => Ok(Value::Null),
18575            }
18576        }
18577        "net.http2_server_wait" => {
18578            let server_id =
18579                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_wait server id")?;
18580            dispatch_http2_wait_loop(process, server_id, true)
18581        }
18582        "net.http2_server_close" => {
18583            let server_id =
18584                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_close server id")?;
18585            let server = {
18586                let mut state = process.http2.shared.lock().map_err(|_| {
18587                    SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
18588                })?;
18589                state.servers.remove(&server_id)
18590            }
18591            .ok_or_else(|| {
18592                SidecarError::InvalidState(format!("unknown HTTP/2 server {server_id}"))
18593            })?;
18594            server.closed.store(true, Ordering::SeqCst);
18595            push_http2_server_event(
18596                &process.http2.shared,
18597                server_id,
18598                Http2BridgeEvent {
18599                    kind: String::from("serverClose"),
18600                    id: server_id,
18601                    ..Http2BridgeEvent::default()
18602                },
18603            );
18604            Ok(Value::Null)
18605        }
18606        "net.http2_server_respond" => {
18607            let server_id = javascript_sync_rpc_arg_u64(
18608                &request.args,
18609                0,
18610                "net.http2_server_respond server id",
18611            )?;
18612            let request_id = javascript_sync_rpc_arg_u64(
18613                &request.args,
18614                1,
18615                "net.http2_server_respond request id",
18616            )?;
18617            let response_json =
18618                javascript_sync_rpc_arg_str(&request.args, 2, "net.http2_server_respond payload")?;
18619            ensure_vm_fetch_response_within_limit(response_json, "net.http2_server_respond")?;
18620            serde_json::from_str::<Value>(response_json).map_err(|error| {
18621                SidecarError::Execution(format!(
18622                    "net.http2_server_respond payload must be valid JSON: {error}"
18623                ))
18624            })?;
18625            let Some(pending) = process
18626                .pending_http_requests
18627                .get_mut(&(server_id, request_id))
18628            else {
18629                return Err(SidecarError::InvalidState(format!(
18630                    "unknown pending HTTP/2 request {request_id} for server {server_id}"
18631                )));
18632            };
18633            *pending = Some(response_json.to_owned());
18634            Ok(Value::Bool(true))
18635        }
18636        "net.http2_session_connect" => {
18637            check_network_resource_limit(
18638                resource_limits.max_sockets,
18639                network_counts.sockets,
18640                1,
18641                "socket",
18642            )?;
18643            check_network_resource_limit(
18644                resource_limits.max_connections,
18645                network_counts.connections,
18646                1,
18647                "connection",
18648            )?;
18649            let payload = parse_http2_connect_payload(request)?;
18650            let authority = payload.authority.clone().unwrap_or_else(|| {
18651                format!(
18652                    "{}://{}:{}",
18653                    payload.protocol.as_deref().unwrap_or("http"),
18654                    payload.host.as_deref().unwrap_or("localhost"),
18655                    payload.port.unwrap_or(80)
18656                )
18657            });
18658            let url = Url::parse(&authority).map_err(|error| {
18659                SidecarError::InvalidState(format!(
18660                    "invalid HTTP/2 authority {authority:?}: {error}"
18661                ))
18662            })?;
18663            let secure = url.scheme() == "https" || payload.protocol.as_deref() == Some("https:");
18664            let host = payload
18665                .host
18666                .as_deref()
18667                .or_else(|| url.host_str())
18668                .unwrap_or("localhost");
18669            let port = payload.port.or_else(|| url.port()).unwrap_or(80);
18670            bridge.require_network_access(
18671                vm_id,
18672                NetworkOperation::Http,
18673                format_tcp_resource(host, port),
18674            )?;
18675            let resolved = {
18676                let shared = process.http2.shared.lock().map_err(|_| {
18677                    SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
18678                })?;
18679                shared
18680                    .servers
18681                    .values()
18682                    .find(|server| {
18683                        is_loopback_request_host(host) && server.guest_local_addr.port() == port
18684                    })
18685                    .map(|server| ResolvedTcpConnectAddr {
18686                        actual_addr: server.actual_local_addr,
18687                        guest_remote_addr: server.guest_local_addr,
18688                        use_kernel_loopback: false,
18689                    })
18690            };
18691            let resolved = match resolved {
18692                Some(resolved) => resolved,
18693                None => {
18694                    resolve_tcp_connect_addr(bridge, kernel, vm_id, dns, host, port, socket_paths)?
18695                }
18696            };
18697            let (command_tx, command_rx) = unbounded_channel();
18698            let snapshot = Arc::new(Mutex::new(Http2SessionSnapshot {
18699                encrypted: secure,
18700                alpn_protocol: Some(String::from(if secure { "h2" } else { "h2c" })),
18701                local_settings: http2_settings_from_value(&payload.settings),
18702                remote_settings: BTreeMap::new(),
18703                state: http2_runtime_snapshot(),
18704                socket: Http2SocketSnapshot {
18705                    encrypted: secure,
18706                    remote_address: Some(resolved.guest_remote_addr.ip().to_string()),
18707                    remote_port: Some(resolved.guest_remote_addr.port()),
18708                    remote_family: Some(
18709                        socket_addr_family(&resolved.guest_remote_addr).to_string(),
18710                    ),
18711                    servername: if secure {
18712                        payload
18713                            .tls
18714                            .as_ref()
18715                            .and_then(|tls| tls.servername.clone())
18716                            .or_else(|| Some(host.to_string()))
18717                    } else {
18718                        None
18719                    },
18720                    alpn_protocol: Some(String::from(if secure { "h2" } else { "h2c" })),
18721                    ..Http2SocketSnapshot::default()
18722                },
18723                ..Http2SessionSnapshot::default()
18724            }));
18725            let session_id = {
18726                let mut state = process.http2.shared.lock().map_err(|_| {
18727                    SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
18728                })?;
18729                let session_id = next_http2_session_id(&mut state);
18730                state
18731                    .sessions
18732                    .insert(session_id, ActiveHttp2Session { command_tx });
18733                state.session_events.entry(session_id).or_default();
18734                session_id
18735            };
18736            spawn_http2_client_session(
18737                Arc::clone(&process.http2.shared),
18738                session_id,
18739                resolved.actual_addr,
18740                if secure {
18741                    Some(payload.tls.unwrap_or(JavascriptTlsBridgeOptions {
18742                        is_server: false,
18743                        servername: Some(host.to_string()),
18744                        alpn_protocols: Some(vec![String::from("h2")]),
18745                        ..JavascriptTlsBridgeOptions::default()
18746                    }))
18747                } else {
18748                    None
18749                },
18750                Arc::clone(&snapshot),
18751                command_rx,
18752            );
18753            let snapshot_json =
18754                http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())?;
18755            javascript_net_json_string(
18756                json!({
18757                    "sessionId": session_id,
18758                    "state": snapshot_json,
18759                }),
18760                "net.http2_session_connect",
18761            )
18762        }
18763        "net.http2_session_request" => {
18764            let session_id = javascript_sync_rpc_arg_u64(
18765                &request.args,
18766                0,
18767                "net.http2_session_request session id",
18768            )?;
18769            let headers_json =
18770                javascript_sync_rpc_arg_str(&request.args, 1, "net.http2_session_request headers")?;
18771            let options_json =
18772                javascript_sync_rpc_arg_str(&request.args, 2, "net.http2_session_request options")?;
18773            let session = http2_session_for_id(process, session_id)?;
18774            send_http2_command(&session, |respond_to| Http2SessionCommand::Request {
18775                headers_json: headers_json.to_owned(),
18776                options_json: options_json.to_owned(),
18777                respond_to,
18778            })
18779        }
18780        "net.http2_session_settings" => {
18781            let session_id = javascript_sync_rpc_arg_u64(
18782                &request.args,
18783                0,
18784                "net.http2_session_settings session id",
18785            )?;
18786            let settings_json = javascript_sync_rpc_arg_str(
18787                &request.args,
18788                1,
18789                "net.http2_session_settings settings",
18790            )?;
18791            let session = http2_session_for_id(process, session_id)?;
18792            send_http2_command(&session, |respond_to| Http2SessionCommand::Settings {
18793                settings_json: settings_json.to_owned(),
18794                respond_to,
18795            })
18796        }
18797        "net.http2_session_set_local_window_size" => {
18798            let session_id = javascript_sync_rpc_arg_u64(
18799                &request.args,
18800                0,
18801                "net.http2_session_set_local_window_size session id",
18802            )?;
18803            let window_size = javascript_sync_rpc_arg_u64(
18804                &request.args,
18805                1,
18806                "net.http2_session_set_local_window_size window size",
18807            )?;
18808            let session = http2_session_for_id(process, session_id)?;
18809            send_http2_command(&session, |respond_to| {
18810                Http2SessionCommand::SetLocalWindowSize {
18811                    size: window_size as u32,
18812                    respond_to,
18813                }
18814            })
18815        }
18816        "net.http2_session_goaway" => {
18817            let session_id = javascript_sync_rpc_arg_u64(
18818                &request.args,
18819                0,
18820                "net.http2_session_goaway session id",
18821            )?;
18822            let error_code = javascript_sync_rpc_arg_u64(
18823                &request.args,
18824                1,
18825                "net.http2_session_goaway error code",
18826            )?;
18827            let last_stream_id = javascript_sync_rpc_arg_u64(
18828                &request.args,
18829                2,
18830                "net.http2_session_goaway last stream id",
18831            )?;
18832            let opaque_data = request
18833                .args
18834                .get(3)
18835                .and_then(Value::as_str)
18836                .map(|value| {
18837                    base64::engine::general_purpose::STANDARD
18838                        .decode(value)
18839                        .map_err(|error| {
18840                            SidecarError::InvalidState(format!("invalid GOAWAY payload: {error}"))
18841                        })
18842                })
18843                .transpose()?;
18844            let session = http2_session_for_id(process, session_id)?;
18845            send_http2_command(&session, |respond_to| Http2SessionCommand::Goaway {
18846                error_code: error_code as u32,
18847                last_stream_id: last_stream_id as u32,
18848                opaque_data,
18849                respond_to,
18850            })
18851        }
18852        "net.http2_session_close" | "net.http2_session_destroy" => {
18853            let session_id = javascript_sync_rpc_arg_u64(
18854                &request.args,
18855                0,
18856                "net.http2_session_close session id",
18857            )?;
18858            let session = http2_session_for_id(process, session_id)?;
18859            send_http2_command(&session, |respond_to| Http2SessionCommand::Close {
18860                abrupt: request.method == "net.http2_session_destroy",
18861                respond_to,
18862            })
18863        }
18864        "net.http2_session_poll" => {
18865            let session_id =
18866                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_session_poll session id")?;
18867            let wait_ms = javascript_sync_rpc_arg_u64_optional(
18868                &request.args,
18869                1,
18870                "net.http2_session_poll wait ms",
18871            )?
18872            .unwrap_or_default();
18873            match wait_for_http2_event(&process.http2.shared, session_id, false, wait_ms) {
18874                Some(event) => http2_event_value(&event),
18875                None => Ok(Value::Null),
18876            }
18877        }
18878        "net.http2_session_wait" => {
18879            let session_id =
18880                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_session_wait session id")?;
18881            dispatch_http2_wait_loop(process, session_id, false)
18882        }
18883        "net.http2_stream_respond" => {
18884            let stream_id = javascript_sync_rpc_arg_u64(
18885                &request.args,
18886                0,
18887                "net.http2_stream_respond stream id",
18888            )?;
18889            let headers_json =
18890                javascript_sync_rpc_arg_str(&request.args, 1, "net.http2_stream_respond headers")?;
18891            let stream = http2_stream_for_id(process, stream_id)?;
18892            let session = http2_session_for_id(process, stream.session_id)?;
18893            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamRespond {
18894                stream_id,
18895                headers_json: headers_json.to_owned(),
18896                respond_to,
18897            })
18898        }
18899        "net.http2_stream_push_stream" => {
18900            let stream_id = javascript_sync_rpc_arg_u64(
18901                &request.args,
18902                0,
18903                "net.http2_stream_push_stream stream id",
18904            )?;
18905            let headers_json = javascript_sync_rpc_arg_str(
18906                &request.args,
18907                1,
18908                "net.http2_stream_push_stream headers",
18909            )?;
18910            let _options_json = javascript_sync_rpc_arg_str(
18911                &request.args,
18912                2,
18913                "net.http2_stream_push_stream options",
18914            )?;
18915            let stream = http2_stream_for_id(process, stream_id)?;
18916            let session = http2_session_for_id(process, stream.session_id)?;
18917            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamPush {
18918                stream_id,
18919                headers_json: headers_json.to_owned(),
18920                respond_to,
18921            })
18922        }
18923        "net.http2_stream_write" => {
18924            let stream_id =
18925                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_write stream id")?;
18926            let chunk =
18927                javascript_sync_rpc_base64_arg(&request.args, 1, "net.http2_stream_write data")?;
18928            let stream = http2_stream_for_id(process, stream_id)?;
18929            let session = http2_session_for_id(process, stream.session_id)?;
18930            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamWrite {
18931                stream_id,
18932                chunk,
18933                end_stream: false,
18934                respond_to,
18935            })
18936        }
18937        "net.http2_stream_end" => {
18938            let stream_id =
18939                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_end stream id")?;
18940            let chunk = request
18941                .args
18942                .get(1)
18943                .and_then(Value::as_str)
18944                .map(|value| {
18945                    base64::engine::general_purpose::STANDARD
18946                        .decode(value)
18947                        .map_err(|error| {
18948                            SidecarError::InvalidState(format!(
18949                                "invalid HTTP/2 stream payload: {error}"
18950                            ))
18951                        })
18952                })
18953                .transpose()?
18954                .unwrap_or_default();
18955            let stream = http2_stream_for_id(process, stream_id)?;
18956            let session = http2_session_for_id(process, stream.session_id)?;
18957            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamWrite {
18958                stream_id,
18959                chunk,
18960                end_stream: true,
18961                respond_to,
18962            })
18963        }
18964        "net.http2_stream_close" => {
18965            let stream_id =
18966                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_close stream id")?;
18967            let code = javascript_sync_rpc_arg_u64_optional(
18968                &request.args,
18969                1,
18970                "net.http2_stream_close error code",
18971            )?
18972            .map(|value| value as u32);
18973            let stream = http2_stream_for_id(process, stream_id)?;
18974            let session = http2_session_for_id(process, stream.session_id)?;
18975            send_http2_command(&session, |respond_to| Http2SessionCommand::StreamClose {
18976                stream_id,
18977                error_code: code,
18978                respond_to,
18979            })
18980        }
18981        "net.http2_stream_pause" => {
18982            let stream_id =
18983                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_pause stream id")?;
18984            let stream = http2_stream_for_id(process, stream_id)?;
18985            stream.paused.store(true, Ordering::SeqCst);
18986            Ok(Value::Null)
18987        }
18988        "net.http2_stream_resume" => {
18989            let stream_id =
18990                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_resume stream id")?;
18991            let stream = http2_stream_for_id(process, stream_id)?;
18992            stream.paused.store(false, Ordering::SeqCst);
18993            Ok(Value::Null)
18994        }
18995        "net.http2_stream_respond_with_file" => {
18996            let stream_id = javascript_sync_rpc_arg_u64(
18997                &request.args,
18998                0,
18999                "net.http2_stream_respond_with_file stream id",
19000            )?;
19001            let path = javascript_sync_rpc_arg_str(
19002                &request.args,
19003                1,
19004                "net.http2_stream_respond_with_file path",
19005            )?;
19006            let headers_json = javascript_sync_rpc_arg_str(
19007                &request.args,
19008                2,
19009                "net.http2_stream_respond_with_file headers",
19010            )?;
19011            let options_json = javascript_sync_rpc_arg_str(
19012                &request.args,
19013                3,
19014                "net.http2_stream_respond_with_file options",
19015            )?;
19016            let stream = http2_stream_for_id(process, stream_id)?;
19017            let session = http2_session_for_id(process, stream.session_id)?;
19018            let guest_path = resolve_http2_file_response_guest_path(process, path);
19019            let body = kernel.read_file(&guest_path).map_err(kernel_error)?;
19020            send_http2_command(&session, |respond_to| {
19021                Http2SessionCommand::StreamRespondWithFile {
19022                    stream_id,
19023                    body,
19024                    headers_json: headers_json.to_owned(),
19025                    options_json: options_json.to_owned(),
19026                    respond_to,
19027                }
19028            })
19029        }
19030        other => Err(SidecarError::InvalidState(format!(
19031            "unsupported JavaScript HTTP/2 sync RPC method {other}"
19032        ))),
19033    }
19034}
19035
19036const JAVASCRIPT_NET_POLL_MAX_WAIT: Duration = Duration::from_millis(50);
19037const EXITED_PROCESS_SNAPSHOT_RETENTION: Duration = Duration::from_secs(2);
19038
19039fn resolve_http2_file_response_guest_path(process: &ActiveProcess, path: &str) -> String {
19040    if Path::new(path).is_absolute() {
19041        normalize_path(path)
19042    } else {
19043        normalize_path(&format!("{}/{}", process.guest_cwd, path))
19044    }
19045}
19046
19047pub(crate) fn clamp_javascript_net_poll_wait(wait_ms: u64) -> Duration {
19048    // WASM net.poll runs on the sidecar's sync-RPC main thread. Guest-controlled waits
19049    // must stay bounded so one VM cannot stall dispose/shutdown or unrelated VM work.
19050    if wait_ms == 0 {
19051        Duration::ZERO
19052    } else {
19053        Duration::from_millis(wait_ms).min(JAVASCRIPT_NET_POLL_MAX_WAIT)
19054    }
19055}
19056
19057pub(crate) fn service_javascript_net_sync_rpc<B>(
19058    request: JavascriptNetSyncRpcServiceRequest<'_, B>,
19059) -> Result<Value, SidecarError>
19060where
19061    B: NativeSidecarBridge + Send + 'static,
19062    BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19063{
19064    let JavascriptNetSyncRpcServiceRequest {
19065        bridge,
19066        vm_id,
19067        dns,
19068        socket_paths,
19069        kernel,
19070        process,
19071        sync_request: request,
19072        resource_limits,
19073        network_counts,
19074    } = request;
19075    match request.method.as_str() {
19076        "net.http_listen" => {
19077            check_network_resource_limit(
19078                resource_limits.max_sockets,
19079                network_counts.sockets,
19080                1,
19081                "socket",
19082            )?;
19083            let payload_json =
19084                javascript_sync_rpc_arg_str(&request.args, 0, "net.http_listen payload")?;
19085            let payload: JavascriptHttpListenRequest =
19086                serde_json::from_str(payload_json).map_err(|error| {
19087                    SidecarError::InvalidState(format!(
19088                        "net.http_listen payload must be valid JSON: {error}"
19089                    ))
19090                })?;
19091            let (family, bind_host, guest_host) =
19092                normalize_tcp_listen_host(payload.hostname.as_deref())?;
19093            let requested_port = payload.port.unwrap_or(0);
19094            bridge.require_network_access(
19095                vm_id,
19096                NetworkOperation::Listen,
19097                format_tcp_resource(bind_host, requested_port),
19098            )?;
19099            let port = allocate_guest_listen_port(
19100                requested_port,
19101                family,
19102                &socket_paths.used_tcp_guest_ports,
19103                socket_paths.listen_policy,
19104            )?;
19105            let mut listener = ActiveTcpListener::bind(
19106                bind_host,
19107                guest_host,
19108                port,
19109                Some(DEFAULT_JAVASCRIPT_NET_BACKLOG),
19110            )?;
19111            let guest_local_addr = listener.guest_local_addr();
19112            process.http_servers.insert(
19113                payload.server_id,
19114                ActiveHttpServer {
19115                    listener: listener.listener.take().ok_or_else(|| {
19116                        SidecarError::InvalidState(String::from(
19117                            "HTTP listener missing host TCP socket",
19118                        ))
19119                    })?,
19120                    guest_local_addr,
19121                    next_request_id: 0,
19122                },
19123            );
19124            serde_json::to_string(&json!({
19125                "address": {
19126                    "address": guest_local_addr.ip().to_string(),
19127                    "family": socket_addr_family(&guest_local_addr),
19128                    "port": guest_local_addr.port(),
19129                }
19130            }))
19131            .map(Value::String)
19132            .map_err(|error| {
19133                SidecarError::Execution(format!("ERR_AGENT_OS_NODE_SYNC_RPC: {error}"))
19134            })
19135        }
19136        "net.http_close" => {
19137            let server_id =
19138                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_close server id")?;
19139            let server = process.http_servers.remove(&server_id).ok_or_else(|| {
19140                SidecarError::InvalidState(format!("unknown HTTP server {server_id}"))
19141            })?;
19142            drop(server.listener);
19143            process
19144                .pending_http_requests
19145                .retain(|(pending_server_id, _), _| *pending_server_id != server_id);
19146            Ok(Value::Null)
19147        }
19148        "net.http_wait" => {
19149            let server_id =
19150                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_wait server id")?;
19151            dispatch_http_wait_loop(process, server_id)
19152        }
19153        "net.http_respond" => {
19154            let server_id =
19155                javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_respond server id")?;
19156            let request_id =
19157                javascript_sync_rpc_arg_u64(&request.args, 1, "net.http_respond request id")?;
19158            let response_json =
19159                javascript_sync_rpc_arg_str(&request.args, 2, "net.http_respond payload")?;
19160            ensure_vm_fetch_response_within_limit(response_json, "net.http_respond")?;
19161            serde_json::from_str::<Value>(response_json).map_err(|error| {
19162                SidecarError::Execution(format!(
19163                    "net.http_respond payload must be valid JSON: {error}"
19164                ))
19165            })?;
19166            let Some(pending) = process
19167                .pending_http_requests
19168                .get_mut(&(server_id, request_id))
19169            else {
19170                return Err(SidecarError::InvalidState(format!(
19171                    "unknown pending HTTP request {request_id} for server {server_id}"
19172                )));
19173            };
19174            *pending = Some(response_json.to_owned());
19175            Ok(Value::Null)
19176        }
19177        "net.reserve_tcp_port" => {
19178            let payload = request
19179                .args
19180                .first()
19181                .cloned()
19182                .ok_or_else(|| {
19183                    SidecarError::InvalidState(String::from(
19184                        "net.reserve_tcp_port requires a request payload",
19185                    ))
19186                })
19187                .and_then(|value| {
19188                    serde_json::from_value::<JavascriptNetReserveTcpPortRequest>(value).map_err(
19189                        |error| {
19190                            SidecarError::InvalidState(format!(
19191                                "invalid net.reserve_tcp_port payload: {error}"
19192                            ))
19193                        },
19194                    )
19195                })?;
19196            let (family, _bind_host, guest_host) =
19197                normalize_tcp_listen_host(payload.host.as_deref())?;
19198            let requested_port = payload.port.unwrap_or(0);
19199            let port = allocate_guest_listen_port(
19200                requested_port,
19201                family,
19202                &socket_paths.used_tcp_guest_ports,
19203                socket_paths.listen_policy,
19204            )?;
19205            let reservation_id = process.allocate_tcp_port_reservation_id();
19206            process
19207                .tcp_port_reservations
19208                .insert(reservation_id.clone(), (family, port));
19209            Ok(json!({
19210                "reservationId": reservation_id,
19211                "localAddress": guest_host,
19212                "localPort": port,
19213                "family": match family {
19214                    JavascriptSocketFamily::Ipv4 => "IPv4",
19215                    JavascriptSocketFamily::Ipv6 => "IPv6",
19216                },
19217            }))
19218        }
19219        "net.release_tcp_port" => {
19220            let reservation_id =
19221                javascript_sync_rpc_arg_str(&request.args, 0, "net.release_tcp_port reservation")?;
19222            process.tcp_port_reservations.remove(reservation_id);
19223            Ok(Value::Null)
19224        }
19225        "net.connect" => {
19226            check_network_resource_limit(
19227                resource_limits.max_sockets,
19228                network_counts.sockets,
19229                1,
19230                "socket",
19231            )?;
19232            check_network_resource_limit(
19233                resource_limits.max_connections,
19234                network_counts.connections,
19235                1,
19236                "connection",
19237            )?;
19238            let payload = request
19239                .args
19240                .first()
19241                .cloned()
19242                .ok_or_else(|| {
19243                    SidecarError::InvalidState(String::from(
19244                        "net.connect requires a request payload",
19245                    ))
19246                })
19247                .and_then(|value| {
19248                    serde_json::from_value::<JavascriptNetConnectRequest>(value).map_err(|error| {
19249                        SidecarError::InvalidState(format!("invalid net.connect payload: {error}"))
19250                    })
19251                })?;
19252            if let Some(path) = payload.path.as_deref() {
19253                let guest_path = normalize_path(path);
19254                let host_path = resolve_guest_socket_host_path(socket_paths, &guest_path);
19255                let socket = ActiveUnixSocket::connect(&host_path, &guest_path)?;
19256                let socket_id = process.allocate_unix_socket_id();
19257                process.unix_sockets.insert(socket_id.clone(), socket);
19258                Ok(json!({
19259                    "socketId": socket_id,
19260                    "remotePath": guest_path,
19261                }))
19262            } else {
19263                let port = payload.port.ok_or_else(|| {
19264                    SidecarError::InvalidState(String::from(
19265                        "net.connect requires either a path or port",
19266                    ))
19267                })?;
19268                let host = payload.host.as_deref().unwrap_or("localhost");
19269                let local_reservation = payload.local_reservation.as_deref().and_then(|id| {
19270                    process
19271                        .tcp_port_reservations
19272                        .remove(id)
19273                        .map(|reservation| (id.to_owned(), reservation))
19274                });
19275                bridge.require_network_access(
19276                    vm_id,
19277                    NetworkOperation::Http,
19278                    format_tcp_resource(host, port),
19279                )?;
19280                let connect_result = ActiveTcpSocket::connect(ActiveTcpConnectRequest {
19281                    bridge,
19282                    kernel,
19283                    kernel_pid: process.kernel_pid,
19284                    vm_id,
19285                    dns,
19286                    host,
19287                    port,
19288                    local_address: payload.local_address.as_deref(),
19289                    local_port: payload.local_port,
19290                    local_reservation: local_reservation
19291                        .as_ref()
19292                        .map(|(_, reservation)| *reservation),
19293                    context: socket_paths,
19294                });
19295                if let Err(error) = connect_result {
19296                    if let Some((reservation_id, reservation)) = local_reservation {
19297                        process
19298                            .tcp_port_reservations
19299                            .insert(reservation_id, reservation);
19300                    }
19301                    return Err(error);
19302                }
19303                let socket = connect_result?;
19304                let socket_id = process.allocate_tcp_socket_id();
19305                let local_addr = socket.guest_local_addr;
19306                let remote_addr = socket.guest_remote_addr;
19307                process.tcp_sockets.insert(socket_id.clone(), socket);
19308                Ok(json!({
19309                    "socketId": socket_id,
19310                    "localAddress": local_addr.ip().to_string(),
19311                    "localPort": local_addr.port(),
19312                    "remoteAddress": remote_addr.ip().to_string(),
19313                    "remotePort": remote_addr.port(),
19314                    "remoteFamily": socket_addr_family(&remote_addr),
19315                }))
19316            }
19317        }
19318        "net.listen" => {
19319            check_network_resource_limit(
19320                resource_limits.max_sockets,
19321                network_counts.sockets,
19322                1,
19323                "socket",
19324            )?;
19325            let payload = request
19326                .args
19327                .first()
19328                .cloned()
19329                .ok_or_else(|| {
19330                    SidecarError::InvalidState(String::from(
19331                        "net.listen requires a request payload",
19332                    ))
19333                })
19334                .and_then(|value| match value {
19335                    Value::String(json) => {
19336                        serde_json::from_str::<JavascriptNetListenRequest>(&json).map_err(|error| {
19337                            SidecarError::InvalidState(format!(
19338                                "invalid net.listen payload: {error}"
19339                            ))
19340                        })
19341                    }
19342                    other => serde_json::from_value::<JavascriptNetListenRequest>(other).map_err(
19343                        |error| {
19344                            SidecarError::InvalidState(format!(
19345                                "invalid net.listen payload: {error}"
19346                            ))
19347                        },
19348                    ),
19349                })?;
19350            if let Some(path) = payload.path.as_deref() {
19351                let guest_path = normalize_path(path);
19352                if kernel.exists(&guest_path).map_err(kernel_error)? {
19353                    return Err(sidecar_net_error(std::io::Error::from_raw_os_error(
19354                        libc::EADDRINUSE,
19355                    )));
19356                }
19357
19358                let host_path = resolve_guest_socket_host_path(socket_paths, &guest_path);
19359                let on_host_mount =
19360                    host_mount_path_for_guest_path_from_mounts(&socket_paths.mounts, &guest_path)
19361                        .is_some();
19362                let listener = ActiveUnixListener::bind(&host_path, &guest_path, payload.backlog)?;
19363                if !on_host_mount {
19364                    ensure_kernel_parent_directories(kernel, &guest_path)?;
19365                    kernel
19366                        .write_file(&guest_path, Vec::new())
19367                        .map_err(kernel_error)?;
19368                }
19369                let listener_id = process.allocate_unix_listener_id();
19370                process.unix_listeners.insert(listener_id.clone(), listener);
19371                Ok(json!({
19372                    "serverId": listener_id,
19373                    "path": guest_path,
19374                }))
19375            } else {
19376                let (family, bind_host, guest_host) =
19377                    normalize_tcp_listen_host(payload.host.as_deref())?;
19378                let requested_port = payload.port.unwrap_or(0);
19379                bridge.require_network_access(
19380                    vm_id,
19381                    NetworkOperation::Listen,
19382                    format_tcp_resource(bind_host, requested_port),
19383                )?;
19384                let local_reservation = payload.local_reservation.as_deref().and_then(|id| {
19385                    process
19386                        .tcp_port_reservations
19387                        .remove(id)
19388                        .map(|reservation| (id.to_owned(), reservation))
19389                });
19390                let port = if requested_port != 0
19391                    && local_reservation
19392                        .as_ref()
19393                        .map(|(_, reservation)| *reservation)
19394                        == Some((family, requested_port))
19395                {
19396                    requested_port
19397                } else {
19398                    allocate_guest_listen_port(
19399                        requested_port,
19400                        family,
19401                        &socket_paths.used_tcp_guest_ports,
19402                        socket_paths.listen_policy,
19403                    )?
19404                };
19405                let listener_result = ActiveTcpListener::bind_kernel(
19406                    kernel,
19407                    process.kernel_pid,
19408                    guest_host,
19409                    port,
19410                    payload.backlog,
19411                );
19412                if let Err(error) = listener_result {
19413                    if let Some((reservation_id, reservation)) = local_reservation {
19414                        process
19415                            .tcp_port_reservations
19416                            .insert(reservation_id, reservation);
19417                    }
19418                    return Err(error);
19419                }
19420                let listener = listener_result?;
19421                let listener_id = process.allocate_tcp_listener_id();
19422                let local_addr = listener.guest_local_addr();
19423                process.tcp_listeners.insert(listener_id.clone(), listener);
19424                Ok(json!({
19425                    "serverId": listener_id,
19426                    "localAddress": local_addr.ip().to_string(),
19427                    "localPort": local_addr.port(),
19428                    "family": socket_addr_family(&local_addr),
19429                }))
19430            }
19431        }
19432        "net.poll" => {
19433            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.poll socket id")?;
19434            let wait_ms =
19435                javascript_sync_rpc_arg_u64_optional(&request.args, 1, "net.poll wait ms")?
19436                    .unwrap_or_default();
19437            let wait = clamp_javascript_net_poll_wait(wait_ms);
19438            let event = if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
19439                socket.poll(kernel, process.kernel_pid, wait)?
19440            } else if let Some(socket) = process.unix_sockets.get_mut(socket_id) {
19441                socket.poll(wait)?
19442            } else {
19443                return Err(SidecarError::InvalidState(format!(
19444                    "unknown net socket {socket_id}"
19445                )));
19446            };
19447
19448            match event {
19449                Some(JavascriptTcpSocketEvent::Data(chunk)) => Ok(json!({
19450                    "type": "data",
19451                    "data": javascript_sync_rpc_bytes_value(&chunk),
19452                })),
19453                Some(JavascriptTcpSocketEvent::End) => Ok(json!({
19454                    "type": "end",
19455                })),
19456                Some(JavascriptTcpSocketEvent::Error { code, message }) => Ok(json!({
19457                    "type": "error",
19458                    "code": code,
19459                    "message": message,
19460                })),
19461                Some(JavascriptTcpSocketEvent::Close { had_error }) => {
19462                    if let Some(socket) = process.tcp_sockets.remove(socket_id) {
19463                        if let Some(listener_id) = socket.listener_id.as_deref() {
19464                            if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
19465                                listener.release_connection(socket_id);
19466                            }
19467                        }
19468                    } else if let Some(socket) = process.unix_sockets.remove(socket_id) {
19469                        if let Some(listener_id) = socket.listener_id.as_deref() {
19470                            if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
19471                                listener.release_connection(socket_id);
19472                            }
19473                        }
19474                    }
19475                    Ok(json!({
19476                        "type": "close",
19477                        "hadError": had_error,
19478                    }))
19479                }
19480                None => Ok(Value::Null),
19481            }
19482        }
19483        "net.socket_wait_connect" => {
19484            let socket_id =
19485                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_wait_connect socket id")?;
19486            if let Some(socket) = process.tcp_sockets.get(socket_id) {
19487                javascript_net_json_string(socket.socket_info(), "net.socket_wait_connect")
19488            } else {
19489                let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
19490                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
19491                })?;
19492                javascript_net_json_string(socket.socket_info(), "net.socket_wait_connect")
19493            }
19494        }
19495        "net.socket_read" => {
19496            let socket_id =
19497                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_read socket id")?;
19498            if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
19499                javascript_net_read_value(socket.poll(
19500                    kernel,
19501                    process.kernel_pid,
19502                    Duration::ZERO,
19503                )?)
19504            } else {
19505                let socket = process.unix_sockets.get_mut(socket_id).ok_or_else(|| {
19506                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
19507                })?;
19508                javascript_net_read_value(socket.poll(Duration::ZERO)?)
19509            }
19510        }
19511        "net.socket_set_no_delay" => {
19512            let socket_id =
19513                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_set_no_delay socket id")?;
19514            let enable =
19515                javascript_sync_rpc_arg_bool(&request.args, 1, "net.socket_set_no_delay enabled")?;
19516            if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
19517                socket.set_no_delay(enable)?;
19518            } else if !process.unix_sockets.contains_key(socket_id) {
19519                return Err(SidecarError::InvalidState(format!(
19520                    "unknown net socket {socket_id}"
19521                )));
19522            }
19523            Ok(Value::Null)
19524        }
19525        "net.socket_set_keep_alive" => {
19526            let socket_id = javascript_sync_rpc_arg_str(
19527                &request.args,
19528                0,
19529                "net.socket_set_keep_alive socket id",
19530            )?;
19531            let enable = javascript_sync_rpc_arg_bool(
19532                &request.args,
19533                1,
19534                "net.socket_set_keep_alive enabled",
19535            )?;
19536            let initial_delay_secs = javascript_sync_rpc_arg_u64_optional(
19537                &request.args,
19538                2,
19539                "net.socket_set_keep_alive initial delay seconds",
19540            )?;
19541            if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
19542                socket.set_keep_alive(enable, initial_delay_secs)?;
19543            } else if !process.unix_sockets.contains_key(socket_id) {
19544                return Err(SidecarError::InvalidState(format!(
19545                    "unknown net socket {socket_id}"
19546                )));
19547            }
19548            Ok(Value::Null)
19549        }
19550        "net.socket_upgrade_tls" => {
19551            let socket_id =
19552                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_upgrade_tls socket id")?;
19553            let options_json =
19554                javascript_sync_rpc_arg_str(&request.args, 1, "net.socket_upgrade_tls options")?;
19555            let options: JavascriptTlsBridgeOptions =
19556                serde_json::from_str(options_json).map_err(|error| {
19557                    SidecarError::InvalidState(format!(
19558                        "net.socket_upgrade_tls options must be valid JSON: {error}"
19559                    ))
19560                })?;
19561            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
19562                SidecarError::InvalidState(format!(
19563                    "unknown TCP socket {socket_id} for TLS upgrade"
19564                ))
19565            })?;
19566            socket.upgrade_tls(vm_id, kernel, options)?;
19567            Ok(Value::Null)
19568        }
19569        "net.socket_get_tls_client_hello" => {
19570            let socket_id = javascript_sync_rpc_arg_str(
19571                &request.args,
19572                0,
19573                "net.socket_get_tls_client_hello socket id",
19574            )?;
19575            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
19576                SidecarError::InvalidState(format!(
19577                    "unknown TCP socket {socket_id} for TLS client hello query"
19578                ))
19579            })?;
19580            socket.tls_client_hello_json(vm_id, kernel)
19581        }
19582        "net.socket_tls_query" => {
19583            let socket_id =
19584                javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_tls_query socket id")?;
19585            let query =
19586                javascript_sync_rpc_arg_str(&request.args, 1, "net.socket_tls_query query")?;
19587            let detailed = request
19588                .args
19589                .get(2)
19590                .and_then(Value::as_bool)
19591                .unwrap_or(false);
19592            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
19593                SidecarError::InvalidState(format!("unknown TCP socket {socket_id} for TLS query"))
19594            })?;
19595            socket.tls_query(query, detailed)
19596        }
19597        "net.server_poll" => {
19598            let listener_id =
19599                javascript_sync_rpc_arg_str(&request.args, 0, "net.server_poll listener id")?;
19600            let wait_ms =
19601                javascript_sync_rpc_arg_u64_optional(&request.args, 1, "net.server_poll wait ms")?
19602                    .unwrap_or_default();
19603            let tcp_event = if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
19604                Some(listener.poll(kernel, process.kernel_pid, Duration::from_millis(wait_ms))?)
19605            } else {
19606                None
19607            };
19608
19609            if let Some(event) = tcp_event {
19610                return match event {
19611                    Some(JavascriptTcpListenerEvent::Connection(pending)) => {
19612                        let PendingTcpSocket {
19613                            stream,
19614                            kernel_socket_id,
19615                            preallocated,
19616                            guest_local_addr,
19617                            guest_remote_addr,
19618                        } = pending;
19619                        if !preallocated {
19620                            if let Err(error) = check_network_resource_limit(
19621                                resource_limits.max_sockets,
19622                                network_counts.sockets,
19623                                1,
19624                                "socket",
19625                            )
19626                            .and_then(|()| {
19627                                check_network_resource_limit(
19628                                    resource_limits.max_connections,
19629                                    network_counts.connections,
19630                                    1,
19631                                    "connection",
19632                                )
19633                            }) {
19634                                if let Some(stream) = stream {
19635                                    let _ = stream.shutdown(Shutdown::Both);
19636                                }
19637                                return Ok(json!({
19638                                    "type": "error",
19639                                    "code": "EAGAIN",
19640                                    "message": error.to_string(),
19641                                }));
19642                            }
19643                        }
19644                        let socket = if let Some(stream) = stream {
19645                            ActiveTcpSocket::from_stream(
19646                                stream,
19647                                Some(listener_id.to_string()),
19648                                guest_local_addr,
19649                                guest_remote_addr,
19650                            )?
19651                        } else {
19652                            ActiveTcpSocket::from_kernel(
19653                                kernel_socket_id.ok_or_else(|| {
19654                                    SidecarError::InvalidState(String::from(
19655                                        "kernel TCP accept missing socket id",
19656                                    ))
19657                                })?,
19658                                Some(listener_id.to_string()),
19659                                guest_local_addr,
19660                                guest_remote_addr,
19661                            )
19662                        };
19663                        let socket_id = process.allocate_tcp_socket_id();
19664                        if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
19665                            listener.register_connection(&socket_id);
19666                        }
19667                        process.tcp_sockets.insert(socket_id.clone(), socket);
19668                        Ok(json!({
19669                            "type": "connection",
19670                            "socketId": socket_id,
19671                            "localAddress": guest_local_addr.ip().to_string(),
19672                            "localPort": guest_local_addr.port(),
19673                            "remoteAddress": guest_remote_addr.ip().to_string(),
19674                            "remotePort": guest_remote_addr.port(),
19675                            "remoteFamily": socket_addr_family(&guest_remote_addr),
19676                        }))
19677                    }
19678                    Some(JavascriptTcpListenerEvent::Error { code, message }) => Ok(json!({
19679                        "type": "error",
19680                        "code": code,
19681                        "message": message,
19682                    })),
19683                    None => Ok(Value::Null),
19684                };
19685            }
19686
19687            let event = {
19688                let listener = process.unix_listeners.get_mut(listener_id).ok_or_else(|| {
19689                    SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
19690                })?;
19691                listener.poll(Duration::from_millis(wait_ms))?
19692            };
19693
19694            match event {
19695                Some(JavascriptUnixListenerEvent::Connection(pending)) => {
19696                    if let Err(error) = check_network_resource_limit(
19697                        resource_limits.max_sockets,
19698                        network_counts.sockets,
19699                        1,
19700                        "socket",
19701                    )
19702                    .and_then(|()| {
19703                        check_network_resource_limit(
19704                            resource_limits.max_connections,
19705                            network_counts.connections,
19706                            1,
19707                            "connection",
19708                        )
19709                    }) {
19710                        let _ = pending.stream.shutdown(Shutdown::Both);
19711                        return Ok(json!({
19712                            "type": "error",
19713                            "code": "EAGAIN",
19714                            "message": error.to_string(),
19715                        }));
19716                    }
19717                    let socket = ActiveUnixSocket::from_stream(
19718                        pending.stream,
19719                        Some(listener_id.to_string()),
19720                        pending.local_path.clone(),
19721                        pending.remote_path.clone(),
19722                    )?;
19723                    let socket_id = process.allocate_unix_socket_id();
19724                    if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
19725                        listener.register_connection(&socket_id);
19726                    }
19727                    process.unix_sockets.insert(socket_id.clone(), socket);
19728                    Ok(json!({
19729                        "type": "connection",
19730                        "socketId": socket_id,
19731                        "localPath": pending.local_path,
19732                        "remotePath": pending.remote_path,
19733                    }))
19734                }
19735                Some(JavascriptUnixListenerEvent::Error { code, message }) => Ok(json!({
19736                    "type": "error",
19737                    "code": code,
19738                    "message": message,
19739                })),
19740                None => Ok(Value::Null),
19741            }
19742        }
19743        "net.server_accept" => {
19744            let listener_id =
19745                javascript_sync_rpc_arg_str(&request.args, 0, "net.server_accept listener id")?;
19746            if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
19747                return match listener.poll(kernel, process.kernel_pid, Duration::ZERO)? {
19748                    Some(JavascriptTcpListenerEvent::Connection(pending)) => {
19749                        let PendingTcpSocket {
19750                            stream,
19751                            kernel_socket_id,
19752                            preallocated,
19753                            guest_local_addr,
19754                            guest_remote_addr,
19755                        } = pending;
19756                        if !preallocated {
19757                            check_network_resource_limit(
19758                                resource_limits.max_sockets,
19759                                network_counts.sockets,
19760                                1,
19761                                "socket",
19762                            )?;
19763                            check_network_resource_limit(
19764                                resource_limits.max_connections,
19765                                network_counts.connections,
19766                                1,
19767                                "connection",
19768                            )?;
19769                        }
19770                        let info = json!({
19771                            "localAddress": guest_local_addr.ip().to_string(),
19772                            "localPort": guest_local_addr.port(),
19773                            "localFamily": socket_addr_family(&guest_local_addr),
19774                            "remoteAddress": guest_remote_addr.ip().to_string(),
19775                            "remotePort": guest_remote_addr.port(),
19776                            "remoteFamily": socket_addr_family(&guest_remote_addr),
19777                        });
19778                        let socket = if let Some(stream) = stream {
19779                            ActiveTcpSocket::from_stream(
19780                                stream,
19781                                Some(listener_id.to_string()),
19782                                guest_local_addr,
19783                                guest_remote_addr,
19784                            )?
19785                        } else {
19786                            ActiveTcpSocket::from_kernel(
19787                                kernel_socket_id.ok_or_else(|| {
19788                                    SidecarError::InvalidState(String::from(
19789                                        "kernel TCP accept missing socket id",
19790                                    ))
19791                                })?,
19792                                Some(listener_id.to_string()),
19793                                guest_local_addr,
19794                                guest_remote_addr,
19795                            )
19796                        };
19797                        let socket_id = process.allocate_tcp_socket_id();
19798                        if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
19799                            listener.register_connection(&socket_id);
19800                        }
19801                        process.tcp_sockets.insert(socket_id.clone(), socket);
19802                        javascript_net_json_string(
19803                            json!({
19804                                "socketId": socket_id,
19805                                "info": info,
19806                            }),
19807                            "net.server_accept",
19808                        )
19809                    }
19810                    Some(JavascriptTcpListenerEvent::Error { code, message }) => {
19811                        let detail = code.unwrap_or_else(|| String::from("server accept"));
19812                        Err(SidecarError::Execution(format!("{detail}: {message}")))
19813                    }
19814                    None => Ok(javascript_net_timeout_value()),
19815                };
19816            }
19817
19818            let listener = process.unix_listeners.get_mut(listener_id).ok_or_else(|| {
19819                SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
19820            })?;
19821            match listener.poll(Duration::ZERO)? {
19822                Some(JavascriptUnixListenerEvent::Connection(pending)) => {
19823                    check_network_resource_limit(
19824                        resource_limits.max_sockets,
19825                        network_counts.sockets,
19826                        1,
19827                        "socket",
19828                    )?;
19829                    check_network_resource_limit(
19830                        resource_limits.max_connections,
19831                        network_counts.connections,
19832                        1,
19833                        "connection",
19834                    )?;
19835                    let info = json!({
19836                        "localPath": pending.local_path.clone(),
19837                        "remotePath": pending.remote_path.clone(),
19838                    });
19839                    let socket = ActiveUnixSocket::from_stream(
19840                        pending.stream,
19841                        Some(listener_id.to_string()),
19842                        pending.local_path,
19843                        pending.remote_path,
19844                    )?;
19845                    let socket_id = process.allocate_unix_socket_id();
19846                    if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
19847                        listener.register_connection(&socket_id);
19848                    }
19849                    process.unix_sockets.insert(socket_id.clone(), socket);
19850                    javascript_net_json_string(
19851                        json!({
19852                            "socketId": socket_id,
19853                            "info": info,
19854                        }),
19855                        "net.server_accept",
19856                    )
19857                }
19858                Some(JavascriptUnixListenerEvent::Error { code, message }) => {
19859                    let detail = code.unwrap_or_else(|| String::from("server accept"));
19860                    Err(SidecarError::Execution(format!("{detail}: {message}")))
19861                }
19862                None => Ok(javascript_net_timeout_value()),
19863            }
19864        }
19865        "net.server_connections" => {
19866            let listener_id = javascript_sync_rpc_arg_str(
19867                &request.args,
19868                0,
19869                "net.server_connections listener id",
19870            )?;
19871            if let Some(listener) = process.tcp_listeners.get(listener_id) {
19872                Ok(json!(listener.active_connection_count()))
19873            } else {
19874                let listener = process.unix_listeners.get(listener_id).ok_or_else(|| {
19875                    SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
19876                })?;
19877                Ok(json!(listener.active_connection_count()))
19878            }
19879        }
19880        "net.upgrade_socket_write" => {
19881            let socket_id = javascript_sync_rpc_arg_str(
19882                &request.args,
19883                0,
19884                "net.upgrade_socket_write socket id",
19885            )?;
19886            let chunk =
19887                javascript_sync_rpc_base64_arg(&request.args, 1, "net.upgrade_socket_write chunk")?;
19888            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
19889                SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
19890            })?;
19891            socket
19892                .write_all(kernel, process.kernel_pid, &chunk)
19893                .map(|written| json!(written))
19894        }
19895        "net.upgrade_socket_end" => {
19896            let socket_id =
19897                javascript_sync_rpc_arg_str(&request.args, 0, "net.upgrade_socket_end socket id")?;
19898            let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
19899                SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
19900            })?;
19901            socket.shutdown_write(kernel, process.kernel_pid)?;
19902            Ok(Value::Null)
19903        }
19904        "net.upgrade_socket_destroy" => {
19905            let socket_id = javascript_sync_rpc_arg_str(
19906                &request.args,
19907                0,
19908                "net.upgrade_socket_destroy socket id",
19909            )?;
19910            let socket = process.tcp_sockets.remove(socket_id).ok_or_else(|| {
19911                SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
19912            })?;
19913            if let Some(listener_id) = socket.listener_id.as_deref() {
19914                if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
19915                    listener.release_connection(socket_id);
19916                }
19917            }
19918            let _ = socket.close(kernel, process.kernel_pid);
19919            Ok(Value::Null)
19920        }
19921        "net.write" => {
19922            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.write socket id")?;
19923            let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "net.write chunk")?;
19924            if let Some(socket) = process.tcp_sockets.get(socket_id) {
19925                socket
19926                    .write_all(kernel, process.kernel_pid, &chunk)
19927                    .map(|written| json!(written))
19928            } else {
19929                let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
19930                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
19931                })?;
19932                socket.write_all(&chunk).map(|written| json!(written))
19933            }
19934        }
19935        "net.shutdown" => {
19936            let socket_id =
19937                javascript_sync_rpc_arg_str(&request.args, 0, "net.shutdown socket id")?;
19938            if let Some(socket) = process.tcp_sockets.get(socket_id) {
19939                socket.shutdown_write(kernel, process.kernel_pid)?;
19940            } else {
19941                let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
19942                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
19943                })?;
19944                socket.shutdown_write()?;
19945            }
19946            Ok(Value::Null)
19947        }
19948        "net.destroy" => {
19949            let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.destroy socket id")?;
19950            if let Some(socket) = process.tcp_sockets.remove(socket_id) {
19951                if let Some(listener_id) = socket.listener_id.as_deref() {
19952                    if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
19953                        listener.release_connection(socket_id);
19954                    }
19955                }
19956                let _ = socket.close(kernel, process.kernel_pid);
19957                Ok(Value::Null)
19958            } else {
19959                let socket = process.unix_sockets.remove(socket_id).ok_or_else(|| {
19960                    SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
19961                })?;
19962                if let Some(listener_id) = socket.listener_id.as_deref() {
19963                    if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
19964                        listener.release_connection(socket_id);
19965                    }
19966                }
19967                let _ = socket.close();
19968                Ok(Value::Null)
19969            }
19970        }
19971        "net.server_close" => {
19972            let listener_id =
19973                javascript_sync_rpc_arg_str(&request.args, 0, "net.server_close listener id")?;
19974            if let Some(listener) = process.tcp_listeners.remove(listener_id) {
19975                listener.close(kernel, process.kernel_pid)?;
19976                Ok(Value::Null)
19977            } else {
19978                let listener = process.unix_listeners.remove(listener_id).ok_or_else(|| {
19979                    SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
19980                })?;
19981                listener.close()?;
19982                Ok(Value::Null)
19983            }
19984        }
19985        "tls.get_ciphers" => javascript_net_json_string(
19986            Value::Array(
19987                tls_provider()
19988                    .cipher_suites
19989                    .iter()
19990                    .filter_map(|suite| {
19991                        suite
19992                            .suite()
19993                            .as_str()
19994                            .map(|value| Value::String(value.to_owned()))
19995                    })
19996                    .collect(),
19997            ),
19998            "tls.get_ciphers",
19999        ),
20000        _ => Err(SidecarError::InvalidState(format!(
20001            "unsupported JavaScript net sync RPC method {}",
20002            request.method
20003        ))),
20004    }
20005}
20006
20007fn signal_name_for_stream_event(signal: i32) -> Option<&'static str> {
20008    match signal {
20009        libc::SIGHUP => Some("SIGHUP"),
20010        libc::SIGINT => Some("SIGINT"),
20011        libc::SIGUSR1 => Some("SIGUSR1"),
20012        libc::SIGALRM => Some("SIGALRM"),
20013        libc::SIGCONT => Some("SIGCONT"),
20014        libc::SIGTERM => Some("SIGTERM"),
20015        libc::SIGCHLD => Some("SIGCHLD"),
20016        libc::SIGWINCH => Some("SIGWINCH"),
20017        _ => None,
20018    }
20019}
20020
20021pub(crate) fn canonical_signal_name(signal: i32) -> Option<&'static str> {
20022    match signal {
20023        1 => Some("SIGHUP"),
20024        2 => Some("SIGINT"),
20025        3 => Some("SIGQUIT"),
20026        4 => Some("SIGILL"),
20027        5 => Some("SIGTRAP"),
20028        6 => Some("SIGABRT"),
20029        7 => Some("SIGBUS"),
20030        8 => Some("SIGFPE"),
20031        9 => Some("SIGKILL"),
20032        10 => Some("SIGUSR1"),
20033        11 => Some("SIGSEGV"),
20034        12 => Some("SIGUSR2"),
20035        13 => Some("SIGPIPE"),
20036        14 => Some("SIGALRM"),
20037        15 => Some("SIGTERM"),
20038        17 => Some("SIGCHLD"),
20039        18 => Some("SIGCONT"),
20040        19 => Some("SIGSTOP"),
20041        20 => Some("SIGTSTP"),
20042        21 => Some("SIGTTIN"),
20043        22 => Some("SIGTTOU"),
20044        23 => Some("SIGURG"),
20045        24 => Some("SIGXCPU"),
20046        25 => Some("SIGXFSZ"),
20047        26 => Some("SIGVTALRM"),
20048        27 => Some("SIGPROF"),
20049        28 => Some("SIGWINCH"),
20050        29 => Some("SIGIO"),
20051        30 => Some("SIGPWR"),
20052        31 => Some("SIGSYS"),
20053        _ => None,
20054    }
20055}
20056
20057fn dispatch_v8_process_signal(process: &ActiveProcess, signal: i32) -> Result<bool, SidecarError> {
20058    let Some(signal_name) = signal_name_for_stream_event(signal) else {
20059        return Ok(false);
20060    };
20061    process.execution.send_javascript_stream_event(
20062        "signal",
20063        json!({
20064            "signal": signal_name,
20065            "number": signal,
20066            "action": "default",
20067        }),
20068    )?;
20069    Ok(true)
20070}
20071
20072fn dispatch_v8_session_signal_async(session: V8SessionHandle, signal: i32) {
20073    let Some(signal_name) = signal_name_for_stream_event(signal).map(str::to_owned) else {
20074        return;
20075    };
20076    thread::spawn(move || {
20077        thread::sleep(Duration::from_millis(1));
20078        let payload = v8_runtime::json_to_cbor_payload(&json!({
20079            "signal": signal_name,
20080            "number": signal,
20081            "action": "default",
20082        }))
20083        .unwrap_or_default();
20084        let _ = session.send_stream_event("signal", payload);
20085    });
20086}
20087
20088pub(crate) fn parse_signal(signal: &str) -> Result<i32, SidecarError> {
20089    let trimmed = signal.trim();
20090    if trimmed.is_empty() {
20091        return Err(SidecarError::InvalidState(String::from(
20092            "kill_process requires a non-empty signal",
20093        )));
20094    }
20095
20096    if let Ok(value) = trimmed.parse::<i32>() {
20097        return match value {
20098            0..=31 => Ok(value),
20099            _ => Err(SidecarError::InvalidState(format!(
20100                "unsupported kill_process signal {signal}"
20101            ))),
20102        };
20103    }
20104
20105    let upper = trimmed.to_ascii_uppercase();
20106    let normalized = upper.strip_prefix("SIG").unwrap_or(&upper);
20107
20108    signal_number_from_name(normalized).ok_or_else(|| {
20109        SidecarError::InvalidState(format!("unsupported kill_process signal {signal}"))
20110    })
20111}
20112
20113fn signal_number_from_name(signal: &str) -> Option<i32> {
20114    match signal {
20115        "0" => Some(0),
20116        "HUP" => Some(1),
20117        "INT" => Some(2),
20118        "QUIT" => Some(3),
20119        "ILL" => Some(4),
20120        "TRAP" => Some(5),
20121        "ABRT" | "IOT" => Some(6),
20122        "BUS" => Some(7),
20123        "FPE" => Some(8),
20124        "KILL" => Some(9),
20125        "USR1" => Some(10),
20126        "SEGV" => Some(11),
20127        "USR2" => Some(12),
20128        "PIPE" => Some(13),
20129        "ALRM" => Some(14),
20130        "TERM" => Some(15),
20131        "STKFLT" => Some(16),
20132        "CHLD" => Some(17),
20133        "CONT" => Some(18),
20134        "STOP" => Some(19),
20135        "TSTP" => Some(20),
20136        "TTIN" => Some(21),
20137        "TTOU" => Some(22),
20138        "URG" => Some(23),
20139        "XCPU" => Some(24),
20140        "XFSZ" => Some(25),
20141        "VTALRM" => Some(26),
20142        "PROF" => Some(27),
20143        "WINCH" => Some(28),
20144        "IO" | "POLL" => Some(29),
20145        "PWR" => Some(30),
20146        "SYS" => Some(31),
20147        _ => None,
20148    }
20149}
20150
20151pub(crate) fn runtime_child_is_alive(child_pid: u32) -> Result<bool, SidecarError> {
20152    Ok(runtime_child_exit_status(child_pid)?.is_none())
20153}
20154
20155fn runtime_child_exit_status(child_pid: u32) -> Result<Option<i32>, SidecarError> {
20156    if child_pid == 0 {
20157        return Ok(Some(0));
20158    }
20159
20160    let wait_flags = WaitPidFlag::WNOHANG
20161        | WaitPidFlag::WNOWAIT
20162        | WaitPidFlag::WEXITED
20163        | WaitPidFlag::WUNTRACED
20164        | WaitPidFlag::WCONTINUED;
20165    match wait_on_child(WaitId::Pid(Pid::from_raw(child_pid as i32)), wait_flags) {
20166        Ok(WaitStatus::StillAlive)
20167        | Ok(WaitStatus::Stopped(_, _))
20168        | Ok(WaitStatus::Continued(_)) => Ok(None),
20169        Ok(WaitStatus::Exited(_, status)) => Ok(Some(status)),
20170        Ok(WaitStatus::Signaled(_, signal, _)) => Ok(Some(128 + signal as i32)),
20171        #[cfg(any(target_os = "linux", target_os = "android"))]
20172        Ok(WaitStatus::PtraceEvent(_, _, _) | WaitStatus::PtraceSyscall(_)) => Ok(None),
20173        Err(nix::errno::Errno::ECHILD) => Ok(Some(0)),
20174        Err(error) => Err(SidecarError::Execution(format!(
20175            "failed to inspect guest runtime process {child_pid}: {error}"
20176        ))),
20177    }
20178}
20179
20180pub(crate) fn signal_runtime_process(child_pid: u32, signal: i32) -> Result<(), SidecarError> {
20181    if child_pid == 0 {
20182        return Ok(());
20183    }
20184
20185    if !runtime_child_is_alive(child_pid)? {
20186        return Ok(());
20187    }
20188
20189    if signal == 0 {
20190        return Ok(());
20191    }
20192
20193    let parsed = Signal::try_from(signal).map_err(|_| {
20194        SidecarError::InvalidState(format!("unsupported kill_process signal {signal}"))
20195    })?;
20196    let result = send_signal(Pid::from_raw(child_pid as i32), Some(parsed));
20197
20198    match result {
20199        Ok(()) => Ok(()),
20200        Err(nix::errno::Errno::ESRCH) => Ok(()),
20201        Err(error) => Err(SidecarError::Execution(format!(
20202            "failed to signal guest runtime process {child_pid}: {error}"
20203        ))),
20204    }
20205}
20206
20207pub(crate) fn error_code(error: &SidecarError) -> &'static str {
20208    match error {
20209        SidecarError::InvalidState(_) => "invalid_state",
20210        SidecarError::ProtocolVersionMismatch(_) => "protocol_version_mismatch",
20211        SidecarError::BridgeVersionMismatch(_) => "bridge_version_mismatch",
20212        SidecarError::Conflict(_) => "conflict",
20213        SidecarError::Unauthorized(_) => "unauthorized",
20214        SidecarError::Unsupported(_) => "unsupported",
20215        SidecarError::FrameTooLarge(_) => "frame_too_large",
20216        SidecarError::Kernel(_) => "kernel_error",
20217        SidecarError::Plugin(_) => "plugin_error",
20218        SidecarError::Execution(_) => "execution_error",
20219        SidecarError::Bridge(_) => "bridge_error",
20220        SidecarError::Io(_) => "io_error",
20221    }
20222}
20223
20224fn guest_errno_code(message: &str) -> Option<&str> {
20225    const TRUSTED_PREFIXES: &[&str] = &[
20226        "ERR_AGENT_OS_NODE_SYNC_RPC",
20227        "ERR_AGENT_OS_PYTHON_VFS_RPC",
20228        "ERR_AGENT_OS_BRIDGE",
20229    ];
20230
20231    let mut segments = message.split(':').map(str::trim);
20232    let first = segments.next()?;
20233    if is_guest_errno_segment(first) {
20234        return Some(first);
20235    }
20236
20237    if TRUSTED_PREFIXES.contains(&first) {
20238        let second = segments.next()?;
20239        if is_guest_errno_segment(second) {
20240            return Some(second);
20241        }
20242    }
20243
20244    None
20245}
20246
20247fn is_guest_errno_segment(segment: &str) -> bool {
20248    segment.len() >= 2
20249        && segment.starts_with('E')
20250        && !segment.starts_with("ERR_")
20251        && segment[1..]
20252            .bytes()
20253            .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_')
20254}
20255
20256pub(crate) fn javascript_sync_rpc_error_code(error: &SidecarError) -> String {
20257    let message = error.to_string();
20258    if let Some(code) = guest_errno_code(&message) {
20259        return code.to_owned();
20260    }
20261    if message.starts_with("ERR_NATIVE_BINARY_NOT_SUPPORTED:") {
20262        return String::from("ERR_NATIVE_BINARY_NOT_SUPPORTED");
20263    }
20264
20265    let lower = message.to_ascii_lowercase();
20266    if lower.contains("no such file or directory")
20267        || lower.contains("entry not found")
20268        || lower.contains("not found")
20269    {
20270        return String::from("ENOENT");
20271    }
20272    if lower.contains("permission denied") {
20273        return String::from("EACCES");
20274    }
20275    if lower.contains("already exists")
20276        || lower.contains("already registered")
20277        || lower.contains("file exists")
20278    {
20279        return String::from("EEXIST");
20280    }
20281    if lower.contains("invalid argument") {
20282        return String::from("EINVAL");
20283    }
20284
20285    String::from("ERR_AGENT_OS_NODE_SYNC_RPC")
20286}
20287
20288pub(crate) fn ignore_stale_javascript_sync_rpc_response(
20289    error: SidecarError,
20290) -> Result<(), SidecarError> {
20291    match error {
20292        SidecarError::Execution(message)
20293            if message.ends_with("is no longer pending")
20294                && message.starts_with("sync RPC request ") =>
20295        {
20296            Ok(())
20297        }
20298        SidecarError::Execution(message) => {
20299            let lower = message.to_ascii_lowercase();
20300            if lower.contains("sync rpc response")
20301                && (lower.contains("broken pipe") || lower.contains("channel closed unexpectedly"))
20302            {
20303                Ok(())
20304            } else {
20305                Err(SidecarError::Execution(message))
20306            }
20307        }
20308        other => Err(other),
20309    }
20310}
20311
20312#[cfg(test)]
20313mod error_code_tests {
20314    use super::{guest_errno_code, javascript_sync_rpc_error_code, SidecarError};
20315
20316    #[test]
20317    fn guest_errno_code_rejects_guest_controlled_errno_segments() {
20318        assert_eq!(guest_errno_code("user said 'EACCES: denied'"), None);
20319        assert_eq!(
20320            guest_errno_code("prefix: user said 'EPERM': more text"),
20321            None
20322        );
20323        assert_eq!(guest_errno_code("ERR_AGENT_OS_FAKE: EACCES: denied"), None);
20324    }
20325
20326    #[test]
20327    fn guest_errno_code_accepts_trusted_secure_exec_prefixes() {
20328        assert_eq!(
20329            guest_errno_code("ERR_AGENT_OS_NODE_SYNC_RPC: EACCES: permission denied on /foo"),
20330            Some("EACCES")
20331        );
20332        assert_eq!(
20333            guest_errno_code("ERR_AGENT_OS_PYTHON_VFS_RPC: ENOENT: missing file"),
20334            Some("ENOENT")
20335        );
20336        assert_eq!(guest_errno_code("EEXIST: already exists"), Some("EEXIST"));
20337    }
20338
20339    #[test]
20340    fn javascript_sync_rpc_error_code_ignores_spoofed_errnos() {
20341        let error = SidecarError::Execution(String::from("user said 'EACCES: denied'"));
20342        assert_eq!(
20343            javascript_sync_rpc_error_code(&error),
20344            "ERR_AGENT_OS_NODE_SYNC_RPC"
20345        );
20346    }
20347
20348    #[test]
20349    fn javascript_sync_rpc_error_code_preserves_real_sidecar_errnos() {
20350        let error = SidecarError::Execution(String::from(
20351            "ERR_AGENT_OS_NODE_SYNC_RPC: EACCES: permission denied on /foo",
20352        ));
20353        assert_eq!(javascript_sync_rpc_error_code(&error), "EACCES");
20354    }
20355
20356    #[test]
20357    fn javascript_sync_rpc_error_code_maps_file_exists_messages() {
20358        let error = SidecarError::Io(String::from(
20359            "failed to create mapped guest directory /.next/server: File exists (os error 17)",
20360        ));
20361        assert_eq!(javascript_sync_rpc_error_code(&error), "EEXIST");
20362    }
20363
20364    #[test]
20365    fn javascript_sync_rpc_error_code_preserves_native_binary_rejections() {
20366        let error = SidecarError::Execution(String::from(
20367            "ERR_NATIVE_BINARY_NOT_SUPPORTED: refused to execute native ELF guest binary at /tmp/fake-rg inside the VM",
20368        ));
20369        assert_eq!(
20370            javascript_sync_rpc_error_code(&error),
20371            "ERR_NATIVE_BINARY_NOT_SUPPORTED"
20372        );
20373    }
20374}
20375#[cfg(test)]
20376mod ssrf_egress_classifier_tests {
20377    // F-005/006/007 (sec-sidecar T1/T7/T11): the egress classifier must treat the
20378    // unspecified address (0.0.0.0 / ::), CGNAT (100.64.0.0/10), IPv6 spellings of
20379    // restricted IPv4 targets (::a.b.c.d), and reserved/multicast (240/4, 224/4) as
20380    // restricted. 0.0.0.0 routes to 127.0.0.1 on connect(), so leaving it
20381    // unclassified let a guest bypass the loopback port-ownership gate.
20382    //
20383    // These are bounded SAFEGUARD tests: they exercise the classifier and the DNS
20384    // egress filter directly (no network I/O, no Node), so they run fast and
20385    // deterministically. See FAILURES.md#F-005, #F-006, #F-007.
20386    use super::{
20387        filter_dns_safe_ip_addrs, is_loopback_ip, restricted_non_loopback_ip_range, SidecarError,
20388    };
20389    use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
20390
20391    fn assert_restricted(ip: IpAddr, expected_label: &str) {
20392        let classification = restricted_non_loopback_ip_range(ip);
20393        assert!(
20394            classification.is_some(),
20395            "{ip} must be classified as a restricted egress target"
20396        );
20397        let (_cidr, label) = classification.unwrap();
20398        assert_eq!(
20399            label, expected_label,
20400            "{ip} should be labelled {expected_label}, got {label}"
20401        );
20402    }
20403
20404    fn assert_dns_denied(ip: IpAddr, label: &str) {
20405        match filter_dns_safe_ip_addrs(vec![ip], "attacker.example") {
20406            Err(SidecarError::Execution(message)) => assert!(
20407                message.starts_with("EACCES:"),
20408                "{label}: egress filter must deny with EACCES, got: {message}"
20409            ),
20410            other => panic!("{label}: expected EACCES denial, got {other:?}"),
20411        }
20412    }
20413
20414    // F-005 (sec-sidecar T1).
20415    #[test]
20416    fn classifier_denies_unspecified_and_cgnat_targets() {
20417        // 0.0.0.0 (IPv4 unspecified) -> would route to host loopback.
20418        assert_restricted(IpAddr::V4(Ipv4Addr::UNSPECIFIED), "unspecified");
20419        // :: (IPv6 unspecified).
20420        assert_restricted(IpAddr::V6(Ipv6Addr::UNSPECIFIED), "unspecified");
20421
20422        // CGNAT 100.64.0.0/10 spans 100.64.x.x .. 100.127.x.x.
20423        assert_restricted(
20424            IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1)),
20425            "carrier-grade-nat",
20426        );
20427        assert_restricted(
20428            IpAddr::V4(Ipv4Addr::new(100, 127, 255, 254)),
20429            "carrier-grade-nat",
20430        );
20431
20432        // Guard against over-blocking: addresses just outside 100.64/10 stay allowed.
20433        assert!(
20434            restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(100, 63, 255, 255)))
20435                .is_none(),
20436            "100.63.255.255 is outside CGNAT and must remain allowed"
20437        );
20438        assert!(
20439            restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(100, 128, 0, 0))).is_none(),
20440            "100.128.0.0 is outside CGNAT and must remain allowed"
20441        );
20442
20443        // The DNS egress filter must also deny these via EACCES.
20444        assert_dns_denied(IpAddr::V4(Ipv4Addr::UNSPECIFIED), "0.0.0.0 (unspecified)");
20445        assert_dns_denied(IpAddr::V6(Ipv6Addr::UNSPECIFIED), ":: (unspecified)");
20446        assert_dns_denied(
20447            IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1)),
20448            "100.64.0.1 (CGNAT)",
20449        );
20450    }
20451
20452    // F-006 (sec-sidecar T7).
20453    #[test]
20454    fn classifier_denies_ipv6_spelled_metadata_addresses() {
20455        // The IPv4-mapped form (::ffff:169.254.169.254) was already handled; the
20456        // IPv4-compatible form (::169.254.169.254) is the gap this fixes.
20457        let mapped = "::ffff:169.254.169.254".parse::<Ipv6Addr>().unwrap();
20458        assert_restricted(IpAddr::V6(mapped), "link-local");
20459
20460        let compat = "::169.254.169.254".parse::<Ipv6Addr>().unwrap();
20461        assert_restricted(IpAddr::V6(compat), "link-local");
20462
20463        // Other IPv4-compatible private/CGNAT spellings must also be canonicalized.
20464        assert_restricted(
20465            IpAddr::V6("::10.0.0.1".parse::<Ipv6Addr>().unwrap()),
20466            "private",
20467        );
20468        assert_restricted(
20469            IpAddr::V6("::100.64.0.1".parse::<Ipv6Addr>().unwrap()),
20470            "carrier-grade-nat",
20471        );
20472
20473        // Guard against over-blocking: the IPv6 unspecified/loopback addresses
20474        // are not IPv4-compatible host targets, and a public IPv4-compatible
20475        // address must remain allowed.
20476        assert_eq!(
20477            restricted_non_loopback_ip_range(IpAddr::V6(Ipv6Addr::UNSPECIFIED)),
20478            Some(("::/128", "unspecified")),
20479            ":: must classify as unspecified, not via the IPv4-compat path"
20480        );
20481        assert!(
20482            restricted_non_loopback_ip_range(IpAddr::V6(Ipv6Addr::LOCALHOST)).is_none()
20483                || is_loopback_ip(IpAddr::V6(Ipv6Addr::LOCALHOST)),
20484            "::1 must not be classified as a restricted IPv4-compatible target"
20485        );
20486        assert!(
20487            restricted_non_loopback_ip_range(IpAddr::V6("::8.8.8.8".parse::<Ipv6Addr>().unwrap()))
20488                .is_none(),
20489            "::8.8.8.8 (public IPv4-compatible) must remain allowed"
20490        );
20491
20492        // The DNS egress filter must deny the IPv4-compat metadata spelling.
20493        assert_dns_denied(
20494            IpAddr::V6("::169.254.169.254".parse::<Ipv6Addr>().unwrap()),
20495            "::169.254.169.254 (IPv4-compat metadata)",
20496        );
20497    }
20498
20499    // F-007 (sec-sidecar T11).
20500    #[test]
20501    fn classifier_denies_reserved_and_multicast_targets() {
20502        // 224.0.0.0/4 (multicast) and 240.0.0.0/4 (reserved / future use) are not
20503        // legitimate unicast egress targets; a guest connect to them must be
20504        // classified as restricted and denied.
20505        assert_restricted(IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)), "multicast");
20506        assert_restricted(IpAddr::V4(Ipv4Addr::new(239, 255, 255, 255)), "multicast");
20507        assert_restricted(IpAddr::V4(Ipv4Addr::new(240, 0, 0, 1)), "reserved");
20508        // 255.255.255.255 (limited broadcast) falls in 240.0.0.0/4.
20509        assert_restricted(IpAddr::V4(Ipv4Addr::BROADCAST), "reserved");
20510
20511        // IPv4-compatible IPv6 spellings must canonicalize and be denied too.
20512        assert_restricted(
20513            IpAddr::V6("::224.0.0.1".parse::<Ipv6Addr>().unwrap()),
20514            "multicast",
20515        );
20516        assert_restricted(
20517            IpAddr::V6("::240.0.0.1".parse::<Ipv6Addr>().unwrap()),
20518            "reserved",
20519        );
20520
20521        // Guard against over-blocking: addresses just outside 224/4 stay allowed.
20522        assert!(
20523            restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(223, 255, 255, 255)))
20524                .is_none(),
20525            "223.255.255.255 is outside 224/4 and must remain allowed"
20526        );
20527
20528        // The DNS egress filter must also deny these via EACCES.
20529        assert_dns_denied(
20530            IpAddr::V4(Ipv4Addr::new(240, 0, 0, 1)),
20531            "240.0.0.1 (reserved)",
20532        );
20533        assert_dns_denied(
20534            IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)),
20535            "224.0.0.1 (multicast)",
20536        );
20537    }
20538}
20539
20540/// Adversarial coverage for the DNS-rebinding gap (VECTORS.md D.3) on the
20541/// Python/Pyodide `httpRequestSync` outbound HTTP path. The egress range guard
20542/// (`filter_dns_safe_ip_addrs`) runs at resolution time, but `ureq` performs its
20543/// own DNS resolution for the TCP/TLS connect, so a rebinding DNS server could
20544/// previously make the second lookup land on a private/link-local/metadata IP
20545/// the first check rejected. The fix pins `ureq`'s resolver to the vetted
20546/// address set; these tests prove the connect is pinned and refuses any other
20547/// host or an empty (fully-rejected) address set.
20548#[cfg(test)]
20549mod dns_rebinding_pin_tests {
20550    use super::{issue_outbound_http_request, split_netloc, JavascriptHttpRequestOptions};
20551    use std::collections::BTreeMap;
20552    use std::io::{Read, Write};
20553    use std::net::{IpAddr, Ipv4Addr, TcpListener};
20554    use std::thread;
20555    use url::Url;
20556
20557    fn empty_headers() -> super::HttpHeaderCollection {
20558        super::parse_http_header_collection(&BTreeMap::new(), "test headers")
20559            .expect("empty header collection")
20560    }
20561
20562    fn options() -> JavascriptHttpRequestOptions {
20563        JavascriptHttpRequestOptions {
20564            method: Some(String::from("GET")),
20565            headers: BTreeMap::new(),
20566            body: None,
20567            reject_unauthorized: None,
20568        }
20569    }
20570
20571    #[test]
20572    fn split_netloc_handles_hostnames_and_bracketed_ipv6() {
20573        assert_eq!(
20574            split_netloc("attacker.example:80"),
20575            Some(("attacker.example", 80))
20576        );
20577        assert_eq!(split_netloc("[::1]:443"), Some(("::1", 443)));
20578        assert_eq!(split_netloc("10.0.0.1:8080"), Some(("10.0.0.1", 8080)));
20579        assert_eq!(split_netloc("no-port"), None);
20580        assert_eq!(split_netloc("host:notaport"), None);
20581    }
20582
20583    /// A loopback HTTP server stands in for the egress-vetted target. The
20584    /// request URL uses a *different* hostname (`attacker.example`) whose real
20585    /// DNS would resolve elsewhere; pinning forces the connect onto the vetted
20586    /// IP only. If the resolver were unpinned, the request would fail to reach
20587    /// this server (and on a real host could land on a private/metadata IP).
20588    #[test]
20589    fn outbound_http_connect_is_pinned_to_vetted_ip() {
20590        let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).expect("bind loopback server");
20591        let port = listener.local_addr().expect("local addr").port();
20592        let server = thread::spawn(move || {
20593            let (mut stream, _) = listener.accept().expect("accept");
20594            let mut buf = [0u8; 1024];
20595            let _ = stream.read(&mut buf);
20596            stream
20597                .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhi")
20598                .expect("write response");
20599            let _ = stream.flush();
20600        });
20601
20602        let url = Url::parse(&format!("http://attacker.example:{port}/")).expect("url");
20603        let pinned = vec![IpAddr::V4(Ipv4Addr::LOCALHOST)];
20604        let result = issue_outbound_http_request(&url, &options(), &empty_headers(), &pinned)
20605            .expect("pinned request should reach the vetted loopback target");
20606        let payload = result.as_str().expect("string payload");
20607        assert!(
20608            payload.contains("\"status\":200"),
20609            "expected 200 from pinned target, got: {payload}"
20610        );
20611        server.join().expect("server thread");
20612    }
20613
20614    /// With no vetted address (every resolved IP was rejected by the range
20615    /// guard, or the literal IP was a blocked range), the pinned resolver must
20616    /// refuse rather than fall back to the host resolver.
20617    #[test]
20618    fn outbound_http_refuses_when_no_vetted_address() {
20619        let url = Url::parse("https://attacker.example/").expect("url");
20620        let error = issue_outbound_http_request(&url, &options(), &empty_headers(), &[])
20621            .expect_err("empty pinned set must be refused");
20622        let message = error.to_string();
20623        assert!(
20624            message.contains("EACCES") || message.contains("ERR_HTTP_REQUEST_FAILED"),
20625            "expected an egress refusal, got: {message}"
20626        );
20627    }
20628}