1use 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, JavascriptHttpLoopbackTarget, JavascriptSocketFamily,
38 JavascriptSocketPathContext, JavascriptTcpListenerEvent, JavascriptTcpSocketEvent,
39 JavascriptTlsBridgeOptions, JavascriptTlsClientHello, JavascriptTlsDataValue,
40 JavascriptTlsMaterial, JavascriptUdpFamily, JavascriptUdpSocketEvent,
41 JavascriptUnixListenerEvent, NetworkResourceCounts, PendingTcpSocket, PendingUnixSocket,
42 ProcNetEntry, ProcessEventEnvelope, ResolvedChildProcessExecution, ResolvedTcpConnectAddr,
43 SharedBridge, SharedSidecarRequestClient, SidecarKernel, SocketQueryKind, ToolExecution,
44 VmDnsConfig, VmListenPolicy, VmState, DEFAULT_JAVASCRIPT_NET_BACKLOG, EXECUTION_DRIVER_NAME,
45 EXECUTION_SANDBOX_ROOT_ENV, JAVASCRIPT_COMMAND, LOOPBACK_EXEMPT_PORTS_ENV,
46 MAPPED_HOST_FD_START, PYTHON_COMMAND, TOOL_DRIVER_NAME,
47 VM_LISTEN_ALLOW_PRIVILEGED_METADATA_KEY, WASM_COMMAND, 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::WaitStatus;
66#[cfg(not(target_os = "macos"))]
67use nix::sys::wait::{waitid as wait_on_child, Id as WaitId, WaitPidFlag};
68#[cfg(target_os = "macos")]
69use nix::sys::wait::{waitpid, WaitPidFlag};
70use nix::unistd::Pid;
71use openssl::bn::{BigNum, BigNumContext};
72use openssl::derive::Deriver;
73use openssl::dh::Dh;
74use openssl::ec::{EcGroup, EcKey, EcPoint, PointConversionForm};
75use openssl::hash::MessageDigest;
76use openssl::nid::Nid;
77use openssl::pkey::{Id as PKeyId, PKey, Params, Private, Public};
78use openssl::rand::rand_bytes;
79use openssl::rsa::{Padding, Rsa};
80use openssl::sign::{Signer, Verifier};
81use openssl::symm::{Cipher, Crypter, Mode};
82use pbkdf2::pbkdf2_hmac;
83use rusqlite::types::ValueRef as SqliteValueRef;
84use rusqlite::{
85 Connection as SqliteConnection, OpenFlags as SqliteOpenFlags, Statement as SqliteStatement,
86};
87use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
88use rustls::crypto::aws_lc_rs;
89use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName};
90use rustls::{
91 ClientConfig, ClientConnection, DigitallySignedStruct, RootCertStore, ServerConfig,
92 ServerConnection, SignatureScheme,
93};
94use scrypt::{scrypt, Params as ScryptParams};
95use secure_exec_bridge::LifecycleState;
96use secure_exec_execution::wasm::WasmExecutionError;
97use secure_exec_execution::{
98 javascript::handle_internal_bridge_call_from_host_context, v8_host::V8SessionHandle,
99 v8_runtime, CreateJavascriptContextRequest, CreatePythonContextRequest,
100 CreateWasmContextRequest, GuestModuleReader, GuestRuntimeConfig, JavascriptExecutionEvent,
101 JavascriptExecutionLimits, JavascriptSyncRpcRequest, ModuleFsReader,
102 NodeSignalDispositionAction, NodeSignalHandlerRegistration, PythonExecutionEvent,
103 PythonExecutionLimits, PythonVfsRpcMethod, PythonVfsRpcRequest, PythonVfsRpcResponsePayload,
104 StartJavascriptExecutionRequest, StartPythonExecutionRequest, StartWasmExecutionRequest,
105 WasmExecutionEvent, WasmExecutionLimits, WasmPermissionTier as ExecutionWasmPermissionTier,
106};
107use secure_exec_kernel::dns::{
108 DnsLookupPolicy, DnsRecordResolution, DnsResolutionSource as KernelDnsResolutionSource,
109};
110use secure_exec_kernel::kernel::{KernelProcessHandle, SpawnOptions, VirtualProcessOptions};
111use secure_exec_kernel::permissions::NetworkOperation;
112use secure_exec_kernel::poll::{PollEvents, PollFd, PollTargetEntry, POLLERR, POLLHUP, POLLIN};
113use secure_exec_kernel::process_table::{ProcessStatus, WaitPidFlags, SIGKILL, SIGTERM};
114use secure_exec_kernel::pty::LineDisciplineConfig;
115use secure_exec_kernel::resource_accounting::ResourceLimits;
116use secure_exec_kernel::root_fs::RootFilesystemMode;
117use secure_exec_kernel::socket_table::{
118 InetSocketAddress, SocketDomain, SocketId, SocketShutdown as KernelSocketShutdown, SocketSpec,
119 SocketState, SocketType,
120};
121use serde::{Deserialize, Serialize};
122use serde_json::{json, Map, Value};
123use sha1::Sha1;
124use sha2::{digest::Digest, Sha256, Sha512};
125use socket2::{SockRef, TcpKeepalive};
126use std::collections::VecDeque;
127use std::collections::{BTreeMap, BTreeSet};
128use std::fmt;
129use std::fs;
130use std::io::{Cursor, Read, Write};
131use std::net::{
132 IpAddr, Ipv4Addr, Ipv6Addr, Shutdown, SocketAddr, TcpListener, TcpStream, ToSocketAddrs,
133 UdpSocket,
134};
135use std::os::unix::fs::{MetadataExt, PermissionsExt};
136use std::os::unix::net::{SocketAddr as UnixSocketAddr, UnixListener, UnixStream};
137use std::path::{Path, PathBuf};
138use std::pin::Pin;
139use std::sync::atomic::{AtomicBool, Ordering};
140use std::sync::mpsc::{self, RecvTimeoutError, Sender};
141use std::sync::{Arc, Mutex, OnceLock, Weak};
142use std::thread;
143use std::time::{Duration, Instant};
144use tokio::io::{AsyncRead, AsyncWrite};
145use tokio::runtime::Builder as TokioRuntimeBuilder;
146use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver};
147use tokio_rustls::{TlsAcceptor, TlsConnector};
148use url::Url;
149
150const DEFAULT_KERNEL_STDIN_READ_MAX_BYTES: usize = 64 * 1024;
151const DEFAULT_KERNEL_STDIN_READ_TIMEOUT_MS: u64 = 100;
152const JAVASCRIPT_NET_TIMEOUT_SENTINEL: &str = "__secure_exec_net_timeout__";
153const PYTHON_PYODIDE_GUEST_ROOT: &str = "/__agentos_pyodide";
154const PYTHON_PYODIDE_CACHE_GUEST_ROOT: &str = "/__agentos_pyodide_cache";
155const TCP_SOCKET_POLL_TIMEOUT: Duration = Duration::from_millis(100);
156const TLS_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(5);
157const HTTP_LOOPBACK_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
158pub(crate) const MAX_PER_PROCESS_STATE_HANDLES: usize = 1024;
159const VM_FETCH_BUFFER_LIMIT_BYTES: usize = DEFAULT_MAX_FRAME_BYTES;
160const DEFAULT_SCRYPT_COST: u64 = 16_384;
161const DEFAULT_SCRYPT_BLOCK_SIZE: u32 = 8;
162const DEFAULT_SCRYPT_PARALLELIZATION: u32 = 1;
163const SQLITE_JS_SAFE_INTEGER_MAX: i64 = 9_007_199_254_740_991;
164const HTTP_LOOPBACK_REQUEST_TIMEOUT_MS_ENV: &str =
165 "SECURE_EXEC_TEST_HTTP_LOOPBACK_REQUEST_TIMEOUT_MS";
166
167trait Http2AsyncIo: AsyncRead + AsyncWrite + Unpin + Send {}
168
169impl<T> Http2AsyncIo for T where T: AsyncRead + AsyncWrite + Unpin + Send {}
170
171fn http_loopback_request_timeout() -> Duration {
172 static TIMEOUT: OnceLock<Duration> = OnceLock::new();
173 *TIMEOUT.get_or_init(|| {
174 std::env::var(HTTP_LOOPBACK_REQUEST_TIMEOUT_MS_ENV)
175 .ok()
176 .and_then(|value| value.parse::<u64>().ok())
177 .map(Duration::from_millis)
178 .unwrap_or(HTTP_LOOPBACK_REQUEST_TIMEOUT)
179 })
180}
181
182const DEFAULT_ALLOWED_NODE_BUILTINS: &[&str] = &[
183 "assert",
184 "buffer",
185 "console",
186 "child_process",
187 "crypto",
188 "dns",
189 "events",
190 "fs",
191 "http",
192 "http2",
193 "https",
194 "module",
195 "os",
196 "path",
197 "perf_hooks",
198 "querystring",
199 "sqlite",
200 "stream",
201 "string_decoder",
202 "timers",
203 "tls",
204 "tty",
205 "url",
206 "util",
207 "zlib",
208];
209
210#[derive(Debug, Clone, Copy, PartialEq, Eq)]
211enum JavascriptCryptoDigestAlgorithm {
212 Md5,
213 Sha1,
214 Sha256,
215 Sha512,
216}
217
218#[derive(Debug, Default, Deserialize)]
219#[serde(default, rename_all = "camelCase")]
220struct JavascriptScryptOptions {
221 #[serde(alias = "N")]
222 cost: Option<u64>,
223 #[serde(alias = "r")]
224 block_size: Option<u32>,
225 #[serde(alias = "p")]
226 parallelization: Option<u32>,
227}
228
229#[derive(Debug, Deserialize)]
230#[serde(rename_all = "camelCase")]
231struct JavascriptHttpListenRequest {
232 server_id: u64,
233 #[serde(default)]
234 port: Option<u16>,
235 #[serde(default)]
236 hostname: Option<String>,
237}
238
239#[derive(Debug, Default, Deserialize)]
240#[serde(default, rename_all = "camelCase")]
241struct JavascriptHttpRequestOptions {
242 method: Option<String>,
243 headers: BTreeMap<String, Value>,
244 body: Option<String>,
245 reject_unauthorized: Option<bool>,
246}
247
248#[derive(Debug, Default, Deserialize)]
249#[serde(default, rename_all = "camelCase")]
250struct JavascriptHttp2ServerListenRequest {
251 server_id: u64,
252 secure: bool,
253 port: Option<u16>,
254 host: Option<String>,
255 backlog: Option<u32>,
256 timeout: Option<u64>,
257 settings: BTreeMap<String, Value>,
258 tls: Option<JavascriptTlsBridgeOptions>,
259}
260
261#[derive(Debug, Default, Deserialize)]
262#[serde(default, rename_all = "camelCase")]
263struct JavascriptHttp2SessionConnectRequest {
264 authority: Option<String>,
265 protocol: Option<String>,
266 host: Option<String>,
267 port: Option<u16>,
268 settings: BTreeMap<String, Value>,
269 tls: Option<JavascriptTlsBridgeOptions>,
270}
271
272#[derive(Debug, Default, Deserialize)]
273#[serde(default, rename_all = "camelCase")]
274struct JavascriptHttp2RequestOptions {
275 end_stream: bool,
276}
277
278#[derive(Debug, Default, Deserialize)]
279#[serde(default, rename_all = "camelCase")]
280struct JavascriptHttp2FileResponseOptions {
281 offset: Option<u64>,
282 length: Option<i64>,
283}
284
285#[derive(Debug, Clone)]
286struct HttpHeaderCollection {
287 normalized: BTreeMap<String, Vec<String>>,
288 raw_pairs: Vec<(String, String)>,
289}
290
291#[derive(Debug)]
292struct InsecureTlsVerifier {
293 supported_schemes: Vec<SignatureScheme>,
294}
295
296impl ServerCertVerifier for InsecureTlsVerifier {
297 fn verify_server_cert(
298 &self,
299 _end_entity: &CertificateDer<'_>,
300 _intermediates: &[CertificateDer<'_>],
301 _server_name: &ServerName<'_>,
302 _ocsp_response: &[u8],
303 _now: rustls::pki_types::UnixTime,
304 ) -> Result<ServerCertVerified, rustls::Error> {
305 Ok(ServerCertVerified::assertion())
306 }
307
308 fn verify_tls12_signature(
309 &self,
310 _message: &[u8],
311 _cert: &CertificateDer<'_>,
312 _dss: &DigitallySignedStruct,
313 ) -> Result<HandshakeSignatureValid, rustls::Error> {
314 Ok(HandshakeSignatureValid::assertion())
315 }
316
317 fn verify_tls13_signature(
318 &self,
319 _message: &[u8],
320 _cert: &CertificateDer<'_>,
321 _dss: &DigitallySignedStruct,
322 ) -> Result<HandshakeSignatureValid, rustls::Error> {
323 Ok(HandshakeSignatureValid::assertion())
324 }
325
326 fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
327 self.supported_schemes.clone()
328 }
329}
330
331impl ActiveProcess {
332 pub(crate) fn new(
333 kernel_pid: u32,
334 kernel_handle: KernelProcessHandle,
335 runtime: GuestRuntimeKind,
336 execution: ActiveExecution,
337 ) -> Self {
338 Self {
339 kernel_pid,
340 kernel_handle,
341 kernel_stdin_writer_fd: None,
342 runtime,
343 detached: false,
344 execution,
345 guest_cwd: String::from("/"),
346 env: BTreeMap::new(),
347 host_cwd: PathBuf::from("/"),
348 mapped_host_fds: BTreeMap::new(),
349 next_mapped_host_fd: MAPPED_HOST_FD_START,
350 pending_execution_events: VecDeque::new(),
351 pending_self_signal_exit: None,
352 child_processes: BTreeMap::new(),
353 next_child_process_id: 0,
354 http_servers: BTreeMap::new(),
355 pending_http_requests: BTreeMap::new(),
356 http2: Default::default(),
357 tcp_listeners: BTreeMap::new(),
358 next_tcp_listener_id: 0,
359 tcp_sockets: BTreeMap::new(),
360 next_tcp_socket_id: 0,
361 tcp_port_reservations: BTreeMap::new(),
362 next_tcp_port_reservation_id: 0,
363 unix_listeners: BTreeMap::new(),
364 next_unix_listener_id: 0,
365 unix_sockets: BTreeMap::new(),
366 next_unix_socket_id: 0,
367 udp_sockets: BTreeMap::new(),
368 next_udp_socket_id: 0,
369 cipher_sessions: BTreeMap::new(),
370 next_cipher_session_id: 0,
371 diffie_hellman_sessions: BTreeMap::new(),
372 next_diffie_hellman_session_id: 0,
373 sqlite_databases: BTreeMap::new(),
374 next_sqlite_database_id: 0,
375 sqlite_statements: BTreeMap::new(),
376 next_sqlite_statement_id: 0,
377 module_resolution_cache: secure_exec_execution::LocalModuleResolutionCache::default(),
378 }
379 }
380
381 pub(crate) fn queue_pending_execution_event(
382 &mut self,
383 event: ActiveExecutionEvent,
384 ) -> Result<(), SidecarError> {
385 if self.pending_execution_events.len() >= MAX_PROCESS_EVENT_QUEUE {
386 return Err(process_event_queue_overflow_error());
387 }
388 self.pending_execution_events.push_back(event);
389 Ok(())
390 }
391
392 pub(crate) fn with_host_cwd(mut self, host_cwd: PathBuf) -> Self {
393 self.host_cwd = host_cwd;
394 self
395 }
396
397 pub(crate) fn with_guest_cwd(mut self, guest_cwd: String) -> Self {
398 self.guest_cwd = guest_cwd;
399 self
400 }
401
402 pub(crate) fn with_env(mut self, env: BTreeMap<String, String>) -> Self {
403 self.env = env;
404 self
405 }
406
407 pub(crate) fn with_kernel_stdin_writer_fd(mut self, fd: u32) -> Self {
408 self.kernel_stdin_writer_fd = Some(fd);
409 self
410 }
411
412 pub(crate) fn with_detached(mut self, detached: bool) -> Self {
413 self.detached = detached;
414 self
415 }
416
417 pub(crate) fn allocate_mapped_host_fd(&mut self, fd: ActiveMappedHostFd) -> u32 {
418 let handle = self.next_mapped_host_fd;
419 self.next_mapped_host_fd = self
420 .next_mapped_host_fd
421 .checked_add(1)
422 .unwrap_or(MAPPED_HOST_FD_START);
423 self.mapped_host_fds.insert(handle, fd);
424 handle
425 }
426
427 pub(crate) fn mapped_host_fd(&self, fd: u32) -> Option<&ActiveMappedHostFd> {
428 self.mapped_host_fds.get(&fd)
429 }
430
431 pub(crate) fn mapped_host_fd_mut(&mut self, fd: u32) -> Option<&mut ActiveMappedHostFd> {
432 self.mapped_host_fds.get_mut(&fd)
433 }
434
435 pub(crate) fn close_mapped_host_fd(&mut self, fd: u32) -> bool {
436 self.mapped_host_fds.remove(&fd).is_some()
437 }
438
439 pub(crate) fn allocate_child_process_id(&mut self) -> String {
440 self.next_child_process_id += 1;
441 format!("child-{}", self.next_child_process_id)
442 }
443
444 fn allocate_tcp_listener_id(&mut self) -> String {
445 self.next_tcp_listener_id += 1;
446 format!("listener-{}", self.next_tcp_listener_id)
447 }
448
449 fn allocate_tcp_socket_id(&mut self) -> String {
450 self.next_tcp_socket_id += 1;
451 format!("socket-{}", self.next_tcp_socket_id)
452 }
453
454 fn allocate_tcp_port_reservation_id(&mut self) -> String {
455 self.next_tcp_port_reservation_id += 1;
456 format!("tcp-port-reservation-{}", self.next_tcp_port_reservation_id)
457 }
458
459 fn allocate_unix_listener_id(&mut self) -> String {
460 self.next_unix_listener_id += 1;
461 format!("unix-listener-{}", self.next_unix_listener_id)
462 }
463
464 fn allocate_unix_socket_id(&mut self) -> String {
465 self.next_unix_socket_id += 1;
466 format!("unix-socket-{}", self.next_unix_socket_id)
467 }
468
469 fn allocate_udp_socket_id(&mut self) -> String {
470 self.next_udp_socket_id += 1;
471 format!("udp-socket-{}", self.next_udp_socket_id)
472 }
473
474 pub(crate) fn network_resource_counts(&self) -> NetworkResourceCounts {
475 let mut counts = NetworkResourceCounts {
476 sockets: self.http_servers.len()
477 + self.tcp_listeners.len()
478 + self.tcp_sockets.len()
479 + self.unix_listeners.len()
480 + self.unix_sockets.len()
481 + self.udp_sockets.len(),
482 connections: self.tcp_sockets.len() + self.unix_sockets.len(),
483 };
484 if let Ok(http2) = self.http2.shared.lock() {
485 counts.sockets += http2.servers.len() + http2.sessions.len();
486 counts.connections += http2.sessions.len();
487 }
488
489 for child in self.child_processes.values() {
490 let child_counts = child.network_resource_counts();
491 counts.sockets += child_counts.sockets;
492 counts.connections += child_counts.connections;
493 }
494
495 counts
496 }
497
498 fn sidecar_only_network_resource_counts(&self) -> NetworkResourceCounts {
499 let mut counts = NetworkResourceCounts {
500 sockets: self.http_servers.len()
501 + self
502 .tcp_listeners
503 .values()
504 .filter(|listener| listener.kernel_socket_id.is_none())
505 .count()
506 + self
507 .tcp_sockets
508 .values()
509 .filter(|socket| socket.kernel_socket_id.is_none())
510 .count()
511 + self.unix_listeners.len()
512 + self.unix_sockets.len()
513 + self
514 .udp_sockets
515 .values()
516 .filter(|socket| socket.kernel_socket_id.is_none())
517 .count(),
518 connections: self
519 .tcp_sockets
520 .values()
521 .filter(|socket| socket.kernel_socket_id.is_none())
522 .count()
523 + self.unix_sockets.len(),
524 };
525 if let Ok(http2) = self.http2.shared.lock() {
526 counts.sockets += http2.servers.len() + http2.sessions.len();
527 counts.connections += http2.sessions.len();
528 }
529
530 for child in self.child_processes.values() {
531 let child_counts = child.sidecar_only_network_resource_counts();
532 counts.sockets += child_counts.sockets;
533 counts.connections += child_counts.connections;
534 }
535
536 counts
537 }
538}
539
540fn poll_tool_process_event(
541 execution: &ToolExecution,
542) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
543 let event = execution
544 .pending_events
545 .lock()
546 .unwrap_or_else(|poisoned| poisoned.into_inner())
547 .pop_front();
548 if event.is_some() {
549 return Ok(event);
550 }
551 if execution.events_overflowed.load(Ordering::Relaxed) {
552 return Err(process_event_queue_overflow_error());
553 }
554 Ok(None)
555}
556
557fn descendant_pending_execution_event_capacity(
558 root: &ActiveProcess,
559 child_path: &[&str],
560) -> Option<usize> {
561 let mut child = root;
562 for child_process_id in child_path {
563 child = child.child_processes.get(*child_process_id)?;
564 }
565 Some(MAX_PROCESS_EVENT_QUEUE.saturating_sub(child.pending_execution_events.len()))
566}
567
568fn poll_child_execution_after_exit(
569 child: &mut ActiveProcess,
570 wait: Duration,
571) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
572 match child.execution.poll_event_blocking(wait) {
573 Ok(event) => Ok(event),
574 Err(SidecarError::Execution(message))
575 if child.runtime == GuestRuntimeKind::WebAssembly
576 && message == WasmExecutionError::EventChannelClosed.to_string() =>
577 {
578 Ok(None)
579 }
580 Err(error) => Err(error),
581 }
582}
583
584fn closed_javascript_event_channel(message: &str) -> bool {
585 message == "guest JavaScript event channel closed unexpectedly"
586}
587
588fn closed_python_event_channel(message: &str) -> bool {
589 message == "guest Python event channel closed unexpectedly"
590}
591
592fn closed_wasm_event_channel(message: &str) -> bool {
593 message == WasmExecutionError::EventChannelClosed.to_string()
594}
595
596fn missing_vm_error(vm_id: &str) -> SidecarError {
597 SidecarError::InvalidState(format!("VM {vm_id} is no longer active"))
598}
599
600fn missing_process_error(vm_id: &str, process_id: &str) -> SidecarError {
601 SidecarError::InvalidState(format!(
602 "VM {vm_id} no longer has active process {process_id}"
603 ))
604}
605
606fn is_broken_pipe_error(error: &SidecarError) -> bool {
607 matches!(error, SidecarError::Execution(message) if message.contains("Broken pipe") || message.contains("os error 32") || message.contains("EPIPE"))
608}
609
610fn javascript_child_process_gone_error(process_id: &str, child_path: &[&str]) -> SidecarError {
611 let child_label = if child_path.is_empty() {
612 process_id.to_owned()
613 } else {
614 format!("{process_id}/{}", child_path.join("/"))
615 };
616 SidecarError::Execution(format!(
617 "ECHILD: child_process {child_label} is no longer available"
618 ))
619}
620
621fn is_javascript_child_process_gone_error(error: &SidecarError) -> bool {
622 matches!(
623 error,
624 SidecarError::Execution(message) if guest_errno_code(message) == Some("ECHILD")
625 )
626}
627
628fn loopback_tls_transport_registry(
629) -> &'static Mutex<BTreeMap<String, Weak<crate::state::LoopbackTlsTransportPair>>> {
630 static REGISTRY: OnceLock<
631 Mutex<BTreeMap<String, Weak<crate::state::LoopbackTlsTransportPair>>>,
632 > = OnceLock::new();
633 REGISTRY.get_or_init(|| Mutex::new(BTreeMap::new()))
634}
635
636fn loopback_tls_transport_key(
637 vm_id: &str,
638 socket_id: SocketId,
639 peer_socket_id: SocketId,
640) -> String {
641 let (lower, higher) = if socket_id <= peer_socket_id {
642 (socket_id, peer_socket_id)
643 } else {
644 (peer_socket_id, socket_id)
645 };
646 format!("{vm_id}:{lower}:{higher}")
647}
648
649fn loopback_tls_endpoint(
650 vm_id: &str,
651 socket_id: SocketId,
652 peer_socket_id: SocketId,
653) -> Result<crate::state::LoopbackTlsEndpoint, SidecarError> {
654 let key = loopback_tls_transport_key(vm_id, socket_id, peer_socket_id);
655 let registry = loopback_tls_transport_registry();
656 let mut transports = registry.lock().map_err(|_| {
657 SidecarError::InvalidState(String::from(
658 "loopback TLS transport registry lock poisoned",
659 ))
660 })?;
661 transports.retain(|_, pair| pair.strong_count() > 0);
662 let pair = transports
663 .get(&key)
664 .and_then(Weak::upgrade)
665 .unwrap_or_else(|| {
666 let pair = Arc::new(crate::state::LoopbackTlsTransportPair {
667 state: Mutex::new(crate::state::LoopbackTlsTransportPairState::default()),
668 ready: std::sync::Condvar::new(),
669 });
670 transports.insert(key, Arc::downgrade(&pair));
671 pair
672 });
673 Ok(crate::state::LoopbackTlsEndpoint {
674 pair,
675 is_lower_socket: socket_id <= peer_socket_id,
676 })
677}
678
679impl crate::state::LoopbackTlsEndpoint {
680 fn shutdown_write(&self) -> Result<(), SidecarError> {
681 let mut state = self.pair.state.lock().map_err(|_| {
682 SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
683 })?;
684 if self.is_lower_socket {
685 state.lower_write_closed = true;
686 } else {
687 state.higher_write_closed = true;
688 }
689 self.pair.ready.notify_all();
690 Ok(())
691 }
692
693 fn close_endpoint(&self) -> Result<(), SidecarError> {
694 let mut state = self.pair.state.lock().map_err(|_| {
695 SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
696 })?;
697 if self.is_lower_socket {
698 state.lower_write_closed = true;
699 state.lower_closed = true;
700 } else {
701 state.higher_write_closed = true;
702 state.higher_closed = true;
703 }
704 self.pair.ready.notify_all();
705 Ok(())
706 }
707}
708
709fn parse_tls_client_hello_from_bytes(
710 buffer: &[u8],
711) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
712 if buffer.is_empty() {
713 return Ok(None);
714 }
715
716 let mut acceptor = rustls::server::Acceptor::default();
717 let mut cursor = Cursor::new(buffer);
718 acceptor.read_tls(&mut cursor).map_err(sidecar_net_error)?;
719 let Some(accepted) = acceptor.accept().map_err(|(error, _)| {
720 SidecarError::Execution(format!("failed to parse TLS client hello: {error}"))
721 })?
722 else {
723 return Ok(None);
724 };
725 let client_hello = accepted.client_hello();
726 let alpn_protocols = client_hello.alpn().map(|protocols| {
727 protocols
728 .filter_map(|protocol| String::from_utf8(protocol.to_vec()).ok())
729 .collect::<Vec<_>>()
730 });
731 Ok(Some(JavascriptTlsClientHello {
732 servername: client_hello.server_name().map(str::to_owned),
733 alpn_protocols,
734 }))
735}
736
737fn peek_loopback_tls_client_hello(
738 vm_id: &str,
739 socket_id: SocketId,
740 peer_socket_id: SocketId,
741) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
742 let key = loopback_tls_transport_key(vm_id, socket_id, peer_socket_id);
743 let registry = loopback_tls_transport_registry();
744 let pair = registry
745 .lock()
746 .map_err(|_| {
747 SidecarError::InvalidState(String::from(
748 "loopback TLS transport registry lock poisoned",
749 ))
750 })?
751 .get(&key)
752 .and_then(Weak::upgrade);
753 let Some(pair) = pair else {
754 return Ok(None);
755 };
756 let is_lower_socket = socket_id <= peer_socket_id;
757 let state = pair.state.lock().map_err(|_| {
758 SidecarError::InvalidState(String::from("loopback TLS transport lock poisoned"))
759 })?;
760 let buffered = if is_lower_socket {
761 state.higher_to_lower.iter().copied().collect::<Vec<_>>()
762 } else {
763 state.lower_to_higher.iter().copied().collect::<Vec<_>>()
764 };
765 drop(state);
766 parse_tls_client_hello_from_bytes(&buffered)
767}
768
769fn wait_for_loopback_peer_socket_id(
770 kernel: &SidecarKernel,
771 socket_id: SocketId,
772) -> Option<SocketId> {
773 for _ in 0..50 {
774 if let Some(peer_socket_id) = kernel
775 .socket_get(socket_id)
776 .and_then(|record| record.peer_socket_id())
777 {
778 return Some(peer_socket_id);
779 }
780 std::thread::sleep(Duration::from_millis(10));
781 }
782 None
783}
784
785impl Drop for crate::state::LoopbackTlsEndpoint {
786 fn drop(&mut self) {
787 let _ = self.close_endpoint();
788 }
789}
790
791impl Read for crate::state::LoopbackTlsEndpoint {
792 fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
793 let mut state = self
794 .pair
795 .state
796 .lock()
797 .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
798
799 loop {
800 let (peer_write_closed, peer_closed) = if self.is_lower_socket {
801 (state.higher_write_closed, state.higher_closed)
802 } else {
803 (state.lower_write_closed, state.lower_closed)
804 };
805
806 let incoming = if self.is_lower_socket {
807 &mut state.higher_to_lower
808 } else {
809 &mut state.lower_to_higher
810 };
811
812 if !incoming.is_empty() {
813 let mut count = 0;
814 while count < buffer.len() {
815 let Some(byte) = incoming.pop_front() else {
816 break;
817 };
818 buffer[count] = byte;
819 count += 1;
820 }
821 return Ok(count);
822 }
823
824 if peer_write_closed || peer_closed {
825 return Ok(0);
826 }
827
828 let (next_state, wait_result) = self
829 .pair
830 .ready
831 .wait_timeout(state, TCP_SOCKET_POLL_TIMEOUT)
832 .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
833 state = next_state;
834 if wait_result.timed_out() {
835 return Err(std::io::Error::new(
836 std::io::ErrorKind::WouldBlock,
837 "loopback TLS transport read timed out",
838 ));
839 }
840 }
841 }
842}
843
844impl Write for crate::state::LoopbackTlsEndpoint {
845 fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
846 let mut state = self
847 .pair
848 .state
849 .lock()
850 .map_err(|_| std::io::Error::other("loopback TLS transport lock poisoned"))?;
851
852 let peer_closed = if self.is_lower_socket {
853 state.higher_closed
854 } else {
855 state.lower_closed
856 };
857 let outgoing = if self.is_lower_socket {
858 &mut state.lower_to_higher
859 } else {
860 &mut state.higher_to_lower
861 };
862 if peer_closed {
863 return Err(std::io::Error::new(
864 std::io::ErrorKind::BrokenPipe,
865 "loopback TLS peer is closed",
866 ));
867 }
868
869 outgoing.extend(buffer.iter().copied());
870 self.pair.ready.notify_all();
871 Ok(buffer.len())
872 }
873
874 fn flush(&mut self) -> std::io::Result<()> {
875 Ok(())
876 }
877}
878
879struct ActiveTcpConnectRequest<'a, B> {
882 bridge: &'a SharedBridge<B>,
883 kernel: &'a mut SidecarKernel,
884 kernel_pid: u32,
885 vm_id: &'a str,
886 dns: &'a VmDnsConfig,
887 host: &'a str,
888 port: u16,
889 local_address: Option<&'a str>,
890 local_port: Option<u16>,
891 local_reservation: Option<(JavascriptSocketFamily, u16)>,
892 context: &'a JavascriptSocketPathContext,
893}
894
895struct ActiveUdpSendToRequest<'a, B> {
896 bridge: &'a SharedBridge<B>,
897 kernel: &'a mut SidecarKernel,
898 kernel_pid: u32,
899 vm_id: &'a str,
900 dns: &'a VmDnsConfig,
901 host: &'a str,
902 port: u16,
903 context: &'a JavascriptSocketPathContext,
904 contents: &'a [u8],
905}
906
907struct UdpRemoteAddrRequest<'a, B> {
908 bridge: &'a SharedBridge<B>,
909 kernel: &'a SidecarKernel,
910 vm_id: &'a str,
911 dns: &'a VmDnsConfig,
912 host: &'a str,
913 port: u16,
914 family: JavascriptUdpFamily,
915 context: &'a JavascriptSocketPathContext,
916}
917
918pub(crate) struct JavascriptSyncRpcServiceRequest<'a, B> {
919 pub(crate) bridge: &'a SharedBridge<B>,
920 pub(crate) vm_id: &'a str,
921 pub(crate) dns: &'a VmDnsConfig,
922 pub(crate) socket_paths: &'a JavascriptSocketPathContext,
923 pub(crate) kernel: &'a mut SidecarKernel,
924 pub(crate) process: &'a mut ActiveProcess,
925 pub(crate) sync_request: &'a JavascriptSyncRpcRequest,
926 pub(crate) resource_limits: &'a ResourceLimits,
927 pub(crate) network_counts: NetworkResourceCounts,
928}
929
930pub(crate) struct JavascriptNetSyncRpcServiceRequest<'a, B> {
931 pub(crate) bridge: &'a SharedBridge<B>,
932 pub(crate) vm_id: &'a str,
933 pub(crate) dns: &'a VmDnsConfig,
934 pub(crate) socket_paths: &'a JavascriptSocketPathContext,
935 pub(crate) kernel: &'a mut SidecarKernel,
936 pub(crate) process: &'a mut ActiveProcess,
937 pub(crate) sync_request: &'a JavascriptSyncRpcRequest,
938 pub(crate) resource_limits: &'a ResourceLimits,
939 pub(crate) network_counts: NetworkResourceCounts,
940}
941
942struct LoopbackHttpResponseWaitRequest<'a, B> {
943 bridge: &'a SharedBridge<B>,
944 vm_id: &'a str,
945 dns: &'a VmDnsConfig,
946 socket_paths: &'a JavascriptSocketPathContext,
947 kernel: &'a mut SidecarKernel,
948 process: &'a mut ActiveProcess,
949 resource_limits: &'a ResourceLimits,
950 request_key: (u64, u64),
951}
952
953pub(crate) struct LoopbackHttpDispatchRequest<'a, B> {
954 pub(crate) bridge: &'a SharedBridge<B>,
955 pub(crate) vm_id: &'a str,
956 pub(crate) dns: &'a VmDnsConfig,
957 pub(crate) socket_paths: &'a JavascriptSocketPathContext,
958 pub(crate) kernel: &'a mut SidecarKernel,
959 pub(crate) process: &'a mut ActiveProcess,
960 pub(crate) resource_limits: &'a ResourceLimits,
961 pub(crate) server_id: u64,
962 pub(crate) request_json: &'a str,
963}
964
965struct JavascriptDgramSyncRpcServiceRequest<'a, B> {
966 bridge: &'a SharedBridge<B>,
967 kernel: &'a mut SidecarKernel,
968 vm_id: &'a str,
969 dns: &'a VmDnsConfig,
970 socket_paths: &'a JavascriptSocketPathContext,
971 process: &'a mut ActiveProcess,
972 sync_request: &'a JavascriptSyncRpcRequest,
973 resource_limits: &'a ResourceLimits,
974 network_counts: NetworkResourceCounts,
975}
976
977struct JavascriptHttp2SyncRpcServiceRequest<'a, B> {
978 bridge: &'a SharedBridge<B>,
979 kernel: &'a mut SidecarKernel,
980 vm_id: &'a str,
981 dns: &'a VmDnsConfig,
982 socket_paths: &'a JavascriptSocketPathContext,
983 process: &'a mut ActiveProcess,
984 sync_request: &'a JavascriptSyncRpcRequest,
985 resource_limits: &'a ResourceLimits,
986 network_counts: NetworkResourceCounts,
987}
988
989impl ActiveTcpSocket {
990 fn connect<B>(request: ActiveTcpConnectRequest<'_, B>) -> Result<Self, SidecarError>
991 where
992 B: NativeSidecarBridge + Send + 'static,
993 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
994 {
995 let ActiveTcpConnectRequest {
996 bridge,
997 kernel,
998 kernel_pid,
999 vm_id,
1000 dns,
1001 host,
1002 port,
1003 local_address,
1004 local_port,
1005 local_reservation,
1006 context,
1007 } = request;
1008 let resolved = resolve_tcp_connect_addr(bridge, kernel, vm_id, dns, host, port, context)?;
1009 if resolved.use_kernel_loopback {
1010 let family = JavascriptSocketFamily::from_ip(resolved.guest_remote_addr.ip());
1011 let requested_local_port = local_port.unwrap_or(0);
1012 let local_port = if requested_local_port != 0
1013 && local_reservation == Some((family, requested_local_port))
1014 {
1015 requested_local_port
1016 } else {
1017 allocate_guest_listen_port(
1018 requested_local_port,
1019 family,
1020 &context.used_tcp_guest_ports,
1021 context.listen_policy,
1022 )?
1023 };
1024 let local_ip = match (family, local_address) {
1025 (JavascriptSocketFamily::Ipv4, Some("0.0.0.0")) => {
1026 IpAddr::V4(Ipv4Addr::UNSPECIFIED)
1027 }
1028 (JavascriptSocketFamily::Ipv4, Some("127.0.0.1") | Some("localhost") | None) => {
1029 IpAddr::V4(Ipv4Addr::LOCALHOST)
1030 }
1031 (JavascriptSocketFamily::Ipv6, Some("::")) => IpAddr::V6(Ipv6Addr::UNSPECIFIED),
1032 (JavascriptSocketFamily::Ipv6, Some("::1") | Some("localhost") | None) => {
1033 IpAddr::V6(Ipv6Addr::LOCALHOST)
1034 }
1035 (JavascriptSocketFamily::Ipv4, Some(other)) => {
1036 return Err(SidecarError::Execution(format!(
1037 "EACCES: TCP sockets must bind to loopback or unspecified addresses, got {other}"
1038 )));
1039 }
1040 (JavascriptSocketFamily::Ipv6, Some(other)) => {
1041 return Err(SidecarError::Execution(format!(
1042 "EACCES: TCP sockets must bind to loopback or unspecified addresses, got {other}"
1043 )));
1044 }
1045 };
1046 let local_addr = SocketAddr::new(local_ip, local_port);
1047 let spec = match family {
1048 JavascriptSocketFamily::Ipv4 => SocketSpec::tcp(),
1049 JavascriptSocketFamily::Ipv6 => {
1050 SocketSpec::new(SocketDomain::Inet6, SocketType::Stream)
1051 }
1052 };
1053 let socket_id = kernel
1054 .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
1055 .map_err(kernel_error)?;
1056 kernel
1057 .socket_bind_inet(
1058 EXECUTION_DRIVER_NAME,
1059 kernel_pid,
1060 socket_id,
1061 InetSocketAddress::new(local_ip.to_string(), local_port),
1062 )
1063 .map_err(kernel_error)?;
1064 kernel
1065 .socket_connect_inet_loopback(
1066 EXECUTION_DRIVER_NAME,
1067 kernel_pid,
1068 socket_id,
1069 InetSocketAddress::new(
1070 resolved.guest_remote_addr.ip().to_string(),
1071 resolved.guest_remote_addr.port(),
1072 ),
1073 )
1074 .map_err(kernel_error)?;
1075 return Ok(Self::from_kernel(
1076 socket_id,
1077 None,
1078 local_addr,
1079 resolved.guest_remote_addr,
1080 ));
1081 }
1082
1083 let stream = TcpStream::connect_timeout(&resolved.actual_addr, Duration::from_secs(30))
1084 .map_err(sidecar_net_error)?;
1085 let guest_local_addr = stream.local_addr().map_err(sidecar_net_error)?;
1086 Self::from_stream(stream, None, guest_local_addr, resolved.guest_remote_addr)
1087 }
1088
1089 fn from_stream(
1090 stream: TcpStream,
1091 listener_id: Option<String>,
1092 guest_local_addr: SocketAddr,
1093 guest_remote_addr: SocketAddr,
1094 ) -> Result<Self, SidecarError> {
1095 let read_stream = stream.try_clone().map_err(sidecar_net_error)?;
1096 read_stream
1097 .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
1098 .map_err(sidecar_net_error)?;
1099 let stream = Arc::new(Mutex::new(stream));
1100 let pending_read_stream = Arc::new(Mutex::new(Some(read_stream)));
1101 let (sender, events) = mpsc::channel();
1102 let tls_mode = Arc::new(AtomicBool::new(false));
1103 let tls_stream = Arc::new(Mutex::new(None));
1104 let tls_state = Arc::new(Mutex::new(None));
1105 let saw_local_shutdown = Arc::new(AtomicBool::new(false));
1106 let saw_remote_end = Arc::new(AtomicBool::new(false));
1107 let close_notified = Arc::new(AtomicBool::new(false));
1108
1109 Ok(Self {
1110 stream: Some(stream),
1111 pending_read_stream: Some(pending_read_stream),
1112 events: Some(events),
1113 event_sender: Some(sender),
1114 kernel_socket_id: None,
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,
1122 tls_stream,
1123 tls_state,
1124 saw_local_shutdown,
1125 saw_remote_end,
1126 close_notified,
1127 })
1128 }
1129
1130 fn from_kernel(
1131 socket_id: SocketId,
1132 listener_id: Option<String>,
1133 guest_local_addr: SocketAddr,
1134 guest_remote_addr: SocketAddr,
1135 ) -> Self {
1136 let (sender, events) = mpsc::channel();
1137 Self {
1138 stream: None,
1139 pending_read_stream: None,
1140 events: Some(events),
1141 event_sender: Some(sender),
1142 kernel_socket_id: Some(socket_id),
1143 no_delay: false,
1144 keep_alive: false,
1145 keep_alive_initial_delay_secs: None,
1146 guest_local_addr,
1147 guest_remote_addr,
1148 listener_id,
1149 tls_mode: Arc::new(AtomicBool::new(false)),
1150 tls_stream: Arc::new(Mutex::new(None)),
1151 tls_state: Arc::new(Mutex::new(None)),
1152 saw_local_shutdown: Arc::new(AtomicBool::new(false)),
1153 saw_remote_end: Arc::new(AtomicBool::new(false)),
1154 close_notified: Arc::new(AtomicBool::new(false)),
1155 }
1156 }
1157
1158 fn poll(
1159 &mut self,
1160 kernel: &mut SidecarKernel,
1161 kernel_pid: u32,
1162 wait: Duration,
1163 ) -> Result<Option<JavascriptTcpSocketEvent>, SidecarError> {
1164 if self.tls_mode.load(Ordering::SeqCst) {
1165 self.ensure_tcp_reader()?;
1166 return match self
1167 .events
1168 .as_ref()
1169 .ok_or_else(|| {
1170 SidecarError::InvalidState(String::from("TCP socket event channel missing"))
1171 })?
1172 .recv_timeout(wait)
1173 {
1174 Ok(event) => Ok(Some(event)),
1175 Err(RecvTimeoutError::Timeout) => Ok(None),
1176 Err(RecvTimeoutError::Disconnected) => Ok(None),
1177 };
1178 }
1179
1180 if let Some(socket_id) = self.kernel_socket_id {
1181 let result = kernel
1182 .poll_targets(
1183 EXECUTION_DRIVER_NAME,
1184 kernel_pid,
1185 vec![PollTargetEntry::socket(
1186 socket_id,
1187 POLLIN | POLLHUP | POLLERR,
1188 )],
1189 i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
1190 )
1191 .map_err(kernel_error)?;
1192 let revents = result
1193 .targets
1194 .first()
1195 .map(|entry| entry.revents)
1196 .unwrap_or_else(PollEvents::empty);
1197 if revents.is_empty() {
1198 return Ok(None);
1199 }
1200 if revents.intersects(POLLIN) {
1201 return match kernel.socket_read(
1202 EXECUTION_DRIVER_NAME,
1203 kernel_pid,
1204 socket_id,
1205 64 * 1024,
1206 ) {
1207 Ok(Some(bytes)) if !bytes.is_empty() => {
1208 Ok(Some(JavascriptTcpSocketEvent::Data(bytes)))
1209 }
1210 Ok(Some(_)) => Ok(Some(JavascriptTcpSocketEvent::Data(Vec::new()))),
1211 Ok(None) => Ok(Some(JavascriptTcpSocketEvent::End)),
1212 Err(error) if error.code() == "EAGAIN" => Ok(None),
1213 Err(error) => Ok(Some(JavascriptTcpSocketEvent::Error {
1214 code: Some(error.code().to_string()),
1215 message: error.to_string(),
1216 })),
1217 };
1218 }
1219 if revents.intersects(POLLHUP) {
1220 return Ok(Some(JavascriptTcpSocketEvent::End));
1221 }
1222 if revents.intersects(POLLERR) {
1223 return Ok(Some(JavascriptTcpSocketEvent::Error {
1224 code: Some(String::from("EPIPE")),
1225 message: String::from("kernel TCP socket reported POLLERR"),
1226 }));
1227 }
1228 return Ok(None);
1229 }
1230
1231 self.ensure_tcp_reader()?;
1232 match self
1233 .events
1234 .as_ref()
1235 .ok_or_else(|| {
1236 SidecarError::InvalidState(String::from("TCP socket event channel missing"))
1237 })?
1238 .recv_timeout(wait)
1239 {
1240 Ok(event) => Ok(Some(event)),
1241 Err(RecvTimeoutError::Timeout) => Ok(None),
1242 Err(RecvTimeoutError::Disconnected) => Ok(None),
1243 }
1244 }
1245
1246 fn ensure_tcp_reader(&self) -> Result<(), SidecarError> {
1247 if self.kernel_socket_id.is_some() {
1248 return Ok(());
1249 }
1250 if self.tls_mode.load(Ordering::SeqCst) {
1251 return Ok(());
1252 }
1253 let read_stream = self
1254 .pending_read_stream
1255 .as_ref()
1256 .ok_or_else(|| {
1257 SidecarError::InvalidState(String::from("TCP socket reader handle missing"))
1258 })?
1259 .lock()
1260 .map_err(|_| {
1261 SidecarError::InvalidState(String::from("TCP socket reader lock poisoned"))
1262 })?
1263 .take();
1264 if let Some(read_stream) = read_stream {
1265 spawn_tcp_socket_reader(
1266 read_stream,
1267 self.event_sender
1268 .as_ref()
1269 .ok_or_else(|| {
1270 SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1271 })?
1272 .clone(),
1273 Arc::clone(&self.tls_mode),
1274 Arc::clone(&self.saw_local_shutdown),
1275 Arc::clone(&self.saw_remote_end),
1276 Arc::clone(&self.close_notified),
1277 );
1278 }
1279 Ok(())
1280 }
1281
1282 fn socket_info(&self) -> Value {
1283 json!({
1284 "localAddress": self.guest_local_addr.ip().to_string(),
1285 "localPort": self.guest_local_addr.port(),
1286 "localFamily": socket_addr_family(&self.guest_local_addr),
1287 "remoteAddress": self.guest_remote_addr.ip().to_string(),
1288 "remotePort": self.guest_remote_addr.port(),
1289 "remoteFamily": socket_addr_family(&self.guest_remote_addr),
1290 })
1291 }
1292
1293 fn set_no_delay(&mut self, enable: bool) -> Result<(), SidecarError> {
1294 self.no_delay = enable;
1295 if self.kernel_socket_id.is_some() {
1296 return Ok(());
1297 }
1298 let stream = self
1299 .stream
1300 .as_ref()
1301 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1302 .lock()
1303 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1304 stream.set_nodelay(enable).map_err(sidecar_net_error)
1305 }
1306
1307 fn set_keep_alive(
1308 &mut self,
1309 enable: bool,
1310 initial_delay_secs: Option<u64>,
1311 ) -> Result<(), SidecarError> {
1312 self.keep_alive = enable;
1313 self.keep_alive_initial_delay_secs = initial_delay_secs;
1314 if self.kernel_socket_id.is_some() {
1315 return Ok(());
1316 }
1317 let stream = self
1318 .stream
1319 .as_ref()
1320 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1321 .lock()
1322 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1323 let socket = SockRef::from(&*stream);
1324 socket.set_keepalive(enable).map_err(sidecar_net_error)?;
1325 if enable {
1326 if let Some(delay_secs) = initial_delay_secs.filter(|delay_secs| *delay_secs > 0) {
1327 socket
1328 .set_tcp_keepalive(
1329 &TcpKeepalive::new().with_time(Duration::from_secs(delay_secs)),
1330 )
1331 .map_err(sidecar_net_error)?;
1332 }
1333 }
1334 Ok(())
1335 }
1336
1337 fn upgrade_tls(
1338 &self,
1339 vm_id: &str,
1340 kernel: &SidecarKernel,
1341 options: JavascriptTlsBridgeOptions,
1342 ) -> Result<(), SidecarError> {
1343 if self.tls_mode.load(Ordering::SeqCst) {
1344 return Ok(());
1345 }
1346
1347 let client_hello = if options.is_server {
1348 self.peek_tls_client_hello(vm_id, kernel)?
1349 } else {
1350 None
1351 };
1352
1353 let tls_stream = if let Some(socket_id) = self.kernel_socket_id {
1354 let peer_socket_id = wait_for_loopback_peer_socket_id(kernel, socket_id)
1355 .ok_or_else(|| {
1356 SidecarError::Execution(format!(
1357 "ERR_NOT_IMPLEMENTED: kernel-backed loopback socket {socket_id} has no peer for TLS upgrade"
1358 ))
1359 })?;
1360 let endpoint = loopback_tls_endpoint(vm_id, socket_id, peer_socket_id)?;
1361 if options.is_server {
1362 ActiveTlsStream::LoopbackServer(build_server_loopback_tls_stream(
1363 endpoint, &options,
1364 )?)
1365 } else {
1366 ActiveTlsStream::LoopbackClient(build_client_loopback_tls_stream(
1367 endpoint, &options,
1368 )?)
1369 }
1370 } else {
1371 self.pending_read_stream
1372 .as_ref()
1373 .ok_or_else(|| {
1374 SidecarError::InvalidState(String::from("TCP socket reader handle missing"))
1375 })?
1376 .lock()
1377 .map_err(|_| {
1378 SidecarError::InvalidState(String::from("TCP socket reader lock poisoned"))
1379 })?
1380 .take();
1381 let stream = self
1382 .stream
1383 .as_ref()
1384 .ok_or_else(|| {
1385 SidecarError::InvalidState(String::from("TCP socket stream missing"))
1386 })?
1387 .lock()
1388 .map_err(|_| {
1389 SidecarError::InvalidState(String::from("TCP socket lock poisoned"))
1390 })?;
1391 let cloned = stream.try_clone().map_err(sidecar_net_error)?;
1392 drop(stream);
1393
1394 if options.is_server {
1395 ActiveTlsStream::Server(build_server_tls_stream(cloned, &options)?)
1396 } else {
1397 ActiveTlsStream::Client(build_client_tls_stream(cloned, &options)?)
1398 }
1399 };
1400
1401 let tls_state = ActiveTlsState {
1402 client_hello,
1403 local_certificates: tls_local_certificates(&options)?,
1404 session_reused: false,
1405 };
1406
1407 self.tls_mode.store(true, Ordering::SeqCst);
1408 {
1409 let mut state = self
1410 .tls_state
1411 .lock()
1412 .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?;
1413 *state = Some(tls_state);
1414 }
1415 {
1416 let mut stream = self.tls_stream.lock().map_err(|_| {
1417 SidecarError::InvalidState(String::from("TLS stream lock poisoned"))
1418 })?;
1419 *stream = Some(tls_stream);
1420 }
1421
1422 spawn_tls_socket_reader(
1423 Arc::clone(&self.tls_stream),
1424 self.event_sender
1425 .as_ref()
1426 .ok_or_else(|| {
1427 SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1428 })?
1429 .clone(),
1430 Arc::clone(&self.saw_local_shutdown),
1431 Arc::clone(&self.saw_remote_end),
1432 Arc::clone(&self.close_notified),
1433 );
1434 Ok(())
1435 }
1436
1437 fn peek_tls_client_hello(
1438 &self,
1439 vm_id: &str,
1440 kernel: &SidecarKernel,
1441 ) -> Result<Option<JavascriptTlsClientHello>, SidecarError> {
1442 if let Some(socket_id) = self.kernel_socket_id {
1443 let Some(peer_socket_id) = kernel
1444 .socket_get(socket_id)
1445 .and_then(|record| record.peer_socket_id())
1446 else {
1447 return Ok(None);
1448 };
1449 return peek_loopback_tls_client_hello(vm_id, socket_id, peer_socket_id);
1450 }
1451
1452 let stream = self
1453 .stream
1454 .as_ref()
1455 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1456 .lock()
1457 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1458 let mut buffer = vec![0_u8; 16 * 1024];
1459 let bytes = match stream.peek(&mut buffer) {
1460 Ok(0) => return Ok(None),
1461 Ok(bytes) => bytes,
1462 Err(error)
1463 if matches!(
1464 error.kind(),
1465 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
1466 ) =>
1467 {
1468 return Ok(None);
1469 }
1470 Err(error) => return Err(sidecar_net_error(error)),
1471 };
1472 parse_tls_client_hello_from_bytes(&buffer[..bytes])
1473 }
1474
1475 fn tls_client_hello_json(
1476 &self,
1477 vm_id: &str,
1478 kernel: &SidecarKernel,
1479 ) -> Result<Value, SidecarError> {
1480 if let Some(client_hello) = self
1481 .tls_state
1482 .lock()
1483 .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?
1484 .as_ref()
1485 .and_then(|state| state.client_hello.clone())
1486 {
1487 return javascript_net_json_string(
1488 serde_json::to_value(client_hello).map_err(|error| {
1489 SidecarError::InvalidState(format!(
1490 "failed to serialize TLS client hello: {error}"
1491 ))
1492 })?,
1493 "net.socket_get_tls_client_hello",
1494 );
1495 }
1496
1497 javascript_net_json_string(
1498 serde_json::to_value(
1499 self.peek_tls_client_hello(vm_id, kernel)?
1500 .unwrap_or_default(),
1501 )
1502 .map_err(|error| {
1503 SidecarError::InvalidState(format!("failed to serialize TLS client hello: {error}"))
1504 })?,
1505 "net.socket_get_tls_client_hello",
1506 )
1507 }
1508
1509 fn tls_query(&self, query: &str, detailed: bool) -> Result<Value, SidecarError> {
1510 let state = self
1511 .tls_state
1512 .lock()
1513 .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?
1514 .clone();
1515 let mut tls_stream = self
1516 .tls_stream
1517 .lock()
1518 .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?;
1519 let Some(stream) = tls_stream.as_mut() else {
1520 return javascript_net_json_string(
1521 tls_bridge_undefined_value(),
1522 "net.socket_tls_query",
1523 );
1524 };
1525
1526 let payload = match query {
1527 "getSession" => tls_bridge_undefined_value(),
1528 "isSessionReused" => Value::Bool(
1529 state
1530 .as_ref()
1531 .is_some_and(|tls_state| tls_state.session_reused),
1532 ),
1533 "getPeerCertificate" => {
1534 let certificate = stream
1535 .peer_certificates()
1536 .and_then(|certificates| certificates.first())
1537 .map(|certificate| {
1538 tls_certificate_bridge_value(certificate.as_ref(), detailed)
1539 });
1540 certificate.unwrap_or_else(tls_bridge_undefined_value)
1541 }
1542 "getCertificate" => state
1543 .as_ref()
1544 .and_then(|tls_state| tls_state.local_certificates.first())
1545 .map(|certificate| tls_certificate_bridge_value(certificate, detailed))
1546 .unwrap_or_else(tls_bridge_undefined_value),
1547 "getProtocol" => stream
1548 .protocol_version()
1549 .map(tls_protocol_name)
1550 .map(Value::String)
1551 .unwrap_or(Value::Null),
1552 "getCipher" => stream
1553 .negotiated_cipher_suite()
1554 .map(tls_cipher_bridge_value)
1555 .unwrap_or_else(tls_bridge_undefined_value),
1556 other => {
1557 return Err(SidecarError::InvalidState(format!(
1558 "unsupported TLS query {other}"
1559 )));
1560 }
1561 };
1562 javascript_net_json_string(payload, "net.socket_tls_query")
1563 }
1564
1565 fn write_all(
1566 &self,
1567 kernel: &mut SidecarKernel,
1568 kernel_pid: u32,
1569 contents: &[u8],
1570 ) -> Result<usize, SidecarError> {
1571 if self.tls_mode.load(Ordering::SeqCst) {
1572 let mut tls_stream = self.tls_stream.lock().map_err(|_| {
1573 SidecarError::InvalidState(String::from("TLS stream lock poisoned"))
1574 })?;
1575 let stream = tls_stream.as_mut().ok_or_else(|| {
1576 SidecarError::InvalidState(String::from("TLS stream missing for upgraded socket"))
1577 })?;
1578 stream.write_all(contents)?;
1579 return Ok(contents.len());
1580 }
1581 if let Some(socket_id) = self.kernel_socket_id {
1582 return kernel
1583 .socket_write(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, contents)
1584 .map_err(kernel_error);
1585 }
1586
1587 let mut stream = self
1588 .stream
1589 .as_ref()
1590 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1591 .lock()
1592 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1593 stream.write_all(contents).map_err(sidecar_net_error)?;
1594 Ok(contents.len())
1595 }
1596
1597 fn shutdown_write(
1598 &self,
1599 kernel: &mut SidecarKernel,
1600 kernel_pid: u32,
1601 ) -> Result<(), SidecarError> {
1602 if self.tls_mode.load(Ordering::SeqCst) {
1603 if let Some(stream) = self
1604 .tls_stream
1605 .lock()
1606 .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?
1607 .as_mut()
1608 {
1609 let _ = stream.send_close_notify();
1610 let _ = stream.shutdown_write();
1611 }
1612 if self.kernel_socket_id.is_some() {
1613 self.saw_local_shutdown.store(true, Ordering::SeqCst);
1614 return Ok(());
1615 }
1616 }
1617 if let Some(socket_id) = self.kernel_socket_id {
1618 return kernel
1619 .socket_shutdown(
1620 EXECUTION_DRIVER_NAME,
1621 kernel_pid,
1622 socket_id,
1623 KernelSocketShutdown::Write,
1624 )
1625 .map_err(kernel_error);
1626 }
1627 let stream = self
1628 .stream
1629 .as_ref()
1630 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1631 .lock()
1632 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1633 self.saw_local_shutdown.store(true, Ordering::SeqCst);
1634 match stream.shutdown(Shutdown::Write) {
1635 Ok(()) => {}
1636 Err(error) if error.kind() == std::io::ErrorKind::NotConnected => {}
1637 Err(error) => return Err(sidecar_net_error(error)),
1638 }
1639 if self.saw_remote_end.load(Ordering::SeqCst)
1640 && !self.close_notified.swap(true, Ordering::SeqCst)
1641 {
1642 let _ = self
1643 .event_sender
1644 .as_ref()
1645 .ok_or_else(|| {
1646 SidecarError::InvalidState(String::from("TCP socket event sender missing"))
1647 })?
1648 .send(JavascriptTcpSocketEvent::Close { had_error: false });
1649 }
1650 Ok(())
1651 }
1652
1653 fn close(&self, kernel: &mut SidecarKernel, kernel_pid: u32) -> Result<(), SidecarError> {
1654 if self.tls_mode.load(Ordering::SeqCst) {
1655 if let Some(stream) = self
1656 .tls_stream
1657 .lock()
1658 .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?
1659 .as_mut()
1660 {
1661 let _ = stream.send_close_notify();
1662 let _ = stream.close();
1663 }
1664 if self.kernel_socket_id.is_some() {
1665 return Ok(());
1666 }
1667 }
1668 if let Some(socket_id) = self.kernel_socket_id {
1669 return kernel
1670 .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id)
1671 .map_err(kernel_error);
1672 }
1673 let stream = self
1674 .stream
1675 .as_ref()
1676 .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))?
1677 .lock()
1678 .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?;
1679 stream.shutdown(Shutdown::Both).map_err(sidecar_net_error)
1680 }
1681}
1682
1683impl ActiveTlsStream {
1684 fn write_all(&mut self, contents: &[u8]) -> Result<(), SidecarError> {
1685 match self {
1686 Self::Client(stream) => {
1687 stream.write_all(contents).map_err(sidecar_net_error)?;
1688 stream.flush().map_err(sidecar_net_error)
1689 }
1690 Self::Server(stream) => {
1691 stream.write_all(contents).map_err(sidecar_net_error)?;
1692 stream.flush().map_err(sidecar_net_error)
1693 }
1694 Self::LoopbackClient(stream) => {
1695 stream.write_all(contents).map_err(sidecar_net_error)?;
1696 stream.flush().map_err(sidecar_net_error)
1697 }
1698 Self::LoopbackServer(stream) => {
1699 stream.write_all(contents).map_err(sidecar_net_error)?;
1700 stream.flush().map_err(sidecar_net_error)
1701 }
1702 }
1703 }
1704
1705 fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
1706 match self {
1707 Self::Client(stream) => stream.read(buffer),
1708 Self::Server(stream) => stream.read(buffer),
1709 Self::LoopbackClient(stream) => stream.read(buffer),
1710 Self::LoopbackServer(stream) => stream.read(buffer),
1711 }
1712 }
1713
1714 fn send_close_notify(&mut self) -> Result<(), SidecarError> {
1715 match self {
1716 Self::Client(stream) => {
1717 stream.conn.send_close_notify();
1718 let _ = stream.conn.complete_io(&mut stream.sock);
1719 }
1720 Self::Server(stream) => {
1721 stream.conn.send_close_notify();
1722 let _ = stream.conn.complete_io(&mut stream.sock);
1723 }
1724 Self::LoopbackClient(stream) => {
1725 stream.conn.send_close_notify();
1726 let _ = stream.conn.complete_io(&mut stream.sock);
1727 }
1728 Self::LoopbackServer(stream) => {
1729 stream.conn.send_close_notify();
1730 let _ = stream.conn.complete_io(&mut stream.sock);
1731 }
1732 }
1733 Ok(())
1734 }
1735
1736 fn shutdown_write(&mut self) -> Result<(), SidecarError> {
1737 match self {
1738 Self::Client(stream) => stream
1739 .sock
1740 .shutdown(Shutdown::Write)
1741 .map_err(sidecar_net_error),
1742 Self::Server(stream) => stream
1743 .sock
1744 .shutdown(Shutdown::Write)
1745 .map_err(sidecar_net_error),
1746 Self::LoopbackClient(stream) => stream.sock.shutdown_write(),
1747 Self::LoopbackServer(stream) => stream.sock.shutdown_write(),
1748 }
1749 }
1750
1751 fn close(&mut self) -> Result<(), SidecarError> {
1752 match self {
1753 Self::Client(stream) => stream
1754 .sock
1755 .shutdown(Shutdown::Both)
1756 .map_err(sidecar_net_error),
1757 Self::Server(stream) => stream
1758 .sock
1759 .shutdown(Shutdown::Both)
1760 .map_err(sidecar_net_error),
1761 Self::LoopbackClient(stream) => stream.sock.close_endpoint(),
1762 Self::LoopbackServer(stream) => stream.sock.close_endpoint(),
1763 }
1764 }
1765
1766 fn peer_certificates(&self) -> Option<&[CertificateDer<'static>]> {
1767 match self {
1768 Self::Client(stream) => stream.conn.peer_certificates(),
1769 Self::Server(stream) => stream.conn.peer_certificates(),
1770 Self::LoopbackClient(stream) => stream.conn.peer_certificates(),
1771 Self::LoopbackServer(stream) => stream.conn.peer_certificates(),
1772 }
1773 }
1774
1775 fn negotiated_cipher_suite(&self) -> Option<rustls::SupportedCipherSuite> {
1776 match self {
1777 Self::Client(stream) => stream.conn.negotiated_cipher_suite(),
1778 Self::Server(stream) => stream.conn.negotiated_cipher_suite(),
1779 Self::LoopbackClient(stream) => stream.conn.negotiated_cipher_suite(),
1780 Self::LoopbackServer(stream) => stream.conn.negotiated_cipher_suite(),
1781 }
1782 }
1783
1784 fn protocol_version(&self) -> Option<rustls::ProtocolVersion> {
1785 match self {
1786 Self::Client(stream) => stream.conn.protocol_version(),
1787 Self::Server(stream) => stream.conn.protocol_version(),
1788 Self::LoopbackClient(stream) => stream.conn.protocol_version(),
1789 Self::LoopbackServer(stream) => stream.conn.protocol_version(),
1790 }
1791 }
1792}
1793
1794impl ActiveUnixSocket {
1799 fn connect(host_path: &Path, guest_path: &str) -> Result<Self, SidecarError> {
1800 let stream = UnixStream::connect(host_path).map_err(sidecar_net_error)?;
1801 Self::from_stream(stream, None, None, Some(guest_path.to_owned()))
1802 }
1803
1804 fn from_stream(
1805 stream: UnixStream,
1806 listener_id: Option<String>,
1807 local_path: Option<String>,
1808 remote_path: Option<String>,
1809 ) -> Result<Self, SidecarError> {
1810 let read_stream = stream.try_clone().map_err(sidecar_net_error)?;
1811 let stream = Arc::new(Mutex::new(stream));
1812 let (sender, events) = mpsc::channel();
1813 let saw_local_shutdown = Arc::new(AtomicBool::new(false));
1814 let saw_remote_end = Arc::new(AtomicBool::new(false));
1815 let close_notified = Arc::new(AtomicBool::new(false));
1816 spawn_unix_socket_reader(
1817 read_stream,
1818 sender.clone(),
1819 Arc::clone(&saw_local_shutdown),
1820 Arc::clone(&saw_remote_end),
1821 Arc::clone(&close_notified),
1822 );
1823
1824 Ok(Self {
1825 stream,
1826 events,
1827 event_sender: sender,
1828 listener_id,
1829 local_path,
1830 remote_path,
1831 saw_local_shutdown,
1832 saw_remote_end,
1833 close_notified,
1834 })
1835 }
1836
1837 fn poll(&mut self, wait: Duration) -> Result<Option<JavascriptTcpSocketEvent>, SidecarError> {
1838 match self.events.recv_timeout(wait) {
1839 Ok(event) => Ok(Some(event)),
1840 Err(RecvTimeoutError::Timeout) => Ok(None),
1841 Err(RecvTimeoutError::Disconnected) => Ok(None),
1842 }
1843 }
1844
1845 fn socket_info(&self) -> Value {
1846 json!({
1847 "localPath": self.local_path.clone(),
1848 "remotePath": self.remote_path.clone(),
1849 })
1850 }
1851
1852 fn write_all(&self, contents: &[u8]) -> Result<usize, SidecarError> {
1853 let mut stream = self
1854 .stream
1855 .lock()
1856 .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
1857 stream.write_all(contents).map_err(sidecar_net_error)?;
1858 Ok(contents.len())
1859 }
1860
1861 fn shutdown_write(&self) -> Result<(), SidecarError> {
1862 let stream = self
1863 .stream
1864 .lock()
1865 .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
1866 self.saw_local_shutdown.store(true, Ordering::SeqCst);
1867 stream
1868 .shutdown(Shutdown::Write)
1869 .map_err(sidecar_net_error)?;
1870 if self.saw_remote_end.load(Ordering::SeqCst)
1871 && !self.close_notified.swap(true, Ordering::SeqCst)
1872 {
1873 let _ = self
1874 .event_sender
1875 .send(JavascriptTcpSocketEvent::Close { had_error: false });
1876 }
1877 Ok(())
1878 }
1879
1880 fn close(&self) -> Result<(), SidecarError> {
1881 let stream = self
1882 .stream
1883 .lock()
1884 .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?;
1885 stream.shutdown(Shutdown::Both).map_err(sidecar_net_error)
1886 }
1887}
1888
1889impl ActiveUnixListener {
1892 fn bind(
1893 host_path: &Path,
1894 guest_path: &str,
1895 backlog: Option<u32>,
1896 ) -> Result<Self, SidecarError> {
1897 if let Some(parent) = host_path.parent() {
1898 fs::create_dir_all(parent).map_err(sidecar_net_error)?;
1899 }
1900 let listener = UnixListener::bind(host_path).map_err(sidecar_net_error)?;
1901 listener.set_nonblocking(true).map_err(sidecar_net_error)?;
1902 Ok(Self {
1903 listener,
1904 path: guest_path.to_owned(),
1905 backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
1906 .expect("default backlog fits within usize"),
1907 active_connection_ids: BTreeSet::new(),
1908 })
1909 }
1910
1911 fn path(&self) -> &str {
1912 &self.path
1913 }
1914
1915 fn poll(
1916 &mut self,
1917 wait: Duration,
1918 ) -> Result<Option<JavascriptUnixListenerEvent>, SidecarError> {
1919 let deadline = Instant::now() + wait;
1920 loop {
1921 match self.listener.accept() {
1922 Ok((stream, remote_addr)) => {
1923 if self.active_connection_ids.len() >= self.backlog {
1924 let _ = stream.shutdown(Shutdown::Both);
1925 if wait.is_zero() || Instant::now() >= deadline {
1926 return Ok(None);
1927 }
1928 continue;
1929 }
1930
1931 let local_path = Some(self.path.clone());
1932 let remote_path = unix_socket_path(&remote_addr);
1933 return Ok(Some(JavascriptUnixListenerEvent::Connection(
1934 PendingUnixSocket {
1935 stream,
1936 local_path,
1937 remote_path,
1938 },
1939 )));
1940 }
1941 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
1942 if wait.is_zero() || Instant::now() >= deadline {
1943 return Ok(None);
1944 }
1945 thread::sleep(Duration::from_millis(10));
1946 }
1947 Err(error) => {
1948 return Ok(Some(JavascriptUnixListenerEvent::Error {
1949 code: io_error_code(&error),
1950 message: error.to_string(),
1951 }));
1952 }
1953 }
1954 }
1955 }
1956
1957 fn close(&self) -> Result<(), SidecarError> {
1958 Ok(())
1959 }
1960
1961 fn active_connection_count(&self) -> usize {
1962 self.active_connection_ids.len()
1963 }
1964
1965 fn register_connection(&mut self, socket_id: &str) {
1966 self.active_connection_ids.insert(socket_id.to_string());
1967 }
1968
1969 fn release_connection(&mut self, socket_id: &str) {
1970 self.active_connection_ids.remove(socket_id);
1971 }
1972}
1973
1974impl ActiveTcpListener {
1975 fn bind(
1976 bind_host: &str,
1977 guest_host: &str,
1978 guest_port: u16,
1979 backlog: Option<u32>,
1980 ) -> Result<Self, SidecarError> {
1981 let bind_addr = resolve_tcp_bind_addr(bind_host, 0)?;
1982 let guest_addr = resolve_tcp_bind_addr(guest_host, guest_port)?;
1983 let listener = TcpListener::bind(bind_addr).map_err(sidecar_net_error)?;
1984 listener.set_nonblocking(true).map_err(sidecar_net_error)?;
1985 let local_addr = listener.local_addr().map_err(sidecar_net_error)?;
1986 Ok(Self {
1987 listener: Some(listener),
1988 kernel_socket_id: None,
1989 local_addr: Some(local_addr),
1990 guest_local_addr: guest_addr,
1991 backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
1992 .expect("default backlog fits within usize"),
1993 active_connection_ids: BTreeSet::new(),
1994 })
1995 }
1996
1997 fn bind_kernel(
1998 kernel: &mut SidecarKernel,
1999 kernel_pid: u32,
2000 guest_host: &str,
2001 guest_port: u16,
2002 backlog: Option<u32>,
2003 ) -> Result<Self, SidecarError> {
2004 let guest_addr = resolve_tcp_bind_addr(guest_host, guest_port)?;
2005 let spec = match guest_addr {
2006 SocketAddr::V4(_) => SocketSpec::tcp(),
2007 SocketAddr::V6(_) => SocketSpec::new(SocketDomain::Inet6, SocketType::Stream),
2008 };
2009 let socket_id = kernel
2010 .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
2011 .map_err(kernel_error)?;
2012 kernel
2013 .socket_bind_inet(
2014 EXECUTION_DRIVER_NAME,
2015 kernel_pid,
2016 socket_id,
2017 InetSocketAddress::new(guest_addr.ip().to_string(), guest_addr.port()),
2018 )
2019 .map_err(kernel_error)?;
2020 kernel
2021 .socket_listen(
2022 EXECUTION_DRIVER_NAME,
2023 kernel_pid,
2024 socket_id,
2025 usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
2026 .expect("default backlog fits within usize"),
2027 )
2028 .map_err(kernel_error)?;
2029 Ok(Self {
2030 listener: None,
2031 kernel_socket_id: Some(socket_id),
2032 local_addr: Some(guest_addr),
2033 guest_local_addr: guest_addr,
2034 backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG))
2035 .expect("default backlog fits within usize"),
2036 active_connection_ids: BTreeSet::new(),
2037 })
2038 }
2039
2040 pub(crate) fn local_addr(&self) -> SocketAddr {
2041 self.local_addr.unwrap_or(self.guest_local_addr)
2042 }
2043
2044 fn guest_local_addr(&self) -> SocketAddr {
2045 self.guest_local_addr
2046 }
2047
2048 fn poll(
2049 &mut self,
2050 kernel: &mut SidecarKernel,
2051 kernel_pid: u32,
2052 wait: Duration,
2053 ) -> Result<Option<JavascriptTcpListenerEvent>, SidecarError> {
2054 if let Some(socket_id) = self.kernel_socket_id {
2055 let result = kernel
2056 .poll_targets(
2057 EXECUTION_DRIVER_NAME,
2058 kernel_pid,
2059 vec![PollTargetEntry::socket(socket_id, POLLIN)],
2060 i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
2061 )
2062 .map_err(kernel_error)?;
2063 let revents = result
2064 .targets
2065 .first()
2066 .map(|entry| entry.revents)
2067 .unwrap_or_else(PollEvents::empty);
2068 if revents.is_empty() {
2069 return Ok(None);
2070 }
2071 let accepted_socket_id =
2072 match kernel.socket_accept(EXECUTION_DRIVER_NAME, kernel_pid, socket_id) {
2073 Ok(accepted_socket_id) => accepted_socket_id,
2074 Err(error) if error.code() == "EAGAIN" => return Ok(None),
2075 Err(error) => {
2076 return Ok(Some(JavascriptTcpListenerEvent::Error {
2077 code: Some(error.code().to_string()),
2078 message: error.to_string(),
2079 }));
2080 }
2081 };
2082 let accepted = kernel.socket_get(accepted_socket_id).ok_or_else(|| {
2083 SidecarError::InvalidState(format!(
2084 "accepted kernel TCP socket {accepted_socket_id} is missing"
2085 ))
2086 })?;
2087 let local_addr = accepted.local_address().ok_or_else(|| {
2088 SidecarError::InvalidState(format!(
2089 "accepted kernel TCP socket {accepted_socket_id} missing local address"
2090 ))
2091 })?;
2092 let remote_addr = accepted.peer_address().ok_or_else(|| {
2093 SidecarError::InvalidState(format!(
2094 "accepted kernel TCP socket {accepted_socket_id} missing peer address"
2095 ))
2096 })?;
2097 return Ok(Some(JavascriptTcpListenerEvent::Connection(
2098 PendingTcpSocket {
2099 stream: None,
2100 kernel_socket_id: Some(accepted_socket_id),
2101 preallocated: true,
2102 guest_local_addr: resolve_tcp_bind_addr(local_addr.host(), local_addr.port())?,
2103 guest_remote_addr: resolve_tcp_bind_addr(
2104 remote_addr.host(),
2105 remote_addr.port(),
2106 )?,
2107 },
2108 )));
2109 }
2110
2111 let deadline = Instant::now() + wait;
2112 loop {
2113 match self
2114 .listener
2115 .as_ref()
2116 .ok_or_else(|| {
2117 SidecarError::InvalidState(String::from("TCP listener socket missing"))
2118 })?
2119 .accept()
2120 {
2121 Ok((stream, remote_addr)) => {
2122 if self.active_connection_ids.len() >= self.backlog {
2123 let _ = stream.shutdown(Shutdown::Both);
2124 if wait.is_zero() || Instant::now() >= deadline {
2125 return Ok(None);
2126 }
2127 continue;
2128 }
2129 return Ok(Some(JavascriptTcpListenerEvent::Connection(
2130 PendingTcpSocket {
2131 stream: Some(stream),
2132 kernel_socket_id: None,
2133 preallocated: false,
2134 guest_local_addr: self.guest_local_addr,
2135 guest_remote_addr: SocketAddr::new(
2136 remote_addr.ip(),
2137 remote_addr.port(),
2138 ),
2139 },
2140 )));
2141 }
2142 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
2143 if wait.is_zero() || Instant::now() >= deadline {
2144 return Ok(None);
2145 }
2146 thread::sleep(Duration::from_millis(10));
2147 }
2148 Err(error) => {
2149 return Ok(Some(JavascriptTcpListenerEvent::Error {
2150 code: io_error_code(&error),
2151 message: error.to_string(),
2152 }));
2153 }
2154 }
2155 }
2156 }
2157
2158 fn close(&self, kernel: &mut SidecarKernel, kernel_pid: u32) -> Result<(), SidecarError> {
2159 if let Some(socket_id) = self.kernel_socket_id {
2160 kernel
2161 .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id)
2162 .map_err(kernel_error)?;
2163 }
2164 Ok(())
2165 }
2166
2167 fn active_connection_count(&self) -> usize {
2168 self.active_connection_ids.len()
2169 }
2170
2171 fn register_connection(&mut self, socket_id: &str) {
2172 self.active_connection_ids.insert(socket_id.to_string());
2173 }
2174
2175 fn release_connection(&mut self, socket_id: &str) {
2176 self.active_connection_ids.remove(socket_id);
2177 }
2178}
2179
2180impl ActiveUdpSocket {
2183 fn new(
2184 kernel: &mut SidecarKernel,
2185 kernel_pid: u32,
2186 family: JavascriptUdpFamily,
2187 ) -> Result<Self, SidecarError> {
2188 let spec = match family {
2189 JavascriptUdpFamily::Ipv4 => SocketSpec::udp(),
2190 JavascriptUdpFamily::Ipv6 => SocketSpec::new(SocketDomain::Inet6, SocketType::Datagram),
2191 };
2192 let socket_id = kernel
2193 .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec)
2194 .map_err(kernel_error)?;
2195 Ok(Self {
2196 family,
2197 socket: None,
2198 kernel_socket_id: Some(socket_id),
2199 guest_local_addr: None,
2200 recv_buffer_size: 0,
2201 send_buffer_size: 0,
2202 })
2203 }
2204
2205 fn local_addr(&self) -> Option<SocketAddr> {
2206 self.guest_local_addr
2207 }
2208
2209 fn socket(&self) -> Result<&UdpSocket, SidecarError> {
2210 self.socket
2211 .as_ref()
2212 .ok_or_else(|| SidecarError::Execution(String::from("EBADF: bad file descriptor")))
2213 }
2214
2215 fn bind(
2216 &mut self,
2217 kernel: &mut SidecarKernel,
2218 kernel_pid: u32,
2219 host: Option<&str>,
2220 port: u16,
2221 context: &JavascriptSocketPathContext,
2222 ) -> Result<SocketAddr, SidecarError> {
2223 if self.socket.is_some() || self.guest_local_addr.is_some() {
2224 return Err(SidecarError::Execution(String::from(
2225 "EINVAL: secure-exec dgram socket is already bound",
2226 )));
2227 }
2228
2229 let (bind_host, guest_host, guest_family) = normalize_udp_bind_host(host, self.family)?;
2230 let guest_port = allocate_guest_listen_port(
2231 port,
2232 guest_family,
2233 &context.used_udp_guest_ports,
2234 context.listen_policy,
2235 )?;
2236 let local_addr = resolve_udp_bind_addr(guest_host, guest_port, self.family)?;
2237 if let Some(socket_id) = self.kernel_socket_id {
2238 kernel
2239 .socket_bind_inet(
2240 EXECUTION_DRIVER_NAME,
2241 kernel_pid,
2242 socket_id,
2243 InetSocketAddress::new(local_addr.ip().to_string(), local_addr.port()),
2244 )
2245 .map_err(kernel_error)?;
2246 } else {
2247 let bind_addr = resolve_udp_bind_addr(bind_host, 0, self.family)?;
2248 let socket = UdpSocket::bind(bind_addr).map_err(sidecar_net_error)?;
2249 socket.set_nonblocking(true).map_err(sidecar_net_error)?;
2250 self.socket = Some(socket);
2251 }
2252 self.guest_local_addr = Some(local_addr);
2253 Ok(local_addr)
2254 }
2255
2256 fn ensure_bound_for_send(
2257 &mut self,
2258 kernel: &mut SidecarKernel,
2259 kernel_pid: u32,
2260 context: &JavascriptSocketPathContext,
2261 ) -> Result<SocketAddr, SidecarError> {
2262 if let Some(local_addr) = self.local_addr() {
2263 return Ok(local_addr);
2264 }
2265
2266 self.bind(kernel, kernel_pid, None, 0, context)
2267 }
2268
2269 fn send_to<B>(
2270 &mut self,
2271 request: ActiveUdpSendToRequest<'_, B>,
2272 ) -> Result<(usize, SocketAddr), SidecarError>
2273 where
2274 B: NativeSidecarBridge + Send + 'static,
2275 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
2276 {
2277 let ActiveUdpSendToRequest {
2278 bridge,
2279 kernel,
2280 kernel_pid,
2281 vm_id,
2282 dns,
2283 host,
2284 port,
2285 context,
2286 contents,
2287 } = request;
2288 let remote_addr = resolve_udp_addr(UdpRemoteAddrRequest {
2289 bridge,
2290 kernel,
2291 vm_id,
2292 dns,
2293 host,
2294 port,
2295 family: self.family,
2296 context,
2297 })?;
2298 let local_addr = self.ensure_bound_for_send(kernel, kernel_pid, context)?;
2299 let written = if let Some(socket_id) = self.kernel_socket_id {
2300 if is_loopback_ip(remote_addr.ip()) && remote_addr.port() == port {
2301 kernel
2302 .socket_send_to_inet_loopback(
2303 EXECUTION_DRIVER_NAME,
2304 kernel_pid,
2305 socket_id,
2306 InetSocketAddress::new(remote_addr.ip().to_string(), remote_addr.port()),
2307 contents,
2308 )
2309 .map_err(kernel_error)?
2310 } else {
2311 return Err(SidecarError::Execution(String::from(
2312 "ERR_NOT_IMPLEMENTED: external UDP datagrams are not yet supported by the kernel-backed V8 bridge",
2313 )));
2314 }
2315 } else {
2316 let socket = self.socket.as_ref().ok_or_else(|| {
2317 SidecarError::InvalidState(String::from("UDP socket is not initialized"))
2318 })?;
2319 socket
2320 .send_to(contents, remote_addr)
2321 .map_err(sidecar_net_error)?
2322 };
2323 Ok((written, local_addr))
2324 }
2325
2326 fn poll(
2327 &self,
2328 kernel: &mut SidecarKernel,
2329 kernel_pid: u32,
2330 wait: Duration,
2331 ) -> Result<Option<JavascriptUdpSocketEvent>, SidecarError> {
2332 if let Some(socket_id) = self.kernel_socket_id {
2333 let result = kernel
2334 .poll_targets(
2335 EXECUTION_DRIVER_NAME,
2336 kernel_pid,
2337 vec![PollTargetEntry::socket(socket_id, POLLIN)],
2338 i32::try_from(wait.as_millis()).unwrap_or(i32::MAX),
2339 )
2340 .map_err(kernel_error)?;
2341 let revents = result
2342 .targets
2343 .first()
2344 .map(|entry| entry.revents)
2345 .unwrap_or_else(PollEvents::empty);
2346 if revents.is_empty() {
2347 return Ok(None);
2348 }
2349 return match kernel.socket_recv_datagram(
2350 EXECUTION_DRIVER_NAME,
2351 kernel_pid,
2352 socket_id,
2353 64 * 1024,
2354 ) {
2355 Ok(Some(datagram)) => {
2356 let (source_address, payload) = datagram.into_parts();
2357 let remote_addr = source_address
2358 .map(|source| {
2359 resolve_udp_bind_addr(source.host(), source.port(), self.family)
2360 })
2361 .transpose()?
2362 .unwrap_or_else(|| match self.family {
2363 JavascriptUdpFamily::Ipv4 => {
2364 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)
2365 }
2366 JavascriptUdpFamily::Ipv6 => {
2367 SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0)
2368 }
2369 });
2370 Ok(Some(JavascriptUdpSocketEvent::Message {
2371 data: payload,
2372 remote_addr,
2373 }))
2374 }
2375 Ok(None) => Ok(None),
2376 Err(error) if error.code() == "EAGAIN" => Ok(None),
2377 Err(error) => Ok(Some(JavascriptUdpSocketEvent::Error {
2378 code: Some(error.code().to_string()),
2379 message: error.to_string(),
2380 })),
2381 };
2382 }
2383 let socket = self.socket()?;
2384 let deadline = Instant::now() + wait;
2385 let mut buffer = vec![0_u8; 64 * 1024];
2386
2387 loop {
2388 match socket.recv_from(&mut buffer) {
2389 Ok((bytes_read, remote_addr)) => {
2390 return Ok(Some(JavascriptUdpSocketEvent::Message {
2391 data: buffer[..bytes_read].to_vec(),
2392 remote_addr,
2393 }));
2394 }
2395 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
2396 if wait.is_zero() || Instant::now() >= deadline {
2397 return Ok(None);
2398 }
2399 thread::sleep(Duration::from_millis(10));
2400 }
2401 Err(error) => {
2402 return Ok(Some(JavascriptUdpSocketEvent::Error {
2403 code: io_error_code(&error),
2404 message: error.to_string(),
2405 }));
2406 }
2407 }
2408 }
2409 }
2410
2411 fn close(&mut self, kernel: &mut SidecarKernel, kernel_pid: u32) {
2412 if let Some(socket_id) = self.kernel_socket_id {
2413 let _ = kernel.socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id);
2414 }
2415 self.socket.take();
2416 self.guest_local_addr = None;
2417 }
2418
2419 fn set_buffer_size(&mut self, which: &str, size: usize) -> Result<(), SidecarError> {
2420 match which {
2421 "recv" => self.recv_buffer_size = size,
2422 "send" => self.send_buffer_size = size,
2423 other => {
2424 return Err(SidecarError::InvalidState(format!(
2425 "unsupported UDP buffer size kind {other}"
2426 )));
2427 }
2428 }
2429 if self.kernel_socket_id.is_some() {
2430 return Ok(());
2431 }
2432 let socket = self.socket()?;
2433 let socket = SockRef::from(socket);
2434 match which {
2435 "recv" => socket.set_recv_buffer_size(size).map_err(sidecar_net_error),
2436 "send" => socket.set_send_buffer_size(size).map_err(sidecar_net_error),
2437 other => Err(SidecarError::InvalidState(format!(
2438 "unsupported UDP buffer size kind {other}"
2439 ))),
2440 }
2441 }
2442
2443 fn get_buffer_size(&self, which: &str) -> Result<usize, SidecarError> {
2444 if self.kernel_socket_id.is_some() {
2445 return Ok(match which {
2446 "recv" => self.recv_buffer_size,
2447 "send" => self.send_buffer_size,
2448 other => {
2449 return Err(SidecarError::InvalidState(format!(
2450 "unsupported UDP buffer size kind {other}"
2451 )));
2452 }
2453 });
2454 }
2455 let socket = self.socket()?;
2456 let socket = SockRef::from(socket);
2457 match which {
2458 "recv" => socket.recv_buffer_size().map_err(sidecar_net_error),
2459 "send" => socket.send_buffer_size().map_err(sidecar_net_error),
2460 other => Err(SidecarError::InvalidState(format!(
2461 "unsupported UDP buffer size kind {other}"
2462 ))),
2463 }
2464 }
2465}
2466
2467impl ActiveExecution {
2470 pub(crate) fn uses_shared_v8_runtime(&self) -> bool {
2471 match self {
2472 Self::Javascript(execution) => execution.uses_shared_v8_runtime(),
2473 Self::Python(execution) => execution.uses_shared_v8_runtime(),
2474 Self::Wasm(execution) => execution.uses_shared_v8_runtime(),
2475 Self::Tool(_) => false,
2476 }
2477 }
2478
2479 pub(crate) fn child_pid(&self) -> u32 {
2480 match self {
2481 Self::Javascript(execution) => execution.child_pid(),
2482 Self::Python(execution) => execution.child_pid(),
2483 Self::Wasm(execution) => execution.child_pid(),
2484 Self::Tool(_) => 0,
2485 }
2486 }
2487
2488 pub(crate) fn write_stdin(&mut self, chunk: &[u8]) -> Result<(), SidecarError> {
2489 match self {
2490 Self::Javascript(execution) => execution
2491 .write_stdin(chunk)
2492 .map_err(|error| SidecarError::Execution(error.to_string())),
2493 Self::Python(execution) => execution
2494 .write_stdin(chunk)
2495 .map_err(|error| SidecarError::Execution(error.to_string())),
2496 Self::Wasm(execution) => execution
2497 .write_stdin(chunk)
2498 .map_err(|error| SidecarError::Execution(error.to_string())),
2499 Self::Tool(_) => Ok(()),
2500 }
2501 }
2502
2503 pub(crate) fn close_stdin(&mut self) -> Result<(), SidecarError> {
2504 match self {
2505 Self::Javascript(execution) => execution
2506 .close_stdin()
2507 .map_err(|error| SidecarError::Execution(error.to_string())),
2508 Self::Python(execution) => execution
2509 .close_stdin()
2510 .map_err(|error| SidecarError::Execution(error.to_string())),
2511 Self::Wasm(execution) => execution
2512 .close_stdin()
2513 .map_err(|error| SidecarError::Execution(error.to_string())),
2514 Self::Tool(_) => Ok(()),
2515 }
2516 }
2517
2518 pub(crate) fn respond_python_vfs_rpc_success(
2519 &mut self,
2520 id: u64,
2521 payload: PythonVfsRpcResponsePayload,
2522 ) -> Result<(), SidecarError> {
2523 match self {
2524 Self::Python(execution) => execution
2525 .respond_vfs_rpc_success(id, payload)
2526 .map_err(|error| SidecarError::Execution(error.to_string())),
2527 _ => Err(SidecarError::InvalidState(String::from(
2528 "only Python executions can service Python VFS RPC responses",
2529 ))),
2530 }
2531 }
2532
2533 pub(crate) fn respond_python_vfs_rpc_error(
2534 &mut self,
2535 id: u64,
2536 code: impl Into<String>,
2537 message: impl Into<String>,
2538 ) -> Result<(), SidecarError> {
2539 match self {
2540 Self::Python(execution) => execution
2541 .respond_vfs_rpc_error(id, code, message)
2542 .map_err(|error| SidecarError::Execution(error.to_string())),
2543 _ => Err(SidecarError::InvalidState(String::from(
2544 "only Python executions can service Python VFS RPC responses",
2545 ))),
2546 }
2547 }
2548
2549 pub(crate) fn send_javascript_stream_event(
2550 &self,
2551 event_type: &str,
2552 payload: Value,
2553 ) -> Result<(), SidecarError> {
2554 match self {
2555 Self::Javascript(execution) => execution
2556 .send_stream_event(event_type, payload)
2557 .map_err(|error| SidecarError::Execution(error.to_string())),
2558 Self::Wasm(execution) => execution
2559 .send_stream_event(event_type, payload)
2560 .map_err(|error| SidecarError::Execution(error.to_string())),
2561 _ => Err(SidecarError::InvalidState(String::from(
2562 "only embedded V8 executions can receive JavaScript stream events",
2563 ))),
2564 }
2565 }
2566
2567 pub(crate) fn javascript_v8_session_handle(&self) -> Option<V8SessionHandle> {
2568 match self {
2569 Self::Javascript(execution) => Some(execution.v8_session_handle()),
2570 Self::Wasm(execution) => Some(execution.v8_session_handle()),
2571 _ => None,
2572 }
2573 }
2574
2575 pub(crate) fn terminate(&mut self) -> Result<(), SidecarError> {
2576 match self {
2577 Self::Javascript(execution) => execution
2578 .terminate()
2579 .map_err(|error| SidecarError::Execution(error.to_string())),
2580 Self::Python(execution) => execution
2581 .kill()
2582 .map_err(|error| SidecarError::Execution(error.to_string())),
2583 Self::Wasm(execution) => execution
2584 .terminate()
2585 .map_err(|error| SidecarError::Execution(error.to_string())),
2586 Self::Tool(_) => Ok(()),
2587 }
2588 }
2589
2590 pub(crate) fn respond_javascript_sync_rpc_success(
2591 &mut self,
2592 id: u64,
2593 result: Value,
2594 ) -> Result<(), SidecarError> {
2595 match self {
2596 Self::Javascript(execution) => execution
2597 .respond_sync_rpc_success(id, result)
2598 .map_err(|error| SidecarError::Execution(error.to_string())),
2599 Self::Python(execution) => execution
2600 .respond_javascript_sync_rpc_success(id, result)
2601 .map_err(|error| SidecarError::Execution(error.to_string())),
2602 Self::Wasm(execution) => execution
2603 .respond_sync_rpc_success(id, result)
2604 .map_err(|error| SidecarError::Execution(error.to_string())),
2605 _ => Err(SidecarError::InvalidState(String::from(
2606 "only JavaScript, Python, and WebAssembly executions can service JavaScript sync RPC responses",
2607 ))),
2608 }
2609 }
2610
2611 pub(crate) fn respond_javascript_sync_rpc_error(
2612 &mut self,
2613 id: u64,
2614 code: impl Into<String>,
2615 message: impl Into<String>,
2616 ) -> Result<(), SidecarError> {
2617 match self {
2618 Self::Javascript(execution) => execution
2619 .respond_sync_rpc_error(id, code, message)
2620 .map_err(|error| SidecarError::Execution(error.to_string())),
2621 Self::Python(execution) => execution
2622 .respond_javascript_sync_rpc_error(id, code, message)
2623 .map_err(|error| SidecarError::Execution(error.to_string())),
2624 Self::Wasm(execution) => execution
2625 .respond_sync_rpc_error(id, code, message)
2626 .map_err(|error| SidecarError::Execution(error.to_string())),
2627 _ => Err(SidecarError::InvalidState(String::from(
2628 "only JavaScript, Python, and WebAssembly executions can service JavaScript sync RPC responses",
2629 ))),
2630 }
2631 }
2632
2633 pub(crate) async fn poll_event(
2634 &mut self,
2635 timeout: Duration,
2636 ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
2637 match self {
2638 Self::Javascript(execution) => execution
2639 .poll_event(timeout)
2640 .await
2641 .map(|event| {
2642 event.map(|event| match event {
2643 JavascriptExecutionEvent::Stdout(chunk) => {
2644 ActiveExecutionEvent::Stdout(chunk)
2645 }
2646 JavascriptExecutionEvent::Stderr(chunk) => {
2647 ActiveExecutionEvent::Stderr(chunk)
2648 }
2649 JavascriptExecutionEvent::SyncRpcRequest(request) => {
2650 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2651 }
2652 JavascriptExecutionEvent::SignalState {
2653 signal,
2654 registration,
2655 } => ActiveExecutionEvent::SignalState {
2656 signal,
2657 registration: map_node_signal_registration(registration),
2658 },
2659 JavascriptExecutionEvent::Exited(code) => {
2660 ActiveExecutionEvent::Exited(code)
2661 }
2662 })
2663 })
2664 .map_err(|error| SidecarError::Execution(error.to_string())),
2665 Self::Python(execution) => execution
2666 .poll_event(timeout)
2667 .await
2668 .map(|event| {
2669 event.map(|event| match event {
2670 PythonExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2671 PythonExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2672 PythonExecutionEvent::JavascriptSyncRpcRequest(request) => {
2673 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2674 }
2675 PythonExecutionEvent::VfsRpcRequest(request) => {
2676 ActiveExecutionEvent::PythonVfsRpcRequest(request)
2677 }
2678 PythonExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2679 })
2680 })
2681 .map_err(|error| SidecarError::Execution(error.to_string())),
2682 Self::Wasm(execution) => execution
2683 .poll_event(timeout)
2684 .await
2685 .map(|event| {
2686 event.map(|event| match event {
2687 WasmExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2688 WasmExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2689 WasmExecutionEvent::SyncRpcRequest(request) => {
2690 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2691 }
2692 WasmExecutionEvent::SignalState {
2693 signal,
2694 registration,
2695 } => ActiveExecutionEvent::SignalState {
2696 signal,
2697 registration: map_wasm_signal_registration(registration),
2698 },
2699 WasmExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2700 })
2701 })
2702 .map_err(|error| SidecarError::Execution(error.to_string())),
2703 Self::Tool(execution) => {
2704 let _ = timeout;
2705 poll_tool_process_event(execution)
2706 }
2707 }
2708 }
2709
2710 pub(crate) fn poll_event_blocking(
2711 &mut self,
2712 timeout: Duration,
2713 ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
2714 match self {
2715 Self::Javascript(execution) => execution
2716 .poll_event_blocking(timeout)
2717 .map(|event| {
2718 event.map(|event| match event {
2719 JavascriptExecutionEvent::Stdout(chunk) => {
2720 ActiveExecutionEvent::Stdout(chunk)
2721 }
2722 JavascriptExecutionEvent::Stderr(chunk) => {
2723 ActiveExecutionEvent::Stderr(chunk)
2724 }
2725 JavascriptExecutionEvent::SyncRpcRequest(request) => {
2726 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2727 }
2728 JavascriptExecutionEvent::SignalState {
2729 signal,
2730 registration,
2731 } => ActiveExecutionEvent::SignalState {
2732 signal,
2733 registration: map_node_signal_registration(registration),
2734 },
2735 JavascriptExecutionEvent::Exited(code) => {
2736 ActiveExecutionEvent::Exited(code)
2737 }
2738 })
2739 })
2740 .map_err(|error| SidecarError::Execution(error.to_string())),
2741 Self::Python(execution) => execution
2742 .poll_event_blocking(timeout)
2743 .map(|event| {
2744 event.map(|event| match event {
2745 PythonExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2746 PythonExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2747 PythonExecutionEvent::JavascriptSyncRpcRequest(request) => {
2748 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2749 }
2750 PythonExecutionEvent::VfsRpcRequest(request) => {
2751 ActiveExecutionEvent::PythonVfsRpcRequest(request)
2752 }
2753 PythonExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2754 })
2755 })
2756 .map_err(|error| SidecarError::Execution(error.to_string())),
2757 Self::Wasm(execution) => execution
2758 .poll_event_blocking(timeout)
2759 .map(|event| {
2760 event.map(|event| match event {
2761 WasmExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk),
2762 WasmExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk),
2763 WasmExecutionEvent::SyncRpcRequest(request) => {
2764 ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
2765 }
2766 WasmExecutionEvent::SignalState {
2767 signal,
2768 registration,
2769 } => ActiveExecutionEvent::SignalState {
2770 signal,
2771 registration: map_wasm_signal_registration(registration),
2772 },
2773 WasmExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code),
2774 })
2775 })
2776 .map_err(|error| SidecarError::Execution(error.to_string())),
2777 Self::Tool(execution) => {
2778 let _ = timeout;
2779 poll_tool_process_event(execution)
2780 }
2781 }
2782 }
2783}
2784
2785struct ToolProcessEventRequest {
2786 sidecar_requests: SharedSidecarRequestClient,
2787 connection_id: String,
2788 session_id: String,
2789 vm_id: String,
2790 tool_resolution: ToolCommandResolution,
2791 cancelled: Arc<AtomicBool>,
2792 pending_events: Arc<Mutex<VecDeque<ActiveExecutionEvent>>>,
2793 events_overflowed: Arc<AtomicBool>,
2794}
2795
2796pub(crate) fn send_tool_process_event(
2797 pending_events: &Arc<Mutex<VecDeque<ActiveExecutionEvent>>>,
2798 events_overflowed: &AtomicBool,
2799 event: ActiveExecutionEvent,
2800) -> bool {
2801 let mut pending_events = pending_events
2802 .lock()
2803 .unwrap_or_else(|poisoned| poisoned.into_inner());
2804 if pending_events.len() >= MAX_PROCESS_EVENT_QUEUE {
2805 events_overflowed.store(true, Ordering::Relaxed);
2806 return false;
2807 }
2808 pending_events.push_back(event);
2809 true
2810}
2811
2812fn spawn_tool_process_events(request: ToolProcessEventRequest) {
2813 let ToolProcessEventRequest {
2814 sidecar_requests,
2815 connection_id,
2816 session_id,
2817 vm_id,
2818 tool_resolution,
2819 cancelled,
2820 pending_events,
2821 events_overflowed,
2822 } = request;
2823 std::thread::spawn(move || match tool_resolution {
2824 ToolCommandResolution::Failure(message) => {
2825 if !send_tool_process_event(
2826 &pending_events,
2827 &events_overflowed,
2828 ActiveExecutionEvent::Stderr(format_tool_failure_output(&message)),
2829 ) {
2830 return;
2831 }
2832 let _ = send_tool_process_event(
2833 &pending_events,
2834 &events_overflowed,
2835 ActiveExecutionEvent::Exited(1),
2836 );
2837 }
2838 ToolCommandResolution::Invoke { request, timeout } => {
2839 let response = sidecar_requests.invoke(
2840 OwnershipScope::vm(connection_id.clone(), session_id.clone(), vm_id.clone()),
2841 SidecarRequestPayload::HostCallback(request.clone()),
2842 timeout,
2843 );
2844 if cancelled.load(Ordering::Relaxed) {
2845 return;
2846 }
2847
2848 match response {
2849 Ok(crate::protocol::SidecarResponsePayload::HostCallbackResult(result)) => {
2850 if let Some(value) = result.result {
2851 let value: serde_json::Value = serde_json::from_str(&value)
2852 .unwrap_or(serde_json::Value::String(value));
2853 let stdout = serde_json::to_vec(&json!({
2854 "ok": true,
2855 "result": value,
2856 }))
2857 .unwrap_or_else(|error| {
2858 format_tool_failure_output(&format!(
2859 "failed to serialize tool result: {error}"
2860 ))
2861 });
2862 if !send_tool_process_event(
2863 &pending_events,
2864 &events_overflowed,
2865 ActiveExecutionEvent::Stdout(stdout),
2866 ) {
2867 return;
2868 }
2869 let _ = send_tool_process_event(
2870 &pending_events,
2871 &events_overflowed,
2872 ActiveExecutionEvent::Exited(0),
2873 );
2874 } else {
2875 let message = result
2876 .error
2877 .unwrap_or_else(|| String::from("tool invocation returned no result"));
2878 if !send_tool_process_event(
2879 &pending_events,
2880 &events_overflowed,
2881 ActiveExecutionEvent::Stderr(format_tool_failure_output(&message)),
2882 ) {
2883 return;
2884 }
2885 let _ = send_tool_process_event(
2886 &pending_events,
2887 &events_overflowed,
2888 ActiveExecutionEvent::Exited(1),
2889 );
2890 }
2891 }
2892 Ok(_) => {
2893 if !send_tool_process_event(
2894 &pending_events,
2895 &events_overflowed,
2896 ActiveExecutionEvent::Stderr(format_tool_failure_output(
2897 "unexpected sidecar tool response",
2898 )),
2899 ) {
2900 return;
2901 }
2902 let _ = send_tool_process_event(
2903 &pending_events,
2904 &events_overflowed,
2905 ActiveExecutionEvent::Exited(1),
2906 );
2907 }
2908 Err(error) => {
2909 if !send_tool_process_event(
2910 &pending_events,
2911 &events_overflowed,
2912 ActiveExecutionEvent::Stderr(format_tool_failure_output(
2913 &error.to_string(),
2914 )),
2915 ) {
2916 return;
2917 }
2918 let _ = send_tool_process_event(
2919 &pending_events,
2920 &events_overflowed,
2921 ActiveExecutionEvent::Exited(1),
2922 );
2923 }
2924 }
2925 }
2926 });
2927}
2928
2929impl<B> NativeSidecar<B>
2930where
2931 B: NativeSidecarBridge + Send + 'static,
2932 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
2933{
2934 pub(crate) async fn execute(
2935 &mut self,
2936 request: &RequestFrame,
2937 payload: ExecuteRequest,
2938 ) -> Result<DispatchResult, SidecarError> {
2939 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
2940 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
2941
2942 let vm = self
2943 .vms
2944 .get_mut(&vm_id)
2945 .ok_or_else(|| missing_vm_error(&vm_id))?;
2946 if vm.active_processes.contains_key(&payload.process_id) {
2947 return Err(SidecarError::InvalidState(format!(
2948 "VM {vm_id} already has an active process with id {}",
2949 payload.process_id
2950 )));
2951 }
2952
2953 if let Some(command) = payload.command.as_deref() {
2954 if let Some(tool_resolution) =
2955 resolve_tool_command(vm, command, &payload.args, payload.cwd.as_deref())?
2956 {
2957 let guest_cwd = payload
2958 .cwd
2959 .as_deref()
2960 .map(normalize_path)
2961 .unwrap_or_else(|| vm.guest_cwd.clone());
2962 let kernel_handle = vm
2963 .kernel
2964 .create_virtual_process(
2965 EXECUTION_DRIVER_NAME,
2966 TOOL_DRIVER_NAME,
2967 command,
2968 std::iter::once(command.to_owned())
2969 .chain(payload.args.iter().cloned())
2970 .collect(),
2971 VirtualProcessOptions {
2972 env: vm.guest_env.clone(),
2973 cwd: Some(guest_cwd.clone()),
2974 ..VirtualProcessOptions::default()
2975 },
2976 )
2977 .map_err(kernel_error)?;
2978 let kernel_pid = kernel_handle.pid();
2979 let tool_execution = ToolExecution::default();
2980 let cancelled = tool_execution.cancelled.clone();
2981 let pending_events = tool_execution.pending_events.clone();
2982 let events_overflowed = tool_execution.events_overflowed.clone();
2983 vm.active_processes.insert(
2984 payload.process_id.clone(),
2985 ActiveProcess::new(
2986 kernel_pid,
2987 kernel_handle,
2988 GuestRuntimeKind::JavaScript,
2989 ActiveExecution::Tool(tool_execution),
2990 )
2991 .with_guest_cwd(guest_cwd.clone())
2992 .with_host_cwd(resolve_vm_guest_path_to_host(vm, &guest_cwd)),
2993 );
2994 self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy)?;
2995 spawn_tool_process_events(ToolProcessEventRequest {
2996 sidecar_requests: self.sidecar_requests.clone(),
2997 connection_id: connection_id.clone(),
2998 session_id: session_id.clone(),
2999 vm_id: vm_id.clone(),
3000 tool_resolution,
3001 cancelled,
3002 pending_events,
3003 events_overflowed,
3004 });
3005
3006 return Ok(DispatchResult {
3007 response: self.respond(
3008 request,
3009 ResponsePayload::ProcessStarted(ProcessStartedResponse {
3010 process_id: payload.process_id,
3011 pid: Some(kernel_pid),
3012 }),
3013 ),
3014 events: Vec::new(),
3015 });
3016 }
3017 }
3018
3019 let resolved = resolve_execute_request(vm, &payload)?;
3020 let mut env = resolved.env.clone();
3021 let sandbox_root = normalize_host_path(&vm.cwd);
3022 env.insert(
3023 String::from(EXECUTION_SANDBOX_ROOT_ENV),
3024 sandbox_root.to_string_lossy().into_owned(),
3025 );
3026 if resolved.runtime == GuestRuntimeKind::JavaScript {
3027 env.insert(
3028 String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
3029 String::from("1"),
3030 );
3031 } else if resolved.runtime == GuestRuntimeKind::WebAssembly {
3032 env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
3033 }
3034 let argv = std::iter::once(resolved.entrypoint.clone())
3035 .chain(resolved.execution_args.iter().cloned())
3036 .collect::<Vec<_>>();
3037 let kernel_handle = vm
3038 .kernel
3039 .spawn_process(
3040 &resolved.command,
3041 argv,
3042 SpawnOptions {
3043 requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
3044 cwd: Some(resolved.guest_cwd.clone()),
3045 ..SpawnOptions::default()
3046 },
3047 )
3048 .map_err(kernel_error)?;
3049 let kernel_pid = kernel_handle.pid();
3050
3051 let (execution, process_env) = match resolved.runtime {
3052 GuestRuntimeKind::JavaScript => {
3053 let inline_code = load_javascript_entrypoint_source(
3054 vm,
3055 &resolved.host_cwd,
3056 &resolved.entrypoint,
3057 &env,
3058 );
3059 prepare_javascript_shadow(vm, &resolved)?;
3060
3061 let context =
3062 self.javascript_engine
3063 .create_context(CreateJavascriptContextRequest {
3064 vm_id: vm_id.clone(),
3065 bootstrap_module: None,
3066 compile_cache_root: Some(self.cache_root.join("node-compile-cache")),
3067 });
3068 let built_reader = build_module_reader(vm, &resolved);
3069 let guest_reader = built_reader.clone().map(|reader| {
3070 Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader))
3071 as Box<dyn GuestModuleReader>
3072 });
3073 let module_reader =
3074 built_reader.map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
3075 let execution = self
3076 .javascript_engine
3077 .start_execution_with_module_reader(
3078 StartJavascriptExecutionRequest {
3079 guest_runtime: guest_runtime_identity(vm, None, None),
3080 vm_id: vm_id.clone(),
3081 context_id: context.context_id,
3082 argv: std::iter::once(resolved.entrypoint.clone())
3083 .chain(resolved.execution_args.iter().cloned())
3084 .collect(),
3085 env: env.clone(),
3086 cwd: resolved.host_cwd.clone(),
3087 limits: javascript_execution_limits(vm),
3088 inline_code,
3089 },
3090 module_reader,
3091 guest_reader,
3092 )
3093 .map_err(javascript_error)?;
3094 (ActiveExecution::Javascript(execution), env.clone())
3095 }
3096 GuestRuntimeKind::Python => {
3097 let python_file_path = python_file_entrypoint(&resolved.entrypoint);
3098 let pyodide_dist_path = self
3099 .python_engine
3100 .bundled_pyodide_dist_path_for_vm(&vm_id)
3101 .map_err(python_error)?;
3102 let pyodide_cache_path = pyodide_dist_path
3103 .parent()
3104 .and_then(Path::parent)
3105 .unwrap_or(pyodide_dist_path.as_path())
3106 .join("pyodide-package-cache");
3107 add_runtime_guest_path_mapping(
3108 &mut env,
3109 PYTHON_PYODIDE_GUEST_ROOT,
3110 &pyodide_dist_path,
3111 );
3112 add_runtime_guest_path_mapping(
3113 &mut env,
3114 PYTHON_PYODIDE_CACHE_GUEST_ROOT,
3115 &pyodide_cache_path,
3116 );
3117 add_runtime_host_access_path(
3118 &mut env,
3119 "AGENTOS_EXTRA_FS_READ_PATHS",
3120 &pyodide_dist_path,
3121 true,
3122 );
3123 add_runtime_host_access_path(
3124 &mut env,
3125 "AGENTOS_EXTRA_FS_READ_PATHS",
3126 &pyodide_cache_path,
3127 true,
3128 );
3129 add_runtime_host_access_path(
3130 &mut env,
3131 "AGENTOS_EXTRA_FS_WRITE_PATHS",
3132 &pyodide_cache_path,
3133 false,
3134 );
3135 let context = self
3136 .python_engine
3137 .create_context(CreatePythonContextRequest {
3138 vm_id: vm_id.clone(),
3139 pyodide_dist_path,
3140 });
3141 let execution = self
3142 .python_engine
3143 .start_execution(StartPythonExecutionRequest {
3144 vm_id: vm_id.clone(),
3145 context_id: context.context_id,
3146 code: resolved.entrypoint.clone(),
3147 file_path: python_file_path,
3148 env: env.clone(),
3149 cwd: resolved.host_cwd.clone(),
3150 limits: python_execution_limits(vm),
3151 guest_runtime: guest_runtime_identity(vm, None, None),
3152 })
3153 .map_err(python_error)?;
3154 (ActiveExecution::Python(execution), env.clone())
3155 }
3156 GuestRuntimeKind::WebAssembly => {
3157 let wasm_limits = wasm_execution_limits(vm);
3158 let wasm_guest_runtime =
3159 guest_runtime_identity(vm, Some(u64::from(kernel_pid)), Some(0));
3160 let wasm_permission_tier = resolved.wasm_permission_tier.unwrap_or_else(|| {
3161 resolve_wasm_permission_tier(
3162 vm,
3163 Some(&resolved.command),
3164 None,
3165 &resolved.entrypoint,
3166 )
3167 });
3168 let context = self.wasm_engine.create_context(CreateWasmContextRequest {
3169 vm_id: vm_id.clone(),
3170 module_path: Some(resolved.entrypoint.clone()),
3171 });
3172 let execution = self
3173 .wasm_engine
3174 .start_execution(StartWasmExecutionRequest {
3175 vm_id: vm_id.clone(),
3176 context_id: context.context_id,
3177 argv: resolved.process_args.clone(),
3178 env: env.clone(),
3179 cwd: resolved.host_cwd.clone(),
3180 permission_tier: execution_wasm_permission_tier(wasm_permission_tier),
3181 limits: wasm_limits,
3182 guest_runtime: wasm_guest_runtime,
3183 })
3184 .map_err(wasm_error)?;
3185 (ActiveExecution::Wasm(Box::new(execution)), env)
3186 }
3187 };
3188 let child_pid = execution.child_pid();
3189 let kernel_stdin_writer_fd = install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?;
3190 vm.active_processes.insert(
3191 payload.process_id.clone(),
3192 ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
3193 .with_kernel_stdin_writer_fd(kernel_stdin_writer_fd)
3194 .with_guest_cwd(resolved.guest_cwd.clone())
3195 .with_env(process_env)
3196 .with_host_cwd(resolved.host_cwd.clone()),
3197 );
3198 self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy)?;
3199
3200 Ok(DispatchResult {
3201 response: self.respond(
3202 request,
3203 ResponsePayload::ProcessStarted(ProcessStartedResponse {
3204 process_id: payload.process_id,
3205 pid: Some(if child_pid == 0 {
3206 kernel_pid
3207 } else {
3208 child_pid
3209 }),
3210 }),
3211 ),
3212 events: Vec::new(),
3213 })
3214 }
3215
3216 pub(crate) async fn write_stdin(
3217 &mut self,
3218 request: &RequestFrame,
3219 payload: WriteStdinRequest,
3220 ) -> Result<DispatchResult, SidecarError> {
3221 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3222 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3223
3224 let vm = self
3225 .vms
3226 .get_mut(&vm_id)
3227 .ok_or_else(|| missing_vm_error(&vm_id))?;
3228 let process = vm
3229 .active_processes
3230 .get_mut(&payload.process_id)
3231 .ok_or_else(|| {
3232 SidecarError::InvalidState(format!(
3233 "VM {vm_id} has no active process {}",
3234 payload.process_id
3235 ))
3236 })?;
3237 process.execution.write_stdin(&payload.chunk)?;
3238 write_kernel_process_stdin(&mut vm.kernel, process, &payload.chunk)?;
3239
3240 Ok(DispatchResult {
3241 response: self.respond(
3242 request,
3243 ResponsePayload::StdinWritten(StdinWrittenResponse {
3244 process_id: payload.process_id,
3245 accepted_bytes: payload.chunk.len() as u64,
3246 }),
3247 ),
3248 events: Vec::new(),
3249 })
3250 }
3251
3252 pub(crate) async fn close_stdin(
3253 &mut self,
3254 request: &RequestFrame,
3255 payload: CloseStdinRequest,
3256 ) -> Result<DispatchResult, SidecarError> {
3257 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3258 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3259
3260 let vm = self
3261 .vms
3262 .get_mut(&vm_id)
3263 .ok_or_else(|| missing_vm_error(&vm_id))?;
3264 let process = vm
3265 .active_processes
3266 .get_mut(&payload.process_id)
3267 .ok_or_else(|| {
3268 SidecarError::InvalidState(format!(
3269 "VM {vm_id} has no active process {}",
3270 payload.process_id
3271 ))
3272 })?;
3273 process.execution.close_stdin()?;
3274 close_kernel_process_stdin(&mut vm.kernel, process)?;
3275
3276 Ok(DispatchResult {
3277 response: self.respond(
3278 request,
3279 ResponsePayload::StdinClosed(StdinClosedResponse {
3280 process_id: payload.process_id,
3281 }),
3282 ),
3283 events: Vec::new(),
3284 })
3285 }
3286
3287 pub(crate) async fn kill_process(
3288 &mut self,
3289 request: &RequestFrame,
3290 payload: KillProcessRequest,
3291 ) -> Result<DispatchResult, SidecarError> {
3292 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3293 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3294 self.kill_process_internal(&vm_id, &payload.process_id, &payload.signal)?;
3295
3296 Ok(DispatchResult {
3297 response: self.respond(
3298 request,
3299 ResponsePayload::ProcessKilled(ProcessKilledResponse {
3300 process_id: payload.process_id,
3301 }),
3302 ),
3303 events: Vec::new(),
3304 })
3305 }
3306
3307 pub(crate) async fn find_listener(
3308 &mut self,
3309 request: &RequestFrame,
3310 payload: FindListenerRequest,
3311 ) -> Result<DispatchResult, SidecarError> {
3312 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3313 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3314 require_vm_inspection_permission(
3315 &self.bridge,
3316 &vm_id,
3317 "network.inspect",
3318 "network",
3319 &socket_query_resource(SocketQueryKind::TcpListener, &payload),
3320 )?;
3321
3322 let listener =
3323 find_socket_state_entry(self.vms.get(&vm_id), SocketQueryKind::TcpListener, &payload)?;
3324
3325 Ok(DispatchResult {
3326 response: self.respond(
3327 request,
3328 ResponsePayload::ListenerSnapshot(ListenerSnapshotResponse { listener }),
3329 ),
3330 events: Vec::new(),
3331 })
3332 }
3333
3334 pub(crate) async fn get_process_snapshot(
3335 &mut self,
3336 request: &RequestFrame,
3337 _payload: GetProcessSnapshotRequest,
3338 ) -> Result<DispatchResult, SidecarError> {
3339 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3340 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3341 require_vm_inspection_permission(
3342 &self.bridge,
3343 &vm_id,
3344 "process.inspect",
3345 "process",
3346 "process://snapshot",
3347 )?;
3348
3349 let processes = self
3350 .vms
3351 .get_mut(&vm_id)
3352 .map(|vm| {
3353 prune_exited_process_snapshots(vm);
3354 snapshot_vm_processes(vm)
3355 })
3356 .unwrap_or_default();
3357
3358 Ok(DispatchResult {
3359 response: self.respond(
3360 request,
3361 ResponsePayload::ProcessSnapshot(ProcessSnapshotResponse { processes }),
3362 ),
3363 events: Vec::new(),
3364 })
3365 }
3366
3367 pub(crate) async fn find_bound_udp(
3368 &mut self,
3369 request: &RequestFrame,
3370 payload: FindBoundUdpRequest,
3371 ) -> Result<DispatchResult, SidecarError> {
3372 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3373 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3374
3375 let lookup_request = FindListenerRequest {
3376 host: payload.host,
3377 port: payload.port,
3378 path: None,
3379 };
3380 require_vm_inspection_permission(
3381 &self.bridge,
3382 &vm_id,
3383 "network.inspect",
3384 "network",
3385 &socket_query_resource(SocketQueryKind::UdpBound, &lookup_request),
3386 )?;
3387 let socket = find_socket_state_entry(
3388 self.vms.get(&vm_id),
3389 SocketQueryKind::UdpBound,
3390 &lookup_request,
3391 )?;
3392
3393 Ok(DispatchResult {
3394 response: self.respond(
3395 request,
3396 ResponsePayload::BoundUdpSnapshot(BoundUdpSnapshotResponse { socket }),
3397 ),
3398 events: Vec::new(),
3399 })
3400 }
3401
3402 pub(crate) async fn vm_fetch(
3403 &mut self,
3404 request: &RequestFrame,
3405 payload: VmFetchRequest,
3406 ) -> Result<DispatchResult, SidecarError> {
3407 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3408 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3409
3410 let vm = self
3411 .vms
3412 .get_mut(&vm_id)
3413 .ok_or_else(|| SidecarError::InvalidState(String::from("unknown sidecar VM")))?;
3414 let target_path = if payload.path.starts_with('/') {
3415 payload.path.clone()
3416 } else {
3417 format!("/{}", payload.path)
3418 };
3419 let request_url = Url::parse(&format!("http://127.0.0.1:{}{target_path}", payload.port))
3420 .map_err(|error| {
3421 SidecarError::InvalidState(format!(
3422 "invalid vm.fetch target {target_path:?}: {error}"
3423 ))
3424 })?;
3425 let header_values: BTreeMap<String, Value> = serde_json::from_str(&payload.headers_json)
3426 .map_err(|error| {
3427 SidecarError::InvalidState(format!(
3428 "vm.fetch headers_json must be valid JSON: {error}"
3429 ))
3430 })?;
3431 let options = JavascriptHttpRequestOptions {
3432 method: Some(payload.method),
3433 headers: header_values,
3434 body: payload.body,
3435 reject_unauthorized: None,
3436 };
3437 let headers = parse_http_header_collection(&options.headers, "vm.fetch headers")?;
3438 let target_process_id = find_kernel_http_listener_process(vm, payload.port);
3439 if let Some(target_process_id) = target_process_id {
3440 let max_fetch_response_bytes = vm.limits.http.max_fetch_response_bytes;
3441 let response_json = match dispatch_kernel_http_fetch(
3442 &self.bridge,
3443 &vm_id,
3444 vm,
3445 &target_process_id,
3446 payload.port,
3447 &target_path,
3448 &options,
3449 &headers,
3450 max_fetch_response_bytes,
3451 ) {
3452 Ok(response_json) => response_json,
3453 Err(error) => {
3454 if let Some(exit_code) = kernel_http_fetch_target_exit_code(&error) {
3455 let _ = vm;
3456 self.finish_active_process_exit(&vm_id, &target_process_id, exit_code)?;
3457 }
3458 return Err(error);
3459 }
3460 };
3461 let response = self.respond(
3462 request,
3463 ResponsePayload::VmFetchResult(VmFetchResponse { response_json }),
3464 );
3465 ensure_vm_fetch_response_frame_within_limit(&response, self.config.max_frame_bytes)?;
3466
3467 return Ok(DispatchResult {
3468 response,
3469 events: Vec::new(),
3470 });
3471 }
3472
3473 let Some((target_process_id, server_id)) =
3474 vm.active_processes
3475 .iter()
3476 .find_map(|(process_id, process)| {
3477 process
3478 .http_servers
3479 .iter()
3480 .find(|(_, server)| server.guest_local_addr.port() == payload.port)
3481 .map(|(server_id, _)| (process_id.clone(), *server_id))
3482 })
3483 else {
3484 return Err(SidecarError::Execution(format!(
3485 "vm.fetch could not find a guest HTTP listener on port {}",
3486 payload.port
3487 )));
3488 };
3489 let socket_paths = build_javascript_socket_path_context(vm)?;
3490 let resource_limits = vm.kernel.resource_limits().clone();
3491 let process = vm
3492 .active_processes
3493 .get_mut(&target_process_id)
3494 .ok_or_else(|| {
3495 SidecarError::InvalidState(format!(
3496 "vm.fetch target process disappeared: {target_process_id}"
3497 ))
3498 })?;
3499 let request_json = serialize_http_loopback_request(&request_url, &options, &headers)?;
3500 let response_json = dispatch_loopback_http_request(LoopbackHttpDispatchRequest {
3501 bridge: &self.bridge,
3502 vm_id: &vm_id,
3503 dns: &vm.dns,
3504 socket_paths: &socket_paths,
3505 kernel: &mut vm.kernel,
3506 process,
3507 resource_limits: &resource_limits,
3508 server_id,
3509 request_json: &request_json,
3510 })?;
3511
3512 let response = self.respond(
3513 request,
3514 ResponsePayload::VmFetchResult(VmFetchResponse { response_json }),
3515 );
3516 ensure_vm_fetch_response_frame_within_limit(&response, self.config.max_frame_bytes)?;
3517
3518 Ok(DispatchResult {
3519 response,
3520 events: Vec::new(),
3521 })
3522 }
3523
3524 pub(crate) async fn get_signal_state(
3525 &mut self,
3526 request: &RequestFrame,
3527 payload: GetSignalStateRequest,
3528 ) -> Result<DispatchResult, SidecarError> {
3529 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3530 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3531
3532 let handlers = self
3533 .vms
3534 .get(&vm_id)
3535 .and_then(|vm| vm.signal_states.get(&payload.process_id))
3536 .cloned()
3537 .unwrap_or_default();
3538
3539 Ok(DispatchResult {
3540 response: self.respond(
3541 request,
3542 ResponsePayload::SignalState(SignalStateResponse {
3543 process_id: payload.process_id,
3544 handlers: handlers.into_iter().collect(),
3545 }),
3546 ),
3547 events: Vec::new(),
3548 })
3549 }
3550
3551 pub(crate) async fn get_zombie_timer_count(
3552 &mut self,
3553 request: &RequestFrame,
3554 _payload: GetZombieTimerCountRequest,
3555 ) -> Result<DispatchResult, SidecarError> {
3556 let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?;
3557 self.require_owned_vm(&connection_id, &session_id, &vm_id)?;
3558
3559 let count = self
3560 .vms
3561 .get(&vm_id)
3562 .map(|vm| vm.kernel.zombie_timer_count() as u64)
3563 .unwrap_or_default();
3564
3565 Ok(DispatchResult {
3566 response: self.respond(
3567 request,
3568 ResponsePayload::ZombieTimerCount(ZombieTimerCountResponse { count }),
3569 ),
3570 events: Vec::new(),
3571 })
3572 }
3573
3574 pub(crate) fn kill_process_internal(
3575 &mut self,
3576 vm_id: &str,
3577 process_id: &str,
3578 signal: &str,
3579 ) -> Result<(), SidecarError> {
3580 let signal_name = signal.to_owned();
3581 let signal = parse_signal(signal)?;
3582 let vm = self
3583 .vms
3584 .get_mut(vm_id)
3585 .ok_or_else(|| SidecarError::InvalidState(format!("unknown sidecar VM {vm_id}")))?;
3586 let process = vm.active_processes.get_mut(process_id).ok_or_else(|| {
3587 SidecarError::InvalidState(format!("VM {vm_id} has no active process {process_id}"))
3588 })?;
3589 let kernel_pid = process.kernel_pid;
3590
3591 enum KillBehavior {
3592 Tool,
3593 SharedV8StateOnly,
3594 SharedV8Continue,
3595 SharedV8Terminate,
3596 SharedV8DispatchOrTerminate,
3597 Noop,
3598 HostPid(u32),
3599 }
3600
3601 let behavior = match &process.execution {
3602 ActiveExecution::Tool(_) => KillBehavior::Tool,
3603 ActiveExecution::Javascript(execution)
3604 if execution.uses_shared_v8_runtime() && matches!(signal, 0 | libc::SIGSTOP) =>
3605 {
3606 KillBehavior::SharedV8StateOnly
3607 }
3608 ActiveExecution::Javascript(execution)
3609 if execution.uses_shared_v8_runtime() && signal == libc::SIGCONT =>
3610 {
3611 KillBehavior::SharedV8Continue
3612 }
3613 ActiveExecution::Wasm(execution)
3614 if execution.uses_shared_v8_runtime()
3615 && matches!(signal, 0 | libc::SIGSTOP | libc::SIGCONT) =>
3616 {
3617 KillBehavior::SharedV8StateOnly
3618 }
3619 ActiveExecution::Python(execution)
3620 if execution.uses_shared_v8_runtime()
3621 && matches!(signal, 0 | libc::SIGSTOP | libc::SIGCONT) =>
3622 {
3623 KillBehavior::SharedV8StateOnly
3624 }
3625 ActiveExecution::Javascript(execution)
3626 if execution.uses_shared_v8_runtime() && signal == SIGKILL =>
3627 {
3628 KillBehavior::SharedV8Terminate
3629 }
3630 ActiveExecution::Wasm(execution)
3631 if execution.uses_shared_v8_runtime() && signal == SIGKILL =>
3632 {
3633 KillBehavior::SharedV8Terminate
3634 }
3635 ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime() => {
3636 KillBehavior::SharedV8DispatchOrTerminate
3637 }
3638 ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime() => {
3639 KillBehavior::SharedV8Terminate
3640 }
3641 ActiveExecution::Python(execution) if execution.uses_shared_v8_runtime() => {
3642 KillBehavior::SharedV8Terminate
3643 }
3644 ActiveExecution::Javascript(execution) if execution.child_pid() == 0 => {
3645 KillBehavior::Noop
3646 }
3647 _ => KillBehavior::HostPid(process.execution.child_pid()),
3648 };
3649
3650 match behavior {
3651 KillBehavior::Tool => {
3652 let ActiveExecution::Tool(execution) = &process.execution else {
3653 unreachable!("kill behavior must match tool execution");
3654 };
3655 if signal != 0 {
3656 execution.cancelled.store(true, Ordering::Relaxed);
3657 process.queue_pending_execution_event(ActiveExecutionEvent::Exited(
3658 128 + signal,
3659 ))?;
3660 }
3661 }
3662 KillBehavior::SharedV8StateOnly => {
3663 if matches!(signal, libc::SIGSTOP | libc::SIGCONT) {
3664 vm.kernel
3665 .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal)
3666 .map_err(kernel_error)?;
3667 }
3668 }
3669 KillBehavior::SharedV8Continue => {
3670 vm.kernel
3671 .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal)
3672 .map_err(kernel_error)?;
3673 if signal != 0 && !dispatch_v8_process_signal(process, signal)? {
3674 process.execution.terminate()?;
3675 }
3676 }
3677 KillBehavior::SharedV8Terminate => {
3678 if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
3679 close_kernel_process_stdin(&mut vm.kernel, process)?;
3680 }
3681 process.execution.terminate()?;
3682 let needs_synthetic_exit = matches!(process.execution, ActiveExecution::Wasm(_))
3683 || (signal == SIGKILL
3684 && matches!(process.execution, ActiveExecution::Javascript(_)));
3685 if signal != 0 && needs_synthetic_exit {
3686 process.queue_pending_execution_event(ActiveExecutionEvent::Exited(
3687 128 + signal,
3688 ))?;
3689 }
3690 }
3691 KillBehavior::SharedV8DispatchOrTerminate => {
3692 if signal != 0 && !dispatch_v8_process_signal(process, signal)? {
3693 process.execution.terminate()?;
3694 }
3695 }
3696 KillBehavior::Noop => {}
3697 KillBehavior::HostPid(pid) => {
3698 if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
3699 close_kernel_process_stdin(&mut vm.kernel, process)?;
3700 }
3701 signal_runtime_process(pid, signal)?;
3702 }
3703 }
3704 emit_security_audit_event(
3705 &self.bridge,
3706 vm_id,
3707 "security.process.kill",
3708 audit_fields([
3709 (String::from("source"), String::from("control_plane")),
3710 (String::from("source_pid"), String::from("0")),
3711 (String::from("target_pid"), process.kernel_pid.to_string()),
3712 (String::from("process_id"), process_id.to_owned()),
3713 (String::from("signal"), signal_name),
3714 (
3715 String::from("host_pid"),
3716 process.execution.child_pid().to_string(),
3717 ),
3718 ]),
3719 );
3720 Ok(())
3721 }
3722
3723 pub async fn pump_process_events(
3724 &mut self,
3725 ownership: &OwnershipScope,
3726 ) -> Result<bool, SidecarError> {
3727 let mut emitted_any = false;
3728
3729 let mut queued_envelopes = Vec::new();
3730 {
3731 let pending_capacity = self.pending_process_event_capacity();
3732 let receiver = self.process_event_receiver.as_mut().ok_or_else(|| {
3733 SidecarError::InvalidState(String::from("process event receiver unavailable"))
3734 })?;
3735 loop {
3736 if queued_envelopes.len() >= pending_capacity {
3737 if receiver.is_empty() {
3738 break;
3739 }
3740 return Err(process_event_queue_overflow_error());
3741 }
3742 match receiver.try_recv() {
3743 Ok(envelope) => {
3744 queued_envelopes.push(envelope);
3745 emitted_any = true;
3746 }
3747 Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
3748 Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
3749 }
3750 }
3751 }
3752 for envelope in queued_envelopes {
3753 self.queue_pending_process_event(envelope)?;
3754 }
3755
3756 let vm_ids = self.vm_ids_for_scope(ownership)?;
3757 for vm_id in vm_ids {
3758 while let Some(vm) = self.vms.get(&vm_id) {
3759 let connection_id = vm.connection_id.clone();
3760 let session_id = vm.session_id.clone();
3761 let process_ids = self
3762 .vms
3763 .get(&vm_id)
3764 .map(|vm| vm.active_processes.keys().cloned().collect::<Vec<_>>())
3765 .unwrap_or_default();
3766 let mut emitted_this_pass = false;
3767
3768 for process_id in process_ids {
3769 if self
3770 .vms
3771 .get(&vm_id)
3772 .is_some_and(|vm| vm.detached_child_processes.contains(&process_id))
3773 {
3774 continue;
3775 }
3776 enum ProcessPollResult {
3777 Event(Box<Option<ActiveExecutionEvent>>),
3778 RecoverClosedChannel,
3779 }
3780 let poll_result = {
3781 let Some(vm) = self.vms.get_mut(&vm_id) else {
3782 continue;
3783 };
3784 let Some(process) = vm.active_processes.get_mut(&process_id) else {
3785 continue;
3786 };
3787 if let Some(event) = process.pending_execution_events.pop_front() {
3788 ProcessPollResult::Event(Box::new(Some(event)))
3789 } else {
3790 match process.execution.poll_event(Duration::ZERO).await {
3791 Ok(event) => ProcessPollResult::Event(Box::new(event)),
3792 Err(SidecarError::Execution(message))
3793 if (process.runtime == GuestRuntimeKind::JavaScript
3794 && closed_javascript_event_channel(&message))
3795 || (process.runtime == GuestRuntimeKind::Python
3796 && closed_python_event_channel(&message))
3797 || (process.runtime == GuestRuntimeKind::WebAssembly
3798 && closed_wasm_event_channel(&message)) =>
3799 {
3800 ProcessPollResult::RecoverClosedChannel
3801 }
3802 Err(other) => return Err(other),
3803 }
3804 }
3805 };
3806 let event = match poll_result {
3807 ProcessPollResult::Event(event) => *event,
3808 ProcessPollResult::RecoverClosedChannel => {
3809 self.recover_closed_root_runtime_process_event(&vm_id, &process_id)?
3810 }
3811 };
3812
3813 let Some(event) = event else {
3814 continue;
3815 };
3816
3817 if Self::internal_execution_event(&event) {
3818 self.handle_execution_event(&vm_id, &process_id, event)?;
3823 } else {
3824 self.queue_pending_process_event(ProcessEventEnvelope {
3825 connection_id: connection_id.clone(),
3826 session_id: session_id.clone(),
3827 vm_id: vm_id.clone(),
3828 process_id: process_id.clone(),
3829 event,
3830 })?;
3831 }
3832 emitted_any = true;
3833 emitted_this_pass = true;
3834 }
3835
3836 if !emitted_this_pass {
3837 break;
3838 }
3839 }
3840
3841 if self.pump_detached_child_process_events(&vm_id)? {
3842 emitted_any = true;
3843 }
3844 }
3845
3846 Ok(emitted_any)
3847 }
3848
3849 fn internal_execution_event(event: &ActiveExecutionEvent) -> bool {
3850 matches!(
3851 event,
3852 ActiveExecutionEvent::JavascriptSyncRpcRequest(_)
3853 | ActiveExecutionEvent::PythonVfsRpcRequest(_)
3854 | ActiveExecutionEvent::SignalState { .. }
3855 )
3856 }
3857
3858 fn recover_closed_root_runtime_process_event(
3859 &mut self,
3860 vm_id: &str,
3861 process_id: &str,
3862 ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
3863 let Some(vm) = self.vms.get_mut(vm_id) else {
3864 return Ok(None);
3865 };
3866 let Some(process) = vm.active_processes.get(process_id) else {
3867 return Ok(None);
3868 };
3869 if process.execution.uses_shared_v8_runtime() {
3870 return Ok(None);
3871 }
3872 if process.runtime != GuestRuntimeKind::JavaScript
3873 && process.runtime != GuestRuntimeKind::Python
3874 && process.runtime != GuestRuntimeKind::WebAssembly
3875 {
3876 return Ok(None);
3877 }
3878 let runtime_child_pid = process.execution.child_pid();
3879 if runtime_child_pid == 0 {
3880 return Ok(None);
3881 }
3882 if let Some(status) = runtime_child_exit_status(runtime_child_pid)? {
3883 return Ok(Some(ActiveExecutionEvent::Exited(status)));
3884 }
3885 if runtime_child_is_alive(runtime_child_pid)? {
3886 return Ok(None);
3887 }
3888 Ok(Some(ActiveExecutionEvent::Exited(0)))
3889 }
3890
3891 fn active_process_by_path<'a>(
3892 process: &'a ActiveProcess,
3893 child_path: &[&str],
3894 ) -> Option<&'a ActiveProcess> {
3895 let mut current = process;
3896 for child_id in child_path {
3897 current = current.child_processes.get(*child_id)?;
3898 }
3899 Some(current)
3900 }
3901
3902 fn active_process_by_path_mut<'a>(
3903 process: &'a mut ActiveProcess,
3904 child_path: &[&str],
3905 ) -> Option<&'a mut ActiveProcess> {
3906 let mut current = process;
3907 for child_id in child_path {
3908 current = current.child_processes.get_mut(*child_id)?;
3909 }
3910 Some(current)
3911 }
3912
3913 fn active_process_by_owned_path_mut<'a>(
3914 process: &'a mut ActiveProcess,
3915 child_path: &[String],
3916 ) -> Option<&'a mut ActiveProcess> {
3917 let mut current = process;
3918 for child_id in child_path {
3919 current = current.child_processes.get_mut(child_id)?;
3920 }
3921 Some(current)
3922 }
3923
3924 fn active_process_path_by_kernel_pid(
3925 process: &ActiveProcess,
3926 kernel_pid: u32,
3927 ) -> Option<Vec<String>> {
3928 if process.kernel_pid == kernel_pid {
3929 return Some(Vec::new());
3930 }
3931
3932 for (child_id, child) in &process.child_processes {
3933 let Some(mut path) = Self::active_process_path_by_kernel_pid(child, kernel_pid) else {
3934 continue;
3935 };
3936 path.insert(0, child_id.clone());
3937 return Some(path);
3938 }
3939
3940 None
3941 }
3942
3943 fn descendant_parent_process<'a>(
3944 vm: &'a VmState,
3945 process_id: &str,
3946 child_path: &[&str],
3947 ) -> Option<&'a ActiveProcess> {
3948 let root = vm.active_processes.get(process_id)?;
3949 Self::active_process_by_path(root, child_path)
3950 }
3951
3952 fn descendant_parent_process_mut<'a>(
3953 vm: &'a mut VmState,
3954 process_id: &str,
3955 child_path: &[&str],
3956 ) -> Option<&'a mut ActiveProcess> {
3957 let root = vm.active_processes.get_mut(process_id)?;
3958 Self::active_process_by_path_mut(root, child_path)
3959 }
3960
3961 fn child_process_path_label(process_id: &str, child_path: &[&str]) -> String {
3962 if child_path.is_empty() {
3963 process_id.to_owned()
3964 } else {
3965 format!("{process_id}/{}", child_path.join("/"))
3966 }
3967 }
3968
3969 fn adopt_detached_child_processes(
3970 current_process_id: &str,
3971 process: &mut ActiveProcess,
3972 ) -> Vec<(String, ActiveProcess)> {
3973 let mut adopted = Vec::new();
3974 let child_ids = process.child_processes.keys().cloned().collect::<Vec<_>>();
3975 for child_id in child_ids {
3976 let child_process_id = format!("{current_process_id}/{child_id}");
3977 let Some(mut child) = process.child_processes.remove(&child_id) else {
3978 continue;
3979 };
3980 if child.detached {
3981 adopted.push((child_process_id, child));
3982 continue;
3983 }
3984
3985 adopted.extend(Self::adopt_detached_child_processes(
3986 &child_process_id,
3987 &mut child,
3988 ));
3989 process.child_processes.insert(child_id, child);
3990 }
3991 adopted
3992 }
3993
3994 fn child_process_signal_key<'a>(process_id: &'a str, child_path: &[&'a str]) -> &'a str {
3995 child_path.last().copied().unwrap_or(process_id)
3996 }
3997
3998 fn resolve_detached_child_process_path(
3999 vm: &VmState,
4000 detached_process_id: &str,
4001 ) -> Option<(String, Vec<String>)> {
4002 let root_process_id = vm
4003 .active_processes
4004 .keys()
4005 .filter(|candidate| {
4006 detached_process_id == candidate.as_str()
4007 || detached_process_id
4008 .strip_prefix(candidate.as_str())
4009 .is_some_and(|remainder| remainder.starts_with('/'))
4010 })
4011 .max_by_key(|candidate| candidate.len())?
4012 .clone();
4013
4014 let remainder = detached_process_id
4015 .strip_prefix(root_process_id.as_str())
4016 .unwrap_or_default();
4017 if remainder.is_empty() {
4018 return Some((root_process_id, Vec::new()));
4019 }
4020
4021 Some((
4022 root_process_id,
4023 remainder
4024 .trim_start_matches('/')
4025 .split('/')
4026 .map(str::to_owned)
4027 .collect(),
4028 ))
4029 }
4030
4031 fn pump_detached_child_process_events(&mut self, vm_id: &str) -> Result<bool, SidecarError> {
4032 let detached_process_ids = self
4033 .vms
4034 .get(vm_id)
4035 .map(|vm| {
4036 vm.detached_child_processes
4037 .iter()
4038 .cloned()
4039 .collect::<Vec<_>>()
4040 })
4041 .unwrap_or_default();
4042 let mut emitted_any = false;
4043 for detached_process_id in detached_process_ids {
4044 let Some((root_process_id, child_path)) = self
4045 .vms
4046 .get(vm_id)
4047 .and_then(|vm| Self::resolve_detached_child_process_path(vm, &detached_process_id))
4048 else {
4049 if let Some(vm) = self.vms.get_mut(vm_id) {
4050 vm.detached_child_processes.remove(&detached_process_id);
4051 }
4052 continue;
4053 };
4054 if child_path.is_empty() {
4055 loop {
4056 enum ProcessPollResult {
4057 Event(Box<Option<ActiveExecutionEvent>>),
4058 RecoverClosedChannel,
4059 }
4060 let poll_result = {
4061 let Some(vm) = self.vms.get_mut(vm_id) else {
4062 break;
4063 };
4064 let Some(process) = vm.active_processes.get_mut(&root_process_id) else {
4065 break;
4066 };
4067 if let Some(event) = process.pending_execution_events.pop_front() {
4068 ProcessPollResult::Event(Box::new(Some(event)))
4069 } else {
4070 match process.execution.poll_event_blocking(Duration::ZERO) {
4071 Ok(event) => ProcessPollResult::Event(Box::new(event)),
4072 Err(SidecarError::Execution(message))
4073 if (process.runtime == GuestRuntimeKind::JavaScript
4074 && closed_javascript_event_channel(&message))
4075 || (process.runtime == GuestRuntimeKind::Python
4076 && closed_python_event_channel(&message))
4077 || (process.runtime == GuestRuntimeKind::WebAssembly
4078 && closed_wasm_event_channel(&message)) =>
4079 {
4080 ProcessPollResult::RecoverClosedChannel
4081 }
4082 Err(error) => return Err(error),
4083 }
4084 }
4085 };
4086 let event = match poll_result {
4087 ProcessPollResult::Event(event) => *event,
4088 ProcessPollResult::RecoverClosedChannel => {
4089 self.recover_closed_root_runtime_process_event(vm_id, &root_process_id)?
4090 }
4091 };
4092 let Some(event) = event else {
4093 break;
4094 };
4095 let Some((connection_id, session_id)) = self
4096 .vms
4097 .get(vm_id)
4098 .map(|vm| (vm.connection_id.clone(), vm.session_id.clone()))
4099 else {
4100 break;
4101 };
4102 match event {
4103 ActiveExecutionEvent::Stdout(chunk) => {
4104 self.queue_pending_process_event(ProcessEventEnvelope {
4105 connection_id,
4106 session_id,
4107 vm_id: vm_id.to_owned(),
4108 process_id: detached_process_id.clone(),
4109 event: ActiveExecutionEvent::Stdout(chunk),
4110 })?;
4111 emitted_any = true;
4112 }
4113 ActiveExecutionEvent::Stderr(chunk) => {
4114 self.queue_pending_process_event(ProcessEventEnvelope {
4115 connection_id,
4116 session_id,
4117 vm_id: vm_id.to_owned(),
4118 process_id: detached_process_id.clone(),
4119 event: ActiveExecutionEvent::Stderr(chunk),
4120 })?;
4121 emitted_any = true;
4122 }
4123 ActiveExecutionEvent::Exited(exit_code) => {
4124 if let Some(vm) = self.vms.get_mut(vm_id) {
4125 vm.detached_child_processes.remove(&detached_process_id);
4126 }
4127 self.queue_pending_process_event(ProcessEventEnvelope {
4128 connection_id,
4129 session_id,
4130 vm_id: vm_id.to_owned(),
4131 process_id: detached_process_id.clone(),
4132 event: ActiveExecutionEvent::Exited(exit_code),
4133 })?;
4134 emitted_any = true;
4135 break;
4136 }
4137 ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
4138 self.handle_javascript_sync_rpc_request(
4139 vm_id,
4140 &root_process_id,
4141 request,
4142 )?;
4143 }
4144 ActiveExecutionEvent::PythonVfsRpcRequest(request) => {
4145 self.handle_python_vfs_rpc_request(vm_id, &root_process_id, *request)?;
4146 }
4147 ActiveExecutionEvent::SignalState {
4148 signal,
4149 registration,
4150 } => {
4151 if let Some(vm) = self.vms.get_mut(vm_id) {
4152 vm.signal_states
4153 .entry(root_process_id.clone())
4154 .or_default()
4155 .insert(signal, registration);
4156 }
4157 }
4158 }
4159 }
4160 continue;
4161 }
4162
4163 let parent_path = child_path[..child_path.len() - 1]
4164 .iter()
4165 .map(String::as_str)
4166 .collect::<Vec<_>>();
4167 let child_process_id = child_path.last().expect("child path cannot be empty");
4168
4169 loop {
4170 let event = match self.poll_descendant_javascript_child_process(
4171 vm_id,
4172 &root_process_id,
4173 &parent_path,
4174 child_process_id,
4175 0,
4176 ) {
4177 Ok(event) => event,
4178 Err(SidecarError::InvalidState(message))
4179 if message.contains("unknown child process")
4180 || message.contains("unknown child process path") =>
4181 {
4182 if let Some(vm) = self.vms.get_mut(vm_id) {
4183 vm.detached_child_processes.remove(&detached_process_id);
4184 }
4185 break;
4186 }
4187 Err(error) if is_javascript_child_process_gone_error(&error) => {
4188 if let Some(vm) = self.vms.get_mut(vm_id) {
4189 vm.detached_child_processes.remove(&detached_process_id);
4190 }
4191 break;
4192 }
4193 Err(error) => return Err(error),
4194 };
4195
4196 let Some(event_type) = event.get("type").and_then(Value::as_str) else {
4197 break;
4198 };
4199 let Some((connection_id, session_id)) = self
4200 .vms
4201 .get(vm_id)
4202 .map(|vm| (vm.connection_id.clone(), vm.session_id.clone()))
4203 else {
4204 break;
4205 };
4206
4207 let envelope = match event_type {
4208 "stdout" => Some(ProcessEventEnvelope {
4209 connection_id: connection_id.clone(),
4210 session_id: session_id.clone(),
4211 vm_id: vm_id.to_owned(),
4212 process_id: detached_process_id.clone(),
4213 event: ActiveExecutionEvent::Stdout(javascript_sync_rpc_bytes_arg(
4214 &[event.get("data").cloned().unwrap_or(Value::Null)],
4215 0,
4216 "detached child_process stdout",
4217 )?),
4218 }),
4219 "stderr" => Some(ProcessEventEnvelope {
4220 connection_id: connection_id.clone(),
4221 session_id: session_id.clone(),
4222 vm_id: vm_id.to_owned(),
4223 process_id: detached_process_id.clone(),
4224 event: ActiveExecutionEvent::Stderr(javascript_sync_rpc_bytes_arg(
4225 &[event.get("data").cloned().unwrap_or(Value::Null)],
4226 0,
4227 "detached child_process stderr",
4228 )?),
4229 }),
4230 "exit" => {
4231 if let Some(vm) = self.vms.get_mut(vm_id) {
4232 vm.detached_child_processes.remove(&detached_process_id);
4233 }
4234 Some(ProcessEventEnvelope {
4235 connection_id,
4236 session_id,
4237 vm_id: vm_id.to_owned(),
4238 process_id: detached_process_id.clone(),
4239 event: ActiveExecutionEvent::Exited(
4240 event
4241 .get("exitCode")
4242 .and_then(Value::as_i64)
4243 .map(|value| value as i32)
4244 .unwrap_or(1),
4245 ),
4246 })
4247 }
4248 _ => None,
4249 };
4250
4251 let Some(envelope) = envelope else {
4252 break;
4253 };
4254 self.queue_pending_process_event(envelope)?;
4255 emitted_any = true;
4256
4257 if event_type == "exit" {
4258 break;
4259 }
4260 }
4261 }
4262
4263 Ok(emitted_any)
4264 }
4265 pub(crate) fn drain_queued_descendant_javascript_child_process_events(
4266 &mut self,
4267 vm_id: &str,
4268 process_id: &str,
4269 child_path: &[&str],
4270 ) -> Result<(), SidecarError> {
4271 if child_path.is_empty() {
4272 return Ok(());
4273 }
4274 let target_process_id = Self::child_process_path_label(process_id, child_path);
4275 let mut child_capacity = self
4276 .vms
4277 .get(vm_id)
4278 .and_then(|vm| vm.active_processes.get(process_id))
4279 .and_then(|root| descendant_pending_execution_event_capacity(root, child_path));
4280
4281 let mut deferred = VecDeque::new();
4282 while let Some(envelope) = self.pending_process_events.pop_front() {
4283 if envelope.vm_id == vm_id && envelope.process_id == target_process_id {
4284 if matches!(child_capacity, Some(0)) {
4285 self.pending_process_events.push_front(envelope);
4286 while let Some(deferred_envelope) = deferred.pop_back() {
4287 self.pending_process_events.push_front(deferred_envelope);
4288 }
4289 return Err(process_event_queue_overflow_error());
4290 }
4291 if let Some(vm) = self.vms.get_mut(vm_id) {
4292 if let Some(root) = vm.active_processes.get_mut(process_id) {
4293 if let Some(child) = Self::active_process_by_path_mut(root, child_path) {
4294 child.queue_pending_execution_event(envelope.event)?;
4295 child_capacity = child_capacity.map(|capacity| capacity - 1);
4296 continue;
4297 }
4298 }
4299 }
4300 }
4301 deferred.push_back(envelope);
4302 }
4303 self.pending_process_events = deferred;
4304
4305 let mut queued = Vec::new();
4306 {
4307 let transfer_capacity = self
4308 .pending_process_event_capacity()
4309 .min(child_capacity.unwrap_or(usize::MAX));
4310 let receiver = self.process_event_receiver.as_mut().ok_or_else(|| {
4311 SidecarError::InvalidState(String::from("process event receiver unavailable"))
4312 })?;
4313 loop {
4314 if queued.len() >= transfer_capacity {
4315 if receiver.is_empty() {
4316 break;
4317 }
4318 return Err(process_event_queue_overflow_error());
4319 }
4320 match receiver.try_recv() {
4321 Ok(envelope) => queued.push(envelope),
4322 Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
4323 Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
4324 }
4325 }
4326 }
4327 for envelope in queued {
4328 if envelope.vm_id == vm_id && envelope.process_id == target_process_id {
4329 if let Some(vm) = self.vms.get_mut(vm_id) {
4330 if let Some(root) = vm.active_processes.get_mut(process_id) {
4331 if let Some(child) = Self::active_process_by_path_mut(root, child_path) {
4332 child.queue_pending_execution_event(envelope.event)?;
4333 continue;
4334 }
4335 }
4336 }
4337 }
4338 self.queue_pending_process_event(envelope)?;
4339 }
4340
4341 Ok(())
4342 }
4343
4344 pub(crate) fn handle_execution_event(
4345 &mut self,
4346 vm_id: &str,
4347 process_id: &str,
4348 event: ActiveExecutionEvent,
4349 ) -> Result<Option<EventFrame>, SidecarError> {
4350 let Some(vm) = self.vms.get(vm_id) else {
4351 log_stale_process_event(&self.bridge, vm_id, process_id, "execution event dispatch");
4352 return Ok(None);
4353 };
4354 if !vm.active_processes.contains_key(process_id) {
4355 log_stale_process_event(&self.bridge, vm_id, process_id, "execution event dispatch");
4356 return Ok(None);
4357 }
4358 let (connection_id, session_id) = { (vm.connection_id.clone(), vm.session_id.clone()) };
4359 let ownership = OwnershipScope::vm(&connection_id, &session_id, vm_id);
4360
4361 if self.capture_extension_process_output_event(vm_id, process_id, &event) {
4362 return Ok(None);
4363 }
4364
4365 match event {
4366 ActiveExecutionEvent::Stdout(chunk) => Ok(Some(EventFrame::new(
4367 ownership,
4368 EventPayload::ProcessOutput(ProcessOutputEvent {
4369 process_id: process_id.to_owned(),
4370 channel: StreamChannel::Stdout,
4371 chunk,
4372 }),
4373 ))),
4374 ActiveExecutionEvent::Stderr(chunk) => Ok(Some(EventFrame::new(
4375 ownership,
4376 EventPayload::ProcessOutput(ProcessOutputEvent {
4377 process_id: process_id.to_owned(),
4378 channel: StreamChannel::Stderr,
4379 chunk,
4380 }),
4381 ))),
4382 ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
4383 self.handle_javascript_sync_rpc_request(vm_id, process_id, request)?;
4384 Ok(None)
4385 }
4386 ActiveExecutionEvent::PythonVfsRpcRequest(request) => {
4387 self.handle_python_vfs_rpc_request(vm_id, process_id, *request)?;
4388 Ok(None)
4389 }
4390 ActiveExecutionEvent::SignalState {
4391 signal,
4392 registration,
4393 } => {
4394 let Some(vm) = self.vms.get_mut(vm_id) else {
4395 return Ok(None);
4396 };
4397 if !vm.active_processes.contains_key(process_id) {
4398 return Ok(None);
4399 }
4400 vm.signal_states
4401 .entry(process_id.to_owned())
4402 .or_default()
4403 .insert(signal, registration);
4404 Ok(None)
4405 }
4406 ActiveExecutionEvent::Exited(exit_code) => {
4407 let became_idle = self
4408 .finish_active_process_exit(vm_id, process_id, exit_code)?
4409 .unwrap_or(false);
4410
4411 if became_idle {
4412 self.bridge.emit_lifecycle(vm_id, LifecycleState::Ready)?;
4413 }
4414
4415 Ok(Some(EventFrame::new(
4416 ownership,
4417 EventPayload::ProcessExited(ProcessExitedEvent {
4418 process_id: process_id.to_owned(),
4419 exit_code,
4420 }),
4421 )))
4422 }
4423 }
4424 }
4425
4426 pub(crate) fn finish_active_process_exit(
4427 &mut self,
4428 vm_id: &str,
4429 process_id: &str,
4430 exit_code: i32,
4431 ) -> Result<Option<bool>, SidecarError> {
4432 let Some(vm) = self.vms.get_mut(vm_id) else {
4433 log_stale_process_event(&self.bridge, vm_id, process_id, "process exit cleanup");
4434 return Ok(None);
4435 };
4436 if !vm.active_processes.contains_key(process_id) {
4437 log_stale_process_event(&self.bridge, vm_id, process_id, "process exit cleanup");
4438 return Ok(None);
4439 }
4440
4441 prune_exited_process_snapshots(vm);
4442 let process_table = vm.kernel.list_processes();
4443 let Some(mut process) = vm.active_processes.remove(process_id) else {
4444 return Ok(None);
4445 };
4446 if let Some(info) = process_table.get(&process.kernel_pid) {
4447 vm.exited_process_snapshots
4448 .push_back(ExitedProcessSnapshot {
4449 captured_at: Instant::now(),
4450 process: build_process_snapshot_entry(
4451 process_id,
4452 &process,
4453 info,
4454 Some(exit_code),
4455 ),
4456 });
4457 }
4458 let detached_children = Self::adopt_detached_child_processes(process_id, &mut process);
4459 sync_process_host_writes_to_kernel(vm, &process)?;
4460 terminate_child_process_tree(&mut vm.kernel, &mut process);
4461 process.kernel_handle.finish(exit_code);
4462 let _ = vm.kernel.wait_and_reap(process.kernel_pid);
4463 vm.signal_states.remove(process_id);
4464 for (detached_process_id, detached_child) in detached_children {
4465 vm.detached_child_processes
4466 .insert(detached_process_id.clone());
4467 vm.active_processes
4468 .insert(detached_process_id, detached_child);
4469 }
4470 let became_idle = vm.active_processes.is_empty();
4471 self.prune_extension_process_resource(process_id);
4472
4473 Ok(Some(became_idle))
4474 }
4475
4476 pub(crate) fn drain_process_events_blocking_with_limit(
4477 &mut self,
4478 vm_id: &str,
4479 process_id: &str,
4480 max_events: usize,
4481 ) -> Result<Vec<ActiveExecutionEvent>, SidecarError> {
4482 let mut events = Vec::new();
4483 if max_events == 0 {
4484 return Ok(events);
4485 }
4486 let mut deadline = Instant::now() + Duration::from_millis(150);
4487
4488 loop {
4489 if events.len() >= max_events {
4490 break;
4491 }
4492 let event = {
4493 let Some(vm) = self.vms.get_mut(vm_id) else {
4494 break;
4495 };
4496 let Some(process) = vm.active_processes.get_mut(process_id) else {
4497 break;
4498 };
4499 if let Some(event) = process.pending_execution_events.pop_front() {
4500 Some(event)
4501 } else {
4502 match process.execution.poll_event_blocking(Duration::ZERO) {
4503 Ok(event) => event,
4504 Err(SidecarError::Execution(_)) => None,
4505 Err(other) => return Err(other),
4506 }
4507 }
4508 };
4509
4510 let Some(event) = event else {
4511 if Instant::now() >= deadline {
4512 break;
4513 }
4514 let blocking_wait = deadline.saturating_duration_since(Instant::now());
4515 if blocking_wait.is_zero() {
4516 break;
4517 }
4518 if events.len() >= max_events {
4519 break;
4520 }
4521 let delayed_event = {
4522 let Some(vm) = self.vms.get_mut(vm_id) else {
4523 break;
4524 };
4525 let Some(process) = vm.active_processes.get_mut(process_id) else {
4526 break;
4527 };
4528 if let Some(event) = process.pending_execution_events.pop_front() {
4529 Some(event)
4530 } else {
4531 match process.execution.poll_event_blocking(blocking_wait) {
4532 Ok(event) => event,
4533 Err(SidecarError::Execution(_)) => None,
4534 Err(other) => return Err(other),
4535 }
4536 }
4537 };
4538 let Some(event) = delayed_event else {
4539 break;
4540 };
4541 events.push(event);
4542 deadline = Instant::now() + Duration::from_millis(150);
4543 continue;
4544 };
4545 events.push(event);
4546 deadline = Instant::now() + Duration::from_millis(150);
4547 }
4548
4549 Ok(events)
4550 }
4551
4552 pub(crate) fn handle_python_vfs_rpc_request(
4553 &mut self,
4554 vm_id: &str,
4555 process_id: &str,
4556 request: PythonVfsRpcRequest,
4557 ) -> Result<(), SidecarError> {
4558 match request.method {
4559 PythonVfsRpcMethod::Read
4560 | PythonVfsRpcMethod::Write
4561 | PythonVfsRpcMethod::Stat
4562 | PythonVfsRpcMethod::ReadDir
4563 | PythonVfsRpcMethod::Mkdir => {
4564 filesystem_handle_python_vfs_rpc_request(self, vm_id, process_id, request)
4565 }
4566 PythonVfsRpcMethod::HttpRequest => {
4567 self.handle_python_http_rpc_request(vm_id, process_id, request)
4568 }
4569 PythonVfsRpcMethod::DnsLookup => {
4570 self.handle_python_dns_rpc_request(vm_id, process_id, request)
4571 }
4572 PythonVfsRpcMethod::SubprocessRun => {
4573 self.handle_python_subprocess_rpc_request(vm_id, process_id, request)
4574 }
4575 }
4576 }
4577
4578 fn handle_python_http_rpc_request(
4579 &mut self,
4580 vm_id: &str,
4581 process_id: &str,
4582 request: PythonVfsRpcRequest,
4583 ) -> Result<(), SidecarError> {
4584 let Some(vm) = self.vms.get(vm_id) else {
4585 return Ok(());
4586 };
4587 if !vm.active_processes.contains_key(process_id) {
4588 return Ok(());
4589 }
4590 let response = (|| {
4591 let url_text = request.url.as_deref().ok_or_else(|| {
4592 SidecarError::InvalidState(String::from("python httpRequest requires a url"))
4593 })?;
4594 let url = Url::parse(url_text)
4595 .map_err(|error| SidecarError::Execution(format!("ERR_INVALID_URL: {error}")))?;
4596 let host = url.host_str().ok_or_else(|| {
4597 SidecarError::Execution(String::from("ERR_INVALID_URL: missing host"))
4598 })?;
4599 let port = url.port_or_known_default().ok_or_else(|| {
4600 SidecarError::Execution(String::from("ERR_INVALID_URL: missing port"))
4601 })?;
4602 self.bridge.require_network_access(
4603 vm_id,
4604 NetworkOperation::Http,
4605 format_tcp_resource(host, port),
4606 )?;
4607 let pinned_addresses = if let Ok(literal_ip) = host.parse::<IpAddr>() {
4614 filter_dns_safe_ip_addrs(vec![literal_ip], host)?
4615 } else {
4616 filter_dns_safe_ip_addrs(
4617 resolve_dns_ip_addrs(
4618 &self.bridge,
4619 &vm.kernel,
4620 vm_id,
4621 &vm.dns,
4622 host,
4623 DnsLookupPolicy::SkipPermissions,
4624 )?,
4625 host,
4626 )?
4627 };
4628 let mut headers = BTreeMap::new();
4629 for (name, value) in &request.headers {
4630 headers.insert(name.clone(), Value::String(value.clone()));
4631 }
4632 let options = JavascriptHttpRequestOptions {
4633 method: Some(
4634 request
4635 .http_method
4636 .clone()
4637 .unwrap_or_else(|| String::from("GET")),
4638 ),
4639 headers,
4640 body: request.body_base64.as_deref().map(|body| {
4641 String::from_utf8(
4642 base64::engine::general_purpose::STANDARD
4643 .decode(body)
4644 .unwrap_or_default(),
4645 )
4646 .unwrap_or_default()
4647 }),
4648 reject_unauthorized: None,
4649 };
4650 let headers =
4651 parse_http_header_collection(&options.headers, "python httpRequest headers")?;
4652 let response =
4653 issue_outbound_http_request(&url, &options, &headers, &pinned_addresses)?;
4654 let payload_json = response.as_str().ok_or_else(|| {
4655 SidecarError::Execution(String::from(
4656 "python httpRequest returned a non-string response payload",
4657 ))
4658 })?;
4659 let payload: Value = serde_json::from_str(payload_json).map_err(|error| {
4660 SidecarError::Execution(format!(
4661 "python httpRequest response must be valid JSON: {error}"
4662 ))
4663 })?;
4664 let header_map = payload
4665 .get("headers")
4666 .and_then(Value::as_array)
4667 .map(|entries| {
4668 let mut normalized = BTreeMap::<String, Vec<String>>::new();
4669 for entry in entries {
4670 let Some(pair) = entry.as_array() else {
4671 continue;
4672 };
4673 let Some(name) = pair.first().and_then(Value::as_str) else {
4674 continue;
4675 };
4676 let Some(value) = pair.get(1).and_then(Value::as_str) else {
4677 continue;
4678 };
4679 normalized
4680 .entry(name.to_owned())
4681 .or_default()
4682 .push(value.to_owned());
4683 }
4684 normalized
4685 })
4686 .unwrap_or_default();
4687 Ok(PythonVfsRpcResponsePayload::Http {
4688 status: payload
4689 .get("status")
4690 .and_then(Value::as_u64)
4691 .map(|value| value as u16)
4692 .unwrap_or_default(),
4693 reason: payload
4694 .get("statusText")
4695 .and_then(Value::as_str)
4696 .unwrap_or_default()
4697 .to_owned(),
4698 url: payload
4699 .get("url")
4700 .and_then(Value::as_str)
4701 .unwrap_or(url_text)
4702 .to_owned(),
4703 headers: header_map,
4704 body_base64: payload
4705 .get("body")
4706 .and_then(Value::as_str)
4707 .unwrap_or_default()
4708 .to_owned(),
4709 })
4710 })();
4711
4712 self.respond_python_rpc(vm_id, process_id, request.id, response)
4713 }
4714
4715 fn handle_python_dns_rpc_request(
4716 &mut self,
4717 vm_id: &str,
4718 process_id: &str,
4719 request: PythonVfsRpcRequest,
4720 ) -> Result<(), SidecarError> {
4721 let Some(vm) = self.vms.get(vm_id) else {
4722 return Ok(());
4723 };
4724 if !vm.active_processes.contains_key(process_id) {
4725 return Ok(());
4726 }
4727 let response = (|| {
4728 let hostname = request.hostname.as_deref().ok_or_else(|| {
4729 SidecarError::InvalidState(String::from("python dnsLookup requires a hostname"))
4730 })?;
4731 let mut addresses = filter_dns_safe_ip_addrs(
4732 resolve_dns_ip_addrs(
4733 &self.bridge,
4734 &vm.kernel,
4735 vm_id,
4736 &vm.dns,
4737 hostname,
4738 DnsLookupPolicy::CheckPermissions,
4739 )?,
4740 hostname,
4741 )?;
4742 if let Some(family) = request.family {
4743 addresses.retain(|address| {
4744 matches!((family, address), (4, IpAddr::V4(_)) | (6, IpAddr::V6(_)))
4745 });
4746 }
4747 Ok(PythonVfsRpcResponsePayload::DnsLookup {
4748 addresses: addresses
4749 .into_iter()
4750 .map(|address| address.to_string())
4751 .collect(),
4752 })
4753 })();
4754
4755 self.respond_python_rpc(vm_id, process_id, request.id, response)
4756 }
4757
4758 fn handle_python_subprocess_rpc_request(
4759 &mut self,
4760 vm_id: &str,
4761 process_id: &str,
4762 request: PythonVfsRpcRequest,
4763 ) -> Result<(), SidecarError> {
4764 let command = request.command.clone().ok_or_else(|| {
4765 SidecarError::InvalidState(String::from("python subprocessRun requires a command"))
4766 })?;
4767 let (internal_bootstrap_env, cwd) = {
4768 let Some(vm) = self.vms.get(vm_id) else {
4769 return Ok(());
4770 };
4771 let Some(process) = vm.active_processes.get(process_id) else {
4772 return Ok(());
4773 };
4774 let virtual_home = guest_virtual_home(vm);
4775 let cwd = request.cwd.clone().or_else(|| {
4776 guest_runtime_path_for_host_path(
4777 &vm.guest_env,
4778 &virtual_home,
4779 &vm.host_cwd,
4780 &process.host_cwd.to_string_lossy(),
4781 )
4782 });
4783 (
4784 sanitize_javascript_child_process_internal_bootstrap_env(&vm.guest_env),
4785 cwd,
4786 )
4787 };
4788 let response = self
4789 .spawn_javascript_child_process_sync(
4790 vm_id,
4791 process_id,
4792 JavascriptChildProcessSpawnRequest {
4793 command,
4794 args: request.args.clone(),
4795 options: JavascriptChildProcessSpawnOptions {
4796 cwd,
4797 env: request.env.clone(),
4798 input: None,
4799 internal_bootstrap_env,
4800 shell: request.shell,
4801 detached: false,
4802 stdio: vec![
4803 String::from("pipe"),
4804 String::from("pipe"),
4805 String::from("pipe"),
4806 ],
4807 timeout: None,
4808 kill_signal: None,
4809 },
4810 },
4811 request.max_buffer,
4812 )
4813 .map(|payload| PythonVfsRpcResponsePayload::SubprocessRun {
4814 exit_code: payload
4815 .get("code")
4816 .and_then(Value::as_i64)
4817 .map(|value| value as i32)
4818 .unwrap_or(1),
4819 stdout: payload
4820 .get("stdout")
4821 .and_then(Value::as_str)
4822 .unwrap_or_default()
4823 .to_owned(),
4824 stderr: payload
4825 .get("stderr")
4826 .and_then(Value::as_str)
4827 .unwrap_or_default()
4828 .to_owned(),
4829 max_buffer_exceeded: payload
4830 .get("maxBufferExceeded")
4831 .and_then(Value::as_bool)
4832 .unwrap_or(false),
4833 });
4834
4835 self.respond_python_rpc(vm_id, process_id, request.id, response)
4836 }
4837
4838 fn respond_python_rpc(
4839 &mut self,
4840 vm_id: &str,
4841 process_id: &str,
4842 request_id: u64,
4843 response: Result<PythonVfsRpcResponsePayload, SidecarError>,
4844 ) -> Result<(), SidecarError> {
4845 let Some(vm) = self.vms.get_mut(vm_id) else {
4846 return Ok(());
4847 };
4848 let Some(process) = vm.active_processes.get_mut(process_id) else {
4849 return Ok(());
4850 };
4851 let result = match response {
4852 Ok(payload) => process
4853 .execution
4854 .respond_python_vfs_rpc_success(request_id, payload),
4855 Err(error) => process.execution.respond_python_vfs_rpc_error(
4856 request_id,
4857 "ERR_AGENTOS_PYTHON_VFS_RPC",
4858 error.to_string(),
4859 ),
4860 };
4861 match result {
4862 Ok(()) => Ok(()),
4863 Err(error) if is_broken_pipe_error(&error) => Ok(()),
4864 Err(error) => Err(error),
4865 }
4866 }
4867
4868 pub(crate) fn resolve_javascript_child_process_execution(
4869 &self,
4870 vm: &VmState,
4871 parent_env: &BTreeMap<String, String>,
4872 parent_guest_cwd: &str,
4873 parent_host_cwd: &Path,
4874 request: &JavascriptChildProcessSpawnRequest,
4875 ) -> Result<ResolvedChildProcessExecution, SidecarError> {
4876 let mut runtime_env = parent_env.clone();
4877 runtime_env.extend(request.options.internal_bootstrap_env.clone());
4878 let (guest_cwd, host_cwd_override) = request
4879 .options
4880 .cwd
4881 .as_deref()
4882 .map(|cwd| {
4883 let normalized_parent_host_cwd = normalize_host_path(parent_host_cwd);
4884 let requested_host_cwd = normalize_host_path(Path::new(cwd));
4885 if path_is_within_root(&requested_host_cwd, &normalized_parent_host_cwd) {
4886 let relative = requested_host_cwd
4887 .strip_prefix(&normalized_parent_host_cwd)
4888 .unwrap_or_else(|_| Path::new(""));
4889 let relative = relative.to_string_lossy().replace('\\', "/");
4890 let guest_cwd = if relative.is_empty() {
4891 parent_guest_cwd.to_owned()
4892 } else {
4893 normalize_path(&format!("{parent_guest_cwd}/{relative}"))
4894 };
4895 (guest_cwd, Some(requested_host_cwd))
4896 } else if Path::new(cwd).is_relative() {
4897 (
4898 normalize_path(&format!("{parent_guest_cwd}/{cwd}")),
4899 Some(normalize_host_path(&parent_host_cwd.join(cwd))),
4900 )
4901 } else {
4902 (normalize_path(cwd), None)
4903 }
4904 })
4905 .unwrap_or_else(|| (parent_guest_cwd.to_owned(), None));
4906 let inherited_host_cwd = (host_cwd_override.is_none() && guest_cwd == parent_guest_cwd)
4907 .then(|| normalize_host_path(parent_host_cwd));
4908 let host_cwd = host_cwd_override
4909 .or(inherited_host_cwd)
4910 .or_else(|| {
4911 host_runtime_path_for_guest_path_with_env(
4912 vm,
4913 &runtime_env,
4914 &guest_cwd,
4915 parent_host_cwd,
4916 )
4917 })
4918 .unwrap_or_else(|| {
4919 let candidate = PathBuf::from(&guest_cwd);
4920 if guest_cwd == parent_guest_cwd {
4921 normalize_host_path(parent_host_cwd)
4922 } else if candidate.is_absolute() {
4923 shadow_path_for_guest(vm, &guest_cwd)
4924 } else {
4925 vm.host_cwd.clone()
4926 }
4927 });
4928 let mut env = parent_env.clone();
4929 env.extend(request.options.env.clone());
4930 env.remove("AGENTOS_GUEST_ENTRYPOINT");
4933 env.remove("AGENTOS_NODE_EVAL");
4934
4935 let (command, process_args) = if request.options.shell {
4936 let tokens = tokenize_shell_free_command(&request.command);
4937 let requires_shell = command_requires_shell(&request.command)
4938 || tokens.first().is_some_and(|command| {
4939 is_posix_shell_builtin(command) || shell_first_token_requires_shell(command)
4940 });
4941 if requires_shell {
4942 if !vm.command_guest_paths.contains_key("sh") {
4943 return Err(SidecarError::InvalidState(format!(
4944 "shell-mode child_process command requires /bin/sh, which is not \
4945 installed in this VM (install a software package that provides sh, \
4946 for example @secure-exec/coreutils): {}",
4947 request.command
4948 )));
4949 }
4950 (
4951 String::from("sh"),
4952 vec![String::from("-c"), request.command.clone()],
4953 )
4954 } else {
4955 let Some((command, args)) = tokens.split_first() else {
4956 return Err(SidecarError::InvalidState(String::from(
4957 "child_process shell command must not be empty",
4958 )));
4959 };
4960 (command.clone(), args.to_vec())
4961 }
4962 } else {
4963 (request.command.clone(), request.args.clone())
4964 };
4965 let process_args = apply_shell_cwd_prefix(&command, process_args, &guest_cwd);
4966 if is_tool_command(vm, &command) {
4967 let command = normalized_tool_command_name(&command).unwrap_or(command);
4968 return Ok(ResolvedChildProcessExecution {
4969 command: command.clone(),
4970 process_args: std::iter::once(command.clone())
4971 .chain(process_args.iter().cloned())
4972 .collect(),
4973 runtime: GuestRuntimeKind::JavaScript,
4974 entrypoint: command,
4975 execution_args: process_args,
4976 env,
4977 guest_cwd,
4978 host_cwd,
4979 wasm_permission_tier: None,
4980 tool_command: true,
4981 });
4982 }
4983
4984 if is_path_like_specifier(&command)
4985 && matches!(
4986 Path::new(&command).extension().and_then(|ext| ext.to_str()),
4987 Some("js" | "mjs" | "cjs" | "ts" | "mts" | "cts")
4988 )
4989 {
4990 let guest_entrypoint = if command.starts_with('/') {
4991 normalize_path(&command)
4992 } else if command.starts_with("file:") {
4993 normalize_path(command.trim_start_matches("file:"))
4994 } else {
4995 normalize_path(&format!("{guest_cwd}/{command}"))
4996 };
4997 let host_entrypoint = if command.starts_with("./") || command.starts_with("../") {
4998 normalize_host_path(&host_cwd.join(&command))
4999 } else {
5000 host_runtime_path_for_guest_path_with_env(
5001 vm,
5002 &runtime_env,
5003 &guest_entrypoint,
5004 parent_host_cwd,
5005 )
5006 .unwrap_or_else(|| {
5007 let candidate = PathBuf::from(&guest_entrypoint);
5008 if candidate.is_absolute() {
5009 candidate
5010 } else {
5011 host_cwd.join(&guest_entrypoint)
5012 }
5013 })
5014 };
5015 env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint);
5016 let guest_entrypoint = env.get("AGENTOS_GUEST_ENTRYPOINT").cloned();
5017 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
5018
5019 return Ok(ResolvedChildProcessExecution {
5020 command: command.clone(),
5021 process_args: std::iter::once(command)
5022 .chain(process_args.iter().cloned())
5023 .collect(),
5024 runtime: GuestRuntimeKind::JavaScript,
5025 entrypoint: host_entrypoint.to_string_lossy().into_owned(),
5026 execution_args: process_args,
5027 env,
5028 guest_cwd,
5029 host_cwd,
5030 wasm_permission_tier: None,
5031 tool_command: false,
5032 });
5033 }
5034
5035 if is_node_runtime_command(&command) {
5036 if let Some(cli) = resolve_host_node_cli_entrypoint(&command) {
5037 env.insert(
5038 String::from("AGENTOS_NODE_EVAL"),
5039 build_host_node_cli_eval(&cli),
5040 );
5041 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
5042 add_runtime_guest_path_mapping(&mut env, &cli.guest_root, &cli.package_root);
5043 add_runtime_host_access_path(
5044 &mut env,
5045 "AGENTOS_EXTRA_FS_READ_PATHS",
5046 &cli.package_root,
5047 true,
5048 );
5049
5050 return Ok(ResolvedChildProcessExecution {
5051 command: command.clone(),
5052 process_args: std::iter::once(command.clone())
5053 .chain(process_args.iter().cloned())
5054 .collect(),
5055 runtime: GuestRuntimeKind::JavaScript,
5056 entrypoint: String::from("-e"),
5057 execution_args: std::iter::once(cli.guest_entrypoint.clone())
5058 .chain(process_args.iter().cloned())
5059 .collect(),
5060 env,
5061 guest_cwd,
5062 host_cwd,
5063 wasm_permission_tier: None,
5064 tool_command: false,
5065 });
5066 }
5067
5068 if process_args.is_empty() {
5069 env.insert(String::from("AGENTOS_NODE_EVAL"), String::new());
5070 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
5071
5072 return Ok(ResolvedChildProcessExecution {
5073 command: command.clone(),
5074 process_args: vec![command.clone()],
5075 runtime: GuestRuntimeKind::JavaScript,
5076 entrypoint: String::from("-e"),
5077 execution_args: Vec::new(),
5078 env,
5079 guest_cwd,
5080 host_cwd,
5081 wasm_permission_tier: None,
5082 tool_command: false,
5083 });
5084 }
5085
5086 if let Some((entrypoint, execution_args)) =
5087 resolve_special_node_cli_invocation(&process_args, &mut env)
5088 {
5089 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
5090
5091 return Ok(ResolvedChildProcessExecution {
5092 command: command.clone(),
5093 process_args: std::iter::once(command.clone())
5094 .chain(process_args.iter().cloned())
5095 .collect(),
5096 runtime: GuestRuntimeKind::JavaScript,
5097 entrypoint,
5098 execution_args,
5099 env,
5100 guest_cwd,
5101 host_cwd,
5102 wasm_permission_tier: None,
5103 tool_command: false,
5104 });
5105 }
5106
5107 let Some(entrypoint_specifier) = process_args.first() else {
5108 return Err(SidecarError::InvalidState(format!(
5109 "{command} child_process spawn requires an entrypoint"
5110 )));
5111 };
5112
5113 let (entrypoint, execution_args) = if is_path_like_specifier(entrypoint_specifier) {
5114 let guest_entrypoint = if entrypoint_specifier.starts_with('/') {
5115 normalize_path(entrypoint_specifier)
5116 } else if entrypoint_specifier.starts_with("file:") {
5117 normalize_path(entrypoint_specifier.trim_start_matches("file:"))
5118 } else {
5119 normalize_path(&format!("{guest_cwd}/{entrypoint_specifier}"))
5120 };
5121 let host_entrypoint = if entrypoint_specifier.starts_with("./")
5122 || entrypoint_specifier.starts_with("../")
5123 {
5124 normalize_host_path(&host_cwd.join(entrypoint_specifier))
5125 } else {
5126 host_runtime_path_for_guest_path_with_env(
5127 vm,
5128 &runtime_env,
5129 &guest_entrypoint,
5130 parent_host_cwd,
5131 )
5132 .unwrap_or_else(|| {
5133 let candidate = PathBuf::from(&guest_entrypoint);
5134 if candidate.is_absolute() {
5135 candidate
5136 } else {
5137 host_cwd.join(&guest_entrypoint)
5138 }
5139 })
5140 };
5141 env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint);
5142 (
5143 host_entrypoint.to_string_lossy().into_owned(),
5144 process_args.iter().skip(1).cloned().collect(),
5145 )
5146 } else {
5147 (
5148 entrypoint_specifier.clone(),
5149 process_args.iter().skip(1).cloned().collect(),
5150 )
5151 };
5152 let guest_entrypoint = env.get("AGENTOS_GUEST_ENTRYPOINT").cloned();
5153 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
5154
5155 return Ok(ResolvedChildProcessExecution {
5156 command: command.clone(),
5157 process_args: std::iter::once(command)
5158 .chain(process_args.iter().cloned())
5159 .collect(),
5160 runtime: GuestRuntimeKind::JavaScript,
5161 entrypoint,
5162 execution_args,
5163 env,
5164 guest_cwd,
5165 host_cwd,
5166 wasm_permission_tier: None,
5167 tool_command: false,
5168 });
5169 }
5170
5171 if command == PYTHON_COMMAND {
5172 return Err(SidecarError::InvalidState(String::from(
5173 "nested python child_process execution is not supported yet",
5174 )));
5175 }
5176
5177 let guest_entrypoint = resolve_guest_command_entrypoint(
5178 vm,
5179 &guest_cwd,
5180 &command,
5181 env.get("PATH").map(String::as_str),
5182 )
5183 .ok_or_else(|| SidecarError::InvalidState(format!("command not found: {command}")))?;
5184 let host_entrypoint = resolve_vm_guest_path_to_host(vm, &guest_entrypoint);
5185 let wasm_permission_tier = vm.command_permissions.get(&command).copied().or_else(|| {
5186 Path::new(&guest_entrypoint)
5187 .file_name()
5188 .and_then(|name| name.to_str())
5189 .and_then(|name| vm.command_permissions.get(name).copied())
5190 });
5191 if let Some((javascript_guest_entrypoint, javascript_host_entrypoint)) =
5192 resolve_javascript_command_entrypoint(vm, &guest_entrypoint, &host_entrypoint)
5193 {
5194 prepare_guest_runtime_env(
5195 vm,
5196 &mut env,
5197 &guest_cwd,
5198 &host_cwd,
5199 Some(javascript_guest_entrypoint),
5200 )?;
5201
5202 return Ok(ResolvedChildProcessExecution {
5203 command: command.clone(),
5204 process_args: std::iter::once(command)
5205 .chain(process_args.iter().cloned())
5206 .collect(),
5207 runtime: GuestRuntimeKind::JavaScript,
5208 entrypoint: javascript_host_entrypoint.to_string_lossy().into_owned(),
5209 execution_args: process_args,
5210 env,
5211 guest_cwd,
5212 host_cwd,
5213 wasm_permission_tier: None,
5214 tool_command: false,
5215 });
5216 }
5217 prepare_guest_runtime_env(
5218 vm,
5219 &mut env,
5220 &guest_cwd,
5221 &host_cwd,
5222 Some(guest_entrypoint.clone()),
5223 )?;
5224
5225 Ok(ResolvedChildProcessExecution {
5226 command: command.clone(),
5227 process_args: std::iter::once(command)
5228 .chain(process_args.iter().cloned())
5229 .collect(),
5230 runtime: GuestRuntimeKind::WebAssembly,
5231 entrypoint: host_entrypoint.to_string_lossy().into_owned(),
5232 execution_args: process_args,
5233 env,
5234 guest_cwd,
5235 host_cwd,
5236 wasm_permission_tier,
5237 tool_command: false,
5238 })
5239 }
5240
5241 pub(crate) fn spawn_javascript_child_process(
5242 &mut self,
5243 vm_id: &str,
5244 process_id: &str,
5245 request: JavascriptChildProcessSpawnRequest,
5246 ) -> Result<Value, SidecarError> {
5247 let resolved = {
5248 let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?;
5249 let parent = vm
5250 .active_processes
5251 .get(process_id)
5252 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5253 self.resolve_javascript_child_process_execution(
5254 vm,
5255 &parent.env,
5256 &parent.guest_cwd,
5257 &parent.host_cwd,
5258 &request,
5259 )?
5260 };
5261 let (parent_kernel_pid, child_process_id) = {
5262 let vm = self
5263 .vms
5264 .get_mut(vm_id)
5265 .ok_or_else(|| missing_vm_error(vm_id))?;
5266 let process = vm
5267 .active_processes
5268 .get_mut(process_id)
5269 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5270 (process.kernel_pid, process.allocate_child_process_id())
5271 };
5272 let sidecar_requests = self.sidecar_requests.clone();
5273 let vm = self
5274 .vms
5275 .get_mut(vm_id)
5276 .ok_or_else(|| missing_vm_error(vm_id))?;
5277 let (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd) = if resolved
5278 .tool_command
5279 {
5280 let tool_resolution = resolve_tool_command(
5281 vm,
5282 &resolved.command,
5283 &resolved.execution_args,
5284 Some(&resolved.guest_cwd),
5285 )?
5286 .ok_or_else(|| {
5287 SidecarError::InvalidState(format!(
5288 "tool command no longer resolves: {}",
5289 resolved.command
5290 ))
5291 })?;
5292 let kernel_handle = vm
5293 .kernel
5294 .create_virtual_process(
5295 EXECUTION_DRIVER_NAME,
5296 TOOL_DRIVER_NAME,
5297 &resolved.command,
5298 resolved.process_args.clone(),
5299 VirtualProcessOptions {
5300 parent_pid: Some(parent_kernel_pid),
5301 env: resolved.env.clone(),
5302 cwd: Some(resolved.guest_cwd.clone()),
5303 },
5304 )
5305 .map_err(kernel_error)?;
5306 let kernel_pid = kernel_handle.pid();
5307 let tool_execution = ToolExecution::default();
5308 let cancelled = tool_execution.cancelled.clone();
5309 let pending_events = tool_execution.pending_events.clone();
5310 let events_overflowed = tool_execution.events_overflowed.clone();
5311 spawn_tool_process_events(ToolProcessEventRequest {
5312 sidecar_requests: sidecar_requests.clone(),
5313 connection_id: vm.connection_id.clone(),
5314 session_id: vm.session_id.clone(),
5315 vm_id: vm_id.to_owned(),
5316 tool_resolution,
5317 cancelled,
5318 pending_events,
5319 events_overflowed,
5320 });
5321 (
5322 kernel_pid,
5323 kernel_handle,
5324 ActiveExecution::Tool(tool_execution),
5325 None,
5326 )
5327 } else {
5328 let kernel_command = match resolved.runtime {
5329 GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND,
5330 GuestRuntimeKind::WebAssembly => WASM_COMMAND,
5331 GuestRuntimeKind::Python => {
5332 unreachable!("python child_process execution is rejected")
5333 }
5334 };
5335 let kernel_handle = vm
5336 .kernel
5337 .spawn_process(
5338 kernel_command,
5339 resolved.process_args.clone(),
5340 SpawnOptions {
5341 requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
5342 parent_pid: Some(parent_kernel_pid),
5343 env: resolved.env.clone(),
5344 cwd: Some(resolved.guest_cwd.clone()),
5345 },
5346 )
5347 .map_err(kernel_error)?;
5348 let kernel_pid = kernel_handle.pid();
5349 if request.options.detached {
5350 vm.kernel
5351 .setsid(EXECUTION_DRIVER_NAME, kernel_pid)
5352 .map_err(kernel_error)?;
5353 }
5354 let mut execution_env = resolved.env.clone();
5355 execution_env.insert(
5356 String::from(EXECUTION_SANDBOX_ROOT_ENV),
5357 normalize_host_path(&vm.cwd).to_string_lossy().into_owned(),
5358 );
5359
5360 let execution = match resolved.runtime {
5361 GuestRuntimeKind::JavaScript => {
5362 execution_env.extend(sanitize_javascript_child_process_internal_bootstrap_env(
5363 &request.options.internal_bootstrap_env,
5364 ));
5365 execution_env.insert(
5366 String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
5367 String::from("1"),
5368 );
5369 let context =
5370 self.javascript_engine
5371 .create_context(CreateJavascriptContextRequest {
5372 vm_id: vm_id.to_owned(),
5373 bootstrap_module: None,
5374 compile_cache_root: Some(
5375 self.cache_root.join("node-compile-cache"),
5376 ),
5377 });
5378 let inline_code = load_javascript_entrypoint_source(
5379 vm,
5380 &resolved.host_cwd,
5381 &resolved.entrypoint,
5382 &execution_env,
5383 );
5384 prepare_javascript_shadow(vm, &resolved)?;
5385
5386 let built_reader = build_module_reader(vm, &resolved);
5387 let guest_reader = built_reader.clone().map(|reader| {
5388 Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader))
5389 as Box<dyn GuestModuleReader>
5390 });
5391 let module_reader = built_reader
5392 .map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
5393 let execution = self
5394 .javascript_engine
5395 .start_execution_with_module_reader(
5396 StartJavascriptExecutionRequest {
5397 guest_runtime: guest_runtime_identity(
5398 vm,
5399 Some(u64::from(kernel_pid)),
5400 Some(u64::from(parent_kernel_pid)),
5401 ),
5402 vm_id: vm_id.to_owned(),
5403 context_id: context.context_id,
5404 argv: std::iter::once(resolved.entrypoint.clone())
5405 .chain(resolved.execution_args.clone())
5406 .collect(),
5407 env: execution_env,
5408 cwd: resolved.host_cwd.clone(),
5409 limits: javascript_execution_limits(vm),
5410 inline_code,
5411 },
5412 module_reader,
5413 guest_reader,
5414 )
5415 .map_err(javascript_error)?;
5416 ActiveExecution::Javascript(execution)
5417 }
5418 GuestRuntimeKind::WebAssembly => {
5419 execution_env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
5420 let wasm_limits = wasm_execution_limits(vm);
5421 let wasm_guest_runtime = guest_runtime_identity(
5422 vm,
5423 Some(u64::from(kernel_pid)),
5424 Some(u64::from(parent_kernel_pid)),
5425 );
5426 let context = self.wasm_engine.create_context(CreateWasmContextRequest {
5427 vm_id: vm_id.to_owned(),
5428 module_path: Some(resolved.entrypoint.clone()),
5429 });
5430 let execution = self
5431 .wasm_engine
5432 .start_execution(StartWasmExecutionRequest {
5433 vm_id: vm_id.to_owned(),
5434 context_id: context.context_id,
5435 argv: resolved.process_args.clone(),
5436 env: execution_env,
5437 cwd: resolved.host_cwd.clone(),
5438 permission_tier: execution_wasm_permission_tier(
5439 resolved
5440 .wasm_permission_tier
5441 .unwrap_or(WasmPermissionTier::Full),
5442 ),
5443 limits: wasm_limits,
5444 guest_runtime: wasm_guest_runtime,
5445 })
5446 .map_err(wasm_error)?;
5447 ActiveExecution::Wasm(Box::new(execution))
5448 }
5449 GuestRuntimeKind::Python => {
5450 unreachable!("python child_process execution is rejected")
5451 }
5452 };
5453 let kernel_stdin_writer_fd = match javascript_child_process_stdin_mode(&request) {
5454 "pipe" => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5455 "ignore" => {
5456 vm.kernel
5457 .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, 0)
5458 .map_err(kernel_error)?;
5459 None
5460 }
5461 "inherit" => None,
5462 _ => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5463 };
5464 (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd)
5465 };
5466
5467 let process = vm
5468 .active_processes
5469 .get_mut(process_id)
5470 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5471 process.child_processes.insert(
5472 child_process_id.clone(),
5473 ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
5474 .with_detached(request.options.detached)
5475 .with_guest_cwd(resolved.guest_cwd.clone())
5476 .with_env(resolved.env.clone())
5477 .with_host_cwd(resolved.host_cwd.clone()),
5478 );
5479 if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd {
5480 process
5481 .child_processes
5482 .get_mut(&child_process_id)
5483 .ok_or_else(|| {
5484 SidecarError::InvalidState(format!(
5485 "child process {child_process_id} disappeared during spawn"
5486 ))
5487 })?
5488 .kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd);
5489 }
5490 Ok(json!({
5491 "childId": child_process_id,
5492 "pid": kernel_pid,
5493 "command": resolved.command,
5494 "args": resolved.process_args,
5495 }))
5496 }
5497
5498 pub(crate) fn spawn_javascript_child_process_sync(
5499 &mut self,
5500 vm_id: &str,
5501 process_id: &str,
5502 request: JavascriptChildProcessSpawnRequest,
5503 max_buffer: Option<usize>,
5504 ) -> Result<Value, SidecarError> {
5505 let sync_input = javascript_child_process_sync_input_bytes(request.options.input.as_ref())?;
5506 let timeout_deadline = request
5507 .options
5508 .timeout
5509 .map(|timeout_ms| Instant::now() + Duration::from_millis(timeout_ms));
5510 let timeout_signal = request
5511 .options
5512 .kill_signal
5513 .clone()
5514 .unwrap_or_else(|| String::from("SIGTERM"));
5515 let spawned = self.spawn_javascript_child_process(vm_id, process_id, request)?;
5516 let child_process_id = spawned
5517 .get("childId")
5518 .and_then(Value::as_str)
5519 .ok_or_else(|| {
5520 SidecarError::InvalidState(String::from(
5521 "child_process.spawn_sync response is missing childId",
5522 ))
5523 })?
5524 .to_owned();
5525
5526 if let Some(input) = sync_input.as_deref() {
5527 self.write_javascript_child_process_stdin(vm_id, process_id, &child_process_id, input)?;
5528 }
5529 self.close_javascript_child_process_stdin(vm_id, process_id, &child_process_id)?;
5530
5531 let max_buffer = max_buffer.unwrap_or(1024 * 1024);
5532 let mut stdout = Vec::new();
5533 let mut stderr = Vec::new();
5534 let mut max_buffer_exceeded = false;
5535 let mut kill_sent = false;
5536 let mut timed_out = false;
5537
5538 let exit_code = loop {
5539 let wait_ms = if let Some(deadline) = timeout_deadline {
5540 let now = Instant::now();
5541 if now >= deadline {
5542 if !kill_sent {
5543 timed_out = true;
5544 self.kill_javascript_child_process(
5545 vm_id,
5546 process_id,
5547 &child_process_id,
5548 &timeout_signal,
5549 )?;
5550 kill_sent = true;
5551 }
5552 0
5553 } else {
5554 u64::try_from(deadline.saturating_duration_since(now).as_millis().min(50))
5555 .unwrap_or(50)
5556 }
5557 } else {
5558 50
5559 };
5560 let event =
5561 self.poll_javascript_child_process(vm_id, process_id, &child_process_id, wait_ms)?;
5562 if event.is_null() {
5563 continue;
5564 }
5565
5566 match event.get("type").and_then(Value::as_str) {
5567 Some("stdout") => {
5568 let chunk = javascript_sync_rpc_bytes_arg(
5569 &[event.get("data").cloned().unwrap_or(Value::Null)],
5570 0,
5571 "child_process.spawn_sync stdout",
5572 )?;
5573 stdout.extend_from_slice(&chunk);
5574 if stdout.len() > max_buffer && !kill_sent {
5575 max_buffer_exceeded = true;
5576 self.kill_javascript_child_process(
5577 vm_id,
5578 process_id,
5579 &child_process_id,
5580 "SIGTERM",
5581 )?;
5582 kill_sent = true;
5583 }
5584 }
5585 Some("stderr") => {
5586 let chunk = javascript_sync_rpc_bytes_arg(
5587 &[event.get("data").cloned().unwrap_or(Value::Null)],
5588 0,
5589 "child_process.spawn_sync stderr",
5590 )?;
5591 stderr.extend_from_slice(&chunk);
5592 if stderr.len() > max_buffer && !kill_sent {
5593 max_buffer_exceeded = true;
5594 self.kill_javascript_child_process(
5595 vm_id,
5596 process_id,
5597 &child_process_id,
5598 "SIGTERM",
5599 )?;
5600 kill_sent = true;
5601 }
5602 }
5603 Some("exit") => {
5604 break event
5605 .get("exitCode")
5606 .and_then(Value::as_i64)
5607 .map(|value| value as i32)
5608 .unwrap_or(1);
5609 }
5610 _ => {}
5611 }
5612 };
5613
5614 Ok(json!({
5615 "stdout": String::from_utf8_lossy(&stdout),
5616 "stderr": String::from_utf8_lossy(&stderr),
5617 "code": exit_code,
5618 "signal": if timed_out { Value::String(timeout_signal) } else { Value::Null },
5619 "timedOut": timed_out,
5620 "maxBufferExceeded": max_buffer_exceeded,
5621 }))
5622 }
5623
5624 fn spawn_descendant_javascript_child_process(
5625 &mut self,
5626 vm_id: &str,
5627 process_id: &str,
5628 current_process_path: &[&str],
5629 request: JavascriptChildProcessSpawnRequest,
5630 ) -> Result<Value, SidecarError> {
5631 let current_process_label =
5632 Self::child_process_path_label(process_id, current_process_path);
5633 let (resolved, parent_kernel_pid) = {
5634 let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?;
5635 let root = vm
5636 .active_processes
5637 .get(process_id)
5638 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5639 let parent =
5640 Self::active_process_by_path(root, current_process_path).ok_or_else(|| {
5641 SidecarError::InvalidState(format!(
5642 "unknown child process path {current_process_label} during nested spawn"
5643 ))
5644 })?;
5645 (
5646 self.resolve_javascript_child_process_execution(
5647 vm,
5648 &parent.env,
5649 &parent.guest_cwd,
5650 &parent.host_cwd,
5651 &request,
5652 )?,
5653 parent.kernel_pid,
5654 )
5655 };
5656
5657 let sidecar_requests = self.sidecar_requests.clone();
5658 let vm = self
5659 .vms
5660 .get_mut(vm_id)
5661 .ok_or_else(|| missing_vm_error(vm_id))?;
5662 let child_process_id = {
5663 let root = vm
5664 .active_processes
5665 .get_mut(process_id)
5666 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5667 let parent =
5668 Self::active_process_by_path_mut(root, current_process_path).ok_or_else(|| {
5669 SidecarError::InvalidState(format!(
5670 "unknown child process path {current_process_label} during nested spawn"
5671 ))
5672 })?;
5673 parent.allocate_child_process_id()
5674 };
5675 let mut child_path = current_process_path.to_vec();
5676 child_path.push(child_process_id.as_str());
5677 let (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd) = if resolved
5678 .tool_command
5679 {
5680 let tool_resolution = resolve_tool_command(
5681 vm,
5682 &resolved.command,
5683 &resolved.execution_args,
5684 Some(&resolved.guest_cwd),
5685 )?
5686 .ok_or_else(|| {
5687 SidecarError::InvalidState(format!(
5688 "tool command no longer resolves: {}",
5689 resolved.command
5690 ))
5691 })?;
5692 let kernel_handle = vm
5693 .kernel
5694 .create_virtual_process(
5695 EXECUTION_DRIVER_NAME,
5696 TOOL_DRIVER_NAME,
5697 &resolved.command,
5698 resolved.process_args.clone(),
5699 VirtualProcessOptions {
5700 parent_pid: Some(parent_kernel_pid),
5701 env: resolved.env.clone(),
5702 cwd: Some(resolved.guest_cwd.clone()),
5703 },
5704 )
5705 .map_err(kernel_error)?;
5706 let kernel_pid = kernel_handle.pid();
5707 let tool_execution = ToolExecution::default();
5708 let cancelled = tool_execution.cancelled.clone();
5709 let pending_events = tool_execution.pending_events.clone();
5710 let events_overflowed = tool_execution.events_overflowed.clone();
5711 spawn_tool_process_events(ToolProcessEventRequest {
5712 sidecar_requests: sidecar_requests.clone(),
5713 connection_id: vm.connection_id.clone(),
5714 session_id: vm.session_id.clone(),
5715 vm_id: vm_id.to_owned(),
5716 tool_resolution,
5717 cancelled,
5718 pending_events,
5719 events_overflowed,
5720 });
5721 (
5722 kernel_pid,
5723 kernel_handle,
5724 ActiveExecution::Tool(tool_execution),
5725 None,
5726 )
5727 } else {
5728 let kernel_command = match resolved.runtime {
5729 GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND,
5730 GuestRuntimeKind::WebAssembly => WASM_COMMAND,
5731 GuestRuntimeKind::Python => {
5732 unreachable!("python child_process execution is rejected")
5733 }
5734 };
5735 let kernel_handle = vm
5736 .kernel
5737 .spawn_process(
5738 kernel_command,
5739 resolved.process_args.clone(),
5740 SpawnOptions {
5741 requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
5742 parent_pid: Some(parent_kernel_pid),
5743 env: resolved.env.clone(),
5744 cwd: Some(resolved.guest_cwd.clone()),
5745 },
5746 )
5747 .map_err(kernel_error)?;
5748 let kernel_pid = kernel_handle.pid();
5749 if request.options.detached {
5750 vm.kernel
5751 .setsid(EXECUTION_DRIVER_NAME, kernel_pid)
5752 .map_err(kernel_error)?;
5753 }
5754 let mut execution_env = resolved.env.clone();
5755 execution_env.insert(
5756 String::from(EXECUTION_SANDBOX_ROOT_ENV),
5757 normalize_host_path(&vm.cwd).to_string_lossy().into_owned(),
5758 );
5759 let execution = match resolved.runtime {
5760 GuestRuntimeKind::JavaScript => {
5761 execution_env.extend(sanitize_javascript_child_process_internal_bootstrap_env(
5762 &request.options.internal_bootstrap_env,
5763 ));
5764 execution_env.insert(
5765 String::from("SECURE_EXEC_KEEP_STDIN_OPEN"),
5766 String::from("1"),
5767 );
5768 let context =
5769 self.javascript_engine
5770 .create_context(CreateJavascriptContextRequest {
5771 vm_id: vm_id.to_owned(),
5772 bootstrap_module: None,
5773 compile_cache_root: Some(
5774 self.cache_root.join("node-compile-cache"),
5775 ),
5776 });
5777 let inline_code = load_javascript_entrypoint_source(
5778 vm,
5779 &resolved.host_cwd,
5780 &resolved.entrypoint,
5781 &execution_env,
5782 );
5783 prepare_javascript_shadow(vm, &resolved)?;
5784
5785 let built_reader = build_module_reader(vm, &resolved);
5786 let guest_reader = built_reader.clone().map(|reader| {
5787 Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader))
5788 as Box<dyn GuestModuleReader>
5789 });
5790 let module_reader = built_reader
5791 .map(|reader| Box::new(reader) as Box<dyn ModuleFsReader + Send>);
5792 let execution = self
5793 .javascript_engine
5794 .start_execution_with_module_reader(
5795 StartJavascriptExecutionRequest {
5796 guest_runtime: guest_runtime_identity(
5797 vm,
5798 Some(u64::from(kernel_pid)),
5799 Some(u64::from(parent_kernel_pid)),
5800 ),
5801 vm_id: vm_id.to_owned(),
5802 context_id: context.context_id,
5803 argv: std::iter::once(resolved.entrypoint.clone())
5804 .chain(resolved.execution_args.clone())
5805 .collect(),
5806 env: execution_env,
5807 cwd: resolved.host_cwd.clone(),
5808 limits: javascript_execution_limits(vm),
5809 inline_code,
5810 },
5811 module_reader,
5812 guest_reader,
5813 )
5814 .map_err(javascript_error)?;
5815 ActiveExecution::Javascript(execution)
5816 }
5817 GuestRuntimeKind::WebAssembly => {
5818 execution_env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1"));
5819 let wasm_limits = wasm_execution_limits(vm);
5820 let wasm_guest_runtime = guest_runtime_identity(
5821 vm,
5822 Some(u64::from(kernel_pid)),
5823 Some(u64::from(parent_kernel_pid)),
5824 );
5825 let context = self.wasm_engine.create_context(CreateWasmContextRequest {
5826 vm_id: vm_id.to_owned(),
5827 module_path: Some(resolved.entrypoint.clone()),
5828 });
5829 let execution = self
5830 .wasm_engine
5831 .start_execution(StartWasmExecutionRequest {
5832 vm_id: vm_id.to_owned(),
5833 context_id: context.context_id,
5834 argv: resolved.process_args.clone(),
5835 env: execution_env,
5836 cwd: resolved.host_cwd.clone(),
5837 permission_tier: execution_wasm_permission_tier(
5838 resolved
5839 .wasm_permission_tier
5840 .unwrap_or(WasmPermissionTier::Full),
5841 ),
5842 limits: wasm_limits,
5843 guest_runtime: wasm_guest_runtime,
5844 })
5845 .map_err(wasm_error)?;
5846 ActiveExecution::Wasm(Box::new(execution))
5847 }
5848 GuestRuntimeKind::Python => {
5849 unreachable!("python child_process execution is rejected")
5850 }
5851 };
5852 let kernel_stdin_writer_fd = match javascript_child_process_stdin_mode(&request) {
5853 "pipe" => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5854 "ignore" => {
5855 vm.kernel
5856 .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, 0)
5857 .map_err(kernel_error)?;
5858 None
5859 }
5860 "inherit" => None,
5861 _ => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?),
5862 };
5863 (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd)
5864 };
5865
5866 let root = vm
5867 .active_processes
5868 .get_mut(process_id)
5869 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
5870 let parent =
5871 Self::active_process_by_path_mut(root, current_process_path).ok_or_else(|| {
5872 SidecarError::InvalidState(format!(
5873 "unknown child process path {current_process_label} during nested spawn"
5874 ))
5875 })?;
5876 parent.child_processes.insert(
5877 child_process_id.clone(),
5878 ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution)
5879 .with_detached(request.options.detached)
5880 .with_guest_cwd(resolved.guest_cwd.clone())
5881 .with_env(resolved.env.clone())
5882 .with_host_cwd(resolved.host_cwd.clone()),
5883 );
5884 if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd {
5885 parent
5886 .child_processes
5887 .get_mut(&child_process_id)
5888 .ok_or_else(|| {
5889 SidecarError::InvalidState(format!(
5890 "child process {child_process_id} disappeared during nested spawn"
5891 ))
5892 })?
5893 .kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd);
5894 }
5895 Ok(json!({
5896 "childId": child_process_id,
5897 "pid": kernel_pid,
5898 "command": resolved.command,
5899 "args": resolved.process_args,
5900 }))
5901 }
5902
5903 fn spawn_descendant_javascript_child_process_sync(
5904 &mut self,
5905 vm_id: &str,
5906 process_id: &str,
5907 current_process_path: &[&str],
5908 request: JavascriptChildProcessSpawnRequest,
5909 max_buffer: Option<usize>,
5910 ) -> Result<Value, SidecarError> {
5911 let sync_input = javascript_child_process_sync_input_bytes(request.options.input.as_ref())?;
5912 let timeout_deadline = request
5913 .options
5914 .timeout
5915 .map(|timeout_ms| Instant::now() + Duration::from_millis(timeout_ms));
5916 let timeout_signal = request
5917 .options
5918 .kill_signal
5919 .clone()
5920 .unwrap_or_else(|| String::from("SIGTERM"));
5921 let spawned = self.spawn_descendant_javascript_child_process(
5922 vm_id,
5923 process_id,
5924 current_process_path,
5925 request,
5926 )?;
5927 let child_process_id = spawned
5928 .get("childId")
5929 .and_then(Value::as_str)
5930 .ok_or_else(|| {
5931 SidecarError::InvalidState(String::from(
5932 "child_process.spawn_sync response is missing childId",
5933 ))
5934 })?
5935 .to_owned();
5936
5937 if let Some(input) = sync_input.as_deref() {
5938 self.write_descendant_javascript_child_process_stdin(
5939 vm_id,
5940 process_id,
5941 current_process_path,
5942 &child_process_id,
5943 input,
5944 )?;
5945 }
5946 self.close_descendant_javascript_child_process_stdin(
5947 vm_id,
5948 process_id,
5949 current_process_path,
5950 &child_process_id,
5951 )?;
5952
5953 let max_buffer = max_buffer.unwrap_or(1024 * 1024);
5954 let mut stdout = Vec::new();
5955 let mut stderr = Vec::new();
5956 let mut max_buffer_exceeded = false;
5957 let mut kill_sent = false;
5958 let mut timed_out = false;
5959
5960 let exit_code = loop {
5961 let wait_ms = if let Some(deadline) = timeout_deadline {
5962 let now = Instant::now();
5963 if now >= deadline {
5964 if !kill_sent {
5965 timed_out = true;
5966 self.kill_descendant_javascript_child_process(
5967 vm_id,
5968 process_id,
5969 current_process_path,
5970 &child_process_id,
5971 &timeout_signal,
5972 )?;
5973 kill_sent = true;
5974 }
5975 0
5976 } else {
5977 u64::try_from(deadline.saturating_duration_since(now).as_millis().min(50))
5978 .unwrap_or(50)
5979 }
5980 } else {
5981 50
5982 };
5983 let event = self.poll_descendant_javascript_child_process(
5984 vm_id,
5985 process_id,
5986 current_process_path,
5987 &child_process_id,
5988 wait_ms,
5989 )?;
5990 if event.is_null() {
5991 continue;
5992 }
5993
5994 match event.get("type").and_then(Value::as_str) {
5995 Some("stdout") => {
5996 let chunk = javascript_sync_rpc_bytes_arg(
5997 &[event.get("data").cloned().unwrap_or(Value::Null)],
5998 0,
5999 "child_process.spawn_sync stdout",
6000 )?;
6001 stdout.extend_from_slice(&chunk);
6002 if stdout.len() > max_buffer && !kill_sent {
6003 max_buffer_exceeded = true;
6004 self.kill_descendant_javascript_child_process(
6005 vm_id,
6006 process_id,
6007 current_process_path,
6008 &child_process_id,
6009 "SIGTERM",
6010 )?;
6011 kill_sent = true;
6012 }
6013 }
6014 Some("stderr") => {
6015 let chunk = javascript_sync_rpc_bytes_arg(
6016 &[event.get("data").cloned().unwrap_or(Value::Null)],
6017 0,
6018 "child_process.spawn_sync stderr",
6019 )?;
6020 stderr.extend_from_slice(&chunk);
6021 if stderr.len() > max_buffer && !kill_sent {
6022 max_buffer_exceeded = true;
6023 self.kill_descendant_javascript_child_process(
6024 vm_id,
6025 process_id,
6026 current_process_path,
6027 &child_process_id,
6028 "SIGTERM",
6029 )?;
6030 kill_sent = true;
6031 }
6032 }
6033 Some("exit") => {
6034 break event
6035 .get("exitCode")
6036 .and_then(Value::as_i64)
6037 .map(|value| value as i32)
6038 .unwrap_or(1);
6039 }
6040 _ => {}
6041 }
6042 };
6043
6044 Ok(json!({
6045 "stdout": String::from_utf8_lossy(&stdout),
6046 "stderr": String::from_utf8_lossy(&stderr),
6047 "code": exit_code,
6048 "signal": if timed_out { Value::String(timeout_signal) } else { Value::Null },
6049 "timedOut": timed_out,
6050 "maxBufferExceeded": max_buffer_exceeded,
6051 }))
6052 }
6053
6054 fn handle_descendant_javascript_child_process_rpc(
6055 &mut self,
6056 vm_id: &str,
6057 process_id: &str,
6058 current_process_path: &[&str],
6059 request: &JavascriptSyncRpcRequest,
6060 ) -> Result<Value, SidecarError> {
6061 match request.method.as_str() {
6062 "child_process.spawn" => {
6063 let Some(vm) = self.vms.get(vm_id) else {
6064 return Ok(Value::Null);
6065 };
6066 let (payload, _) = parse_javascript_child_process_spawn_request(vm, &request.args)?;
6067 self.spawn_descendant_javascript_child_process(
6068 vm_id,
6069 process_id,
6070 current_process_path,
6071 payload,
6072 )
6073 }
6074 "child_process.spawn_sync" => {
6075 let Some(vm) = self.vms.get(vm_id) else {
6076 return Ok(Value::Null);
6077 };
6078 let (payload, max_buffer) =
6079 parse_javascript_child_process_spawn_request(vm, &request.args)?;
6080 self.spawn_descendant_javascript_child_process_sync(
6081 vm_id,
6082 process_id,
6083 current_process_path,
6084 payload,
6085 max_buffer,
6086 )
6087 }
6088 "child_process.poll" => {
6089 let child_process_id =
6090 javascript_sync_rpc_arg_str(&request.args, 0, "child_process.poll child id")?;
6091 let wait_ms = javascript_sync_rpc_arg_u64_optional(
6092 &request.args,
6093 1,
6094 "child_process.poll wait ms",
6095 )?
6096 .unwrap_or_default();
6097 self.poll_descendant_javascript_child_process(
6098 vm_id,
6099 process_id,
6100 current_process_path,
6101 child_process_id,
6102 wait_ms,
6103 )
6104 }
6105 "child_process.write_stdin" => {
6106 let child_process_id = javascript_sync_rpc_arg_str(
6107 &request.args,
6108 0,
6109 "child_process.write_stdin child id",
6110 )?;
6111 let chunk = javascript_sync_rpc_bytes_arg(
6112 &request.args,
6113 1,
6114 "child_process.write_stdin chunk",
6115 )?;
6116 self.write_descendant_javascript_child_process_stdin(
6117 vm_id,
6118 process_id,
6119 current_process_path,
6120 child_process_id,
6121 &chunk,
6122 )?;
6123 Ok(Value::Null)
6124 }
6125 "child_process.close_stdin" => {
6126 let child_process_id = javascript_sync_rpc_arg_str(
6127 &request.args,
6128 0,
6129 "child_process.close_stdin child id",
6130 )?;
6131 self.close_descendant_javascript_child_process_stdin(
6132 vm_id,
6133 process_id,
6134 current_process_path,
6135 child_process_id,
6136 )?;
6137 Ok(Value::Null)
6138 }
6139 "child_process.kill" => {
6140 let child_process_id =
6141 javascript_sync_rpc_arg_str(&request.args, 0, "child_process.kill child id")?;
6142 let signal =
6143 javascript_sync_rpc_arg_str(&request.args, 1, "child_process.kill signal")?;
6144 self.kill_descendant_javascript_child_process(
6145 vm_id,
6146 process_id,
6147 current_process_path,
6148 child_process_id,
6149 signal,
6150 )?;
6151 Ok(Value::Null)
6152 }
6153 _ => Err(SidecarError::InvalidState(format!(
6154 "unsupported nested child process RPC method {}",
6155 request.method
6156 ))),
6157 }
6158 }
6159
6160 fn poll_descendant_javascript_child_process(
6161 &mut self,
6162 vm_id: &str,
6163 process_id: &str,
6164 current_process_path: &[&str],
6165 child_process_id: &str,
6166 wait_ms: u64,
6167 ) -> Result<Value, SidecarError> {
6168 let mut child_path = current_process_path.to_vec();
6169 child_path.push(child_process_id);
6170 let child_gone_error = || javascript_child_process_gone_error(process_id, &child_path);
6171 let deadline = Instant::now() + Duration::from_millis(wait_ms);
6172 let mut polled_once = false;
6173
6174 loop {
6175 self.drain_queued_descendant_javascript_child_process_events(
6176 vm_id,
6177 process_id,
6178 &child_path,
6179 )?;
6180 enum ChildPollResult {
6181 Event(Box<Option<ActiveExecutionEvent>>),
6182 RecoverRuntimeExit,
6183 Timeout,
6184 }
6185 let wait = if wait_ms == 0 {
6186 Duration::ZERO
6187 } else {
6188 deadline.saturating_duration_since(Instant::now())
6189 };
6190 let poll_result = {
6191 let Some(vm) = self.vms.get_mut(vm_id) else {
6192 return Ok(Value::Null);
6193 };
6194 let Some(parent) =
6195 Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6196 else {
6197 return Err(child_gone_error());
6198 };
6199 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6200 return Err(child_gone_error());
6201 };
6202 if let Some(event) = child.pending_execution_events.pop_front() {
6203 ChildPollResult::Event(Box::new(Some(event)))
6204 } else if polled_once && wait.is_zero() {
6205 ChildPollResult::Timeout
6206 } else {
6207 polled_once = true;
6208 match child.execution.poll_event_blocking(wait) {
6209 Ok(Some(event)) => ChildPollResult::Event(Box::new(Some(event))),
6210 Ok(None) => ChildPollResult::RecoverRuntimeExit,
6211 Err(SidecarError::Execution(message))
6212 if (child.runtime == GuestRuntimeKind::JavaScript
6213 && closed_javascript_event_channel(&message))
6214 || (child.runtime == GuestRuntimeKind::Python
6215 && closed_python_event_channel(&message))
6216 || (child.runtime == GuestRuntimeKind::WebAssembly
6217 && closed_wasm_event_channel(&message)) =>
6218 {
6219 ChildPollResult::RecoverRuntimeExit
6220 }
6221 Err(error) => return Err(error),
6222 }
6223 }
6224 };
6225 let event = match poll_result {
6226 ChildPollResult::Event(event) => *event,
6227 ChildPollResult::Timeout => return Ok(Value::Null),
6228 ChildPollResult::RecoverRuntimeExit => self
6229 .recover_descendant_runtime_child_process_event(
6230 vm_id,
6231 process_id,
6232 current_process_path,
6233 child_process_id,
6234 wait.as_millis().try_into().unwrap_or(u64::MAX),
6235 )?,
6236 };
6237
6238 let Some(event) = event else {
6239 return Ok(Value::Null);
6240 };
6241
6242 match event {
6243 ActiveExecutionEvent::Stdout(chunk) => {
6244 return Ok(json!({
6245 "type": "stdout",
6246 "data": javascript_sync_rpc_bytes_value(&chunk),
6247 }));
6248 }
6249 ActiveExecutionEvent::Stderr(chunk) => {
6250 return Ok(json!({
6251 "type": "stderr",
6252 "data": javascript_sync_rpc_bytes_value(&chunk),
6253 }));
6254 }
6255 ActiveExecutionEvent::Exited(exit_code) => {
6256 let had_trailing_events = {
6257 let Some(vm) = self.vms.get_mut(vm_id) else {
6258 return Ok(Value::Null);
6259 };
6260 let Some(parent) = Self::descendant_parent_process_mut(
6261 vm,
6262 process_id,
6263 current_process_path,
6264 ) else {
6265 return Ok(Value::Null);
6266 };
6267 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6268 return Ok(Value::Null);
6269 };
6270 let deadline = Instant::now() + Duration::from_millis(150);
6271 loop {
6272 let wait = deadline.saturating_duration_since(Instant::now());
6273 let next = poll_child_execution_after_exit(child, wait)?;
6274 let Some(next) = next else {
6275 break;
6276 };
6277 if matches!(next, ActiveExecutionEvent::Exited(_)) {
6278 continue;
6279 }
6280 child.queue_pending_execution_event(next)?;
6281 if Instant::now() >= deadline {
6282 break;
6283 }
6284 }
6285 if !child.pending_execution_events.is_empty() {
6286 child.queue_pending_execution_event(ActiveExecutionEvent::Exited(
6287 exit_code,
6288 ))?;
6289 true
6290 } else {
6291 false
6292 }
6293 };
6294 if had_trailing_events {
6295 continue;
6296 }
6297
6298 let parent_signal_key =
6299 Self::child_process_signal_key(process_id, current_process_path);
6300 let Some(vm) = self.vms.get_mut(vm_id) else {
6301 return Ok(Value::Null);
6302 };
6303 let signal_name = {
6304 let Some(parent) = Self::descendant_parent_process_mut(
6305 vm,
6306 process_id,
6307 current_process_path,
6308 ) else {
6309 return Ok(Value::Null);
6310 };
6311 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6312 return Ok(Value::Null);
6313 };
6314 child.pending_self_signal_exit.take().and_then(|signal| {
6315 if exit_code == 128 + signal {
6316 canonical_signal_name(signal).map(str::to_owned)
6317 } else {
6318 None
6319 }
6320 })
6321 };
6322 let (parent_runtime_pid, parent_v8_signal_session, should_signal_parent) = {
6323 let Some(parent) =
6324 Self::descendant_parent_process(vm, process_id, current_process_path)
6325 else {
6326 return Ok(Value::Null);
6327 };
6328 (
6329 parent.execution.child_pid(),
6330 parent.execution.javascript_v8_session_handle().filter(|_| {
6331 matches!(
6332 &parent.execution,
6333 ActiveExecution::Javascript(execution)
6334 if execution.uses_shared_v8_runtime()
6335 )
6336 }),
6337 vm.signal_states
6338 .get(parent_signal_key)
6339 .and_then(|handlers| handlers.get(&(libc::SIGCHLD as u32)))
6340 .is_some_and(|registration| {
6341 registration.action != SignalDispositionAction::Default
6342 }),
6343 )
6344 };
6345 let Some(parent) =
6346 Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6347 else {
6348 return Ok(Value::Null);
6349 };
6350 let Some(mut child) = parent.child_processes.remove(child_process_id) else {
6351 return Ok(Value::Null);
6352 };
6353 let child_process_label =
6354 Self::child_process_path_label(process_id, &child_path);
6355 let detached_children =
6356 Self::adopt_detached_child_processes(&child_process_label, &mut child);
6357 sync_process_host_writes_to_kernel(vm, &child)?;
6358 terminate_child_process_tree(&mut vm.kernel, &mut child);
6359 child.kernel_handle.finish(exit_code);
6360 let _ = vm.kernel.wait_and_reap(child.kernel_pid);
6361 vm.signal_states.remove(child_process_id);
6362 for (detached_process_id, detached_child) in detached_children {
6363 vm.detached_child_processes
6364 .insert(detached_process_id.clone());
6365 vm.active_processes
6366 .insert(detached_process_id, detached_child);
6367 }
6368 if should_signal_parent {
6369 if let Some(session) = parent_v8_signal_session {
6370 dispatch_v8_session_signal_async(session, libc::SIGCHLD);
6371 } else {
6372 signal_runtime_process(parent_runtime_pid, libc::SIGCHLD)?;
6373 }
6374 }
6375 let mut payload = Map::new();
6376 payload.insert(String::from("type"), Value::String(String::from("exit")));
6377 payload.insert(String::from("exitCode"), Value::from(exit_code));
6378 if let Some(signal_name) = signal_name {
6379 payload.insert(String::from("signal"), Value::String(signal_name));
6380 }
6381 return Ok(Value::Object(payload));
6382 }
6383 ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
6384 let mut current_child_path = current_process_path.to_vec();
6385 current_child_path.push(child_process_id);
6386 let response = if request.method == "process.signal_state" {
6387 let (signal, registration) =
6388 parse_process_signal_state_request(&request.args)?;
6389 let Some(vm) = self.vms.get_mut(vm_id) else {
6390 return Ok(Value::Null);
6391 };
6392 let signal_key =
6393 Self::child_process_signal_key(process_id, ¤t_child_path)
6394 .to_owned();
6395 apply_process_signal_state_update(
6396 &mut vm.signal_states,
6397 &signal_key,
6398 signal,
6399 registration,
6400 );
6401 Ok(Value::Null)
6402 } else if request.method == "process.kill" {
6403 self.handle_descendant_process_kill_rpc(
6404 vm_id,
6405 process_id,
6406 current_process_path,
6407 child_process_id,
6408 &request,
6409 )
6410 } else if request.method.starts_with("child_process.") {
6411 self.handle_descendant_javascript_child_process_rpc(
6412 vm_id,
6413 process_id,
6414 ¤t_child_path,
6415 &request,
6416 )
6417 } else {
6418 let Some(vm) = self.vms.get_mut(vm_id) else {
6419 return Ok(Value::Null);
6420 };
6421 let resource_limits = vm.kernel.resource_limits().clone();
6422 let network_counts = vm_network_resource_counts(vm);
6423 let socket_paths = build_javascript_socket_path_context(vm)?;
6424 let Some(root) = vm.active_processes.get_mut(process_id) else {
6425 return Ok(Value::Null);
6426 };
6427 let Some(parent) =
6428 Self::active_process_by_path_mut(root, current_process_path)
6429 else {
6430 return Ok(Value::Null);
6431 };
6432 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6433 return Ok(Value::Null);
6434 };
6435 service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
6436 bridge: &self.bridge,
6437 vm_id,
6438 dns: &vm.dns,
6439 socket_paths: &socket_paths,
6440 kernel: &mut vm.kernel,
6441 process: child,
6442 sync_request: &request,
6443 resource_limits: &resource_limits,
6444 network_counts,
6445 })
6446 };
6447
6448 let Some(vm) = self.vms.get_mut(vm_id) else {
6449 return Ok(Value::Null);
6450 };
6451 let Some(parent) =
6452 Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6453 else {
6454 return Ok(Value::Null);
6455 };
6456 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6457 return Ok(Value::Null);
6458 };
6459 let parent_signal_event = response.as_ref().ok().and_then(|result| {
6460 let target_path_label =
6461 Self::child_process_path_label(process_id, current_process_path);
6462 if request.method != "process.kill"
6463 || result.get("action").and_then(Value::as_str) != Some("user")
6464 || result.get("targetProcessPath").and_then(Value::as_str)
6465 != Some(target_path_label.as_str())
6466 {
6467 return None;
6468 }
6469 Some(json!({
6470 "type": "signal",
6471 "signal": result.get("signal").and_then(Value::as_str).unwrap_or_default(),
6472 "number": result.get("number").and_then(Value::as_i64).unwrap_or_default(),
6473 }))
6474 });
6475 match response {
6476 Ok(result) => child
6477 .execution
6478 .respond_javascript_sync_rpc_success(request.id, result)
6479 .or_else(ignore_stale_javascript_sync_rpc_response)?,
6480 Err(error) => child
6481 .execution
6482 .respond_javascript_sync_rpc_error(
6483 request.id,
6484 javascript_sync_rpc_error_code(&error),
6485 error.to_string(),
6486 )
6487 .or_else(ignore_stale_javascript_sync_rpc_response)?,
6488 }
6489 if let Some(event) = parent_signal_event {
6490 return Ok(event);
6491 }
6492 }
6493 ActiveExecutionEvent::PythonVfsRpcRequest(_) => {
6494 return Err(SidecarError::InvalidState(String::from(
6495 "nested Python child_process execution is not supported yet",
6496 )));
6497 }
6498 ActiveExecutionEvent::SignalState {
6499 signal,
6500 registration,
6501 } => {
6502 let Some(vm) = self.vms.get_mut(vm_id) else {
6503 return Ok(Value::Null);
6504 };
6505 let signal_key =
6506 Self::child_process_signal_key(process_id, &child_path).to_owned();
6507 apply_process_signal_state_update(
6508 &mut vm.signal_states,
6509 &signal_key,
6510 signal,
6511 registration.clone(),
6512 );
6513 return Ok(json!({
6514 "type": "signal_state",
6515 "signal": signal,
6516 "registration": registration,
6517 }));
6518 }
6519 }
6520 }
6521 }
6522
6523 fn recover_descendant_runtime_child_process_event(
6524 &mut self,
6525 vm_id: &str,
6526 process_id: &str,
6527 current_process_path: &[&str],
6528 child_process_id: &str,
6529 wait_ms: u64,
6530 ) -> Result<Option<ActiveExecutionEvent>, SidecarError> {
6531 let (
6532 parent_kernel_pid,
6533 child_kernel_pid,
6534 child_runtime_pid,
6535 child_runtime,
6536 child_shared_runtime,
6537 ) = {
6538 let mut child_path = current_process_path.to_vec();
6539 child_path.push(child_process_id);
6540 let Some(vm) = self.vms.get_mut(vm_id) else {
6541 return Ok(None);
6542 };
6543 let Some(parent) =
6544 Self::descendant_parent_process_mut(vm, process_id, current_process_path)
6545 else {
6546 return Err(javascript_child_process_gone_error(process_id, &child_path));
6547 };
6548 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6549 return Err(javascript_child_process_gone_error(process_id, &child_path));
6550 };
6551 (
6552 parent.kernel_pid,
6553 child.kernel_pid,
6554 child.execution.child_pid(),
6555 child.runtime.clone(),
6556 child.execution.uses_shared_v8_runtime(),
6557 )
6558 };
6559 if child_runtime != GuestRuntimeKind::JavaScript
6560 && child_runtime != GuestRuntimeKind::Python
6561 && child_runtime != GuestRuntimeKind::WebAssembly
6562 {
6563 return Ok(None);
6564 }
6565 let wait_deadline = Instant::now() + Duration::from_millis(wait_ms.min(25));
6566 loop {
6567 let Some(vm) = self.vms.get_mut(vm_id) else {
6568 return Ok(None);
6569 };
6570 if let Some(process_info) = vm.kernel.list_processes().get(&child_kernel_pid) {
6571 if process_info.status == ProcessStatus::Exited {
6572 return Ok(Some(ActiveExecutionEvent::Exited(
6573 process_info.exit_code.unwrap_or(0),
6574 )));
6575 }
6576 }
6577 if let Some(wait_result) = vm
6578 .kernel
6579 .waitpid_with_options(
6580 EXECUTION_DRIVER_NAME,
6581 parent_kernel_pid,
6582 child_kernel_pid as i32,
6583 WaitPidFlags::WNOHANG,
6584 )
6585 .map_err(kernel_error)?
6586 {
6587 return Ok(Some(ActiveExecutionEvent::Exited(wait_result.status)));
6588 }
6589
6590 if !child_shared_runtime && child_runtime_pid != 0 {
6591 if let Some(status) = runtime_child_exit_status(child_runtime_pid)? {
6592 return Ok(Some(ActiveExecutionEvent::Exited(status)));
6593 }
6594 if !runtime_child_is_alive(child_runtime_pid)? {
6595 return Ok(Some(ActiveExecutionEvent::Exited(0)));
6596 }
6597 }
6598 if Instant::now() >= wait_deadline {
6599 return Ok(None);
6600 }
6601 std::thread::sleep(Duration::from_millis(5));
6602 }
6603 }
6604
6605 fn write_descendant_javascript_child_process_stdin(
6606 &mut self,
6607 vm_id: &str,
6608 process_id: &str,
6609 current_process_path: &[&str],
6610 child_process_id: &str,
6611 chunk: &[u8],
6612 ) -> Result<(), SidecarError> {
6613 let mut child_path = current_process_path.to_vec();
6614 child_path.push(child_process_id);
6615 let Some(vm) = self.vms.get_mut(vm_id) else {
6616 return Err(javascript_child_process_gone_error(process_id, &child_path));
6617 };
6618 let Some(root) = vm.active_processes.get_mut(process_id) else {
6619 return Err(javascript_child_process_gone_error(process_id, &child_path));
6620 };
6621 let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
6622 return Err(javascript_child_process_gone_error(process_id, &child_path));
6623 };
6624 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6625 return Err(javascript_child_process_gone_error(process_id, &child_path));
6626 };
6627 if let Err(error) = child.execution.write_stdin(chunk) {
6628 if is_broken_pipe_error(&error) {
6629 return Ok(());
6630 }
6631 return Err(error);
6632 }
6633 write_kernel_process_stdin(&mut vm.kernel, child, chunk)
6634 }
6635
6636 fn close_descendant_javascript_child_process_stdin(
6637 &mut self,
6638 vm_id: &str,
6639 process_id: &str,
6640 current_process_path: &[&str],
6641 child_process_id: &str,
6642 ) -> Result<(), SidecarError> {
6643 let mut child_path = current_process_path.to_vec();
6644 child_path.push(child_process_id);
6645 let Some(vm) = self.vms.get_mut(vm_id) else {
6646 return Err(javascript_child_process_gone_error(process_id, &child_path));
6647 };
6648 let Some(root) = vm.active_processes.get_mut(process_id) else {
6649 return Err(javascript_child_process_gone_error(process_id, &child_path));
6650 };
6651 let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
6652 return Err(javascript_child_process_gone_error(process_id, &child_path));
6653 };
6654 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6655 return Err(javascript_child_process_gone_error(process_id, &child_path));
6656 };
6657 child.execution.close_stdin()?;
6658 close_kernel_process_stdin(&mut vm.kernel, child)
6659 }
6660
6661 fn kill_descendant_javascript_child_process(
6662 &mut self,
6663 vm_id: &str,
6664 process_id: &str,
6665 current_process_path: &[&str],
6666 child_process_id: &str,
6667 signal: &str,
6668 ) -> Result<(), SidecarError> {
6669 let signal_name = signal.to_owned();
6670 let signal = parse_signal(signal)?;
6671 let Some(vm) = self.vms.get_mut(vm_id) else {
6672 return Ok(());
6673 };
6674 let Some(root) = vm.active_processes.get_mut(process_id) else {
6675 return Ok(());
6676 };
6677 let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else {
6678 return Ok(());
6679 };
6680 let source_pid = parent.kernel_pid;
6681 let Some(child) = parent.child_processes.get_mut(child_process_id) else {
6682 return Ok(());
6683 };
6684 terminate_tracked_child_process_for_signal(&mut vm.kernel, child, signal)?;
6685 let child_process_label = if current_process_path.is_empty() {
6686 child_process_id.to_owned()
6687 } else {
6688 format!("{}/{}", current_process_path.join("/"), child_process_id)
6689 };
6690 emit_security_audit_event(
6691 &self.bridge,
6692 vm_id,
6693 "security.process.kill",
6694 audit_fields([
6695 (String::from("source"), String::from("guest_child_process")),
6696 (String::from("source_pid"), source_pid.to_string()),
6697 (String::from("target_pid"), child.kernel_pid.to_string()),
6698 (String::from("process_id"), process_id.to_owned()),
6699 (String::from("child_process_id"), child_process_label),
6700 (String::from("signal"), signal_name),
6701 ]),
6702 );
6703 Ok(())
6704 }
6705
6706 fn handle_descendant_process_kill_rpc(
6707 &mut self,
6708 vm_id: &str,
6709 process_id: &str,
6710 current_process_path: &[&str],
6711 child_process_id: &str,
6712 request: &JavascriptSyncRpcRequest,
6713 ) -> Result<Value, SidecarError> {
6714 let target_pid = javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?;
6715 let signal_name = javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?;
6716 let signal = parse_signal(signal_name)?;
6717
6718 let mut source_path = current_process_path.to_vec();
6719 source_path.push(child_process_id);
6720
6721 if signal != 0 && target_pid < 0 {
6722 let pgid = target_pid.unsigned_abs();
6723 let caller_kernel_pid = {
6724 let Some(vm) = self.vms.get(vm_id) else {
6725 return Err(SidecarError::InvalidState(String::from(
6726 "ESRCH: unknown VM during process.kill",
6727 )));
6728 };
6729 let Some(root) = vm.active_processes.get(process_id) else {
6730 return Err(SidecarError::InvalidState(format!(
6731 "ESRCH: unknown process {process_id} during process.kill",
6732 )));
6733 };
6734 let Some(source) = Self::active_process_by_path(root, &source_path) else {
6735 return Err(SidecarError::InvalidState(format!(
6736 "ESRCH: unknown child process {child_process_id} during process.kill",
6737 )));
6738 };
6739 source.kernel_pid
6740 };
6741 let caller_is_member =
6742 self.signal_vm_process_group(vm_id, caller_kernel_pid, pgid, signal_name)?;
6743 if !caller_is_member {
6744 return Ok(Value::Null);
6745 }
6746 let Some(vm) = self.vms.get_mut(vm_id) else {
6747 return Ok(Value::Null);
6748 };
6749 let Some(root) = vm.active_processes.get_mut(process_id) else {
6750 return Ok(Value::Null);
6751 };
6752 let Some(source) = Self::active_process_by_path_mut(root, &source_path) else {
6753 return Ok(Value::Null);
6754 };
6755 source.pending_self_signal_exit = None;
6756 if !matches!(
6757 canonical_signal_name(signal),
6758 Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
6759 ) {
6760 source.pending_self_signal_exit = Some(signal);
6761 }
6762 return Ok(json!({
6763 "self": true,
6764 "action": "default",
6765 }));
6766 }
6767
6768 let Some(vm) = self.vms.get_mut(vm_id) else {
6769 return Err(SidecarError::InvalidState(String::from(
6770 "ESRCH: unknown VM during process.kill",
6771 )));
6772 };
6773
6774 if signal == 0 {
6775 vm.kernel
6776 .signal_process(EXECUTION_DRIVER_NAME, target_pid, signal)
6777 .map_err(kernel_error)?;
6778 return Ok(Value::Null);
6779 }
6780
6781 let target_kernel_pid = u32::try_from(target_pid).map_err(|_| {
6782 SidecarError::InvalidState(format!("EINVAL: invalid process pid {target_pid}"))
6783 })?;
6784 let (source_pid, located_target_path) = {
6785 let Some(root) = vm.active_processes.get(process_id) else {
6786 return Err(SidecarError::InvalidState(format!(
6787 "ESRCH: unknown process {process_id} during process.kill",
6788 )));
6789 };
6790 let Some(source) = Self::active_process_by_path(root, &source_path) else {
6791 return Err(SidecarError::InvalidState(format!(
6792 "ESRCH: unknown child process {child_process_id} during process.kill",
6793 )));
6794 };
6795 vm.kernel
6796 .signal_process(EXECUTION_DRIVER_NAME, target_pid, 0)
6797 .map_err(kernel_error)?;
6798 (
6799 source.kernel_pid,
6800 Self::active_process_path_by_kernel_pid(root, target_kernel_pid),
6801 )
6802 };
6803 let Some(target_path) = located_target_path else {
6804 self.signal_vm_kernel_pid(vm_id, target_kernel_pid, signal_name)?;
6808 return Ok(Value::Null);
6809 };
6810 let Some(vm) = self.vms.get_mut(vm_id) else {
6811 return Err(SidecarError::InvalidState(String::from(
6812 "ESRCH: unknown VM during process.kill",
6813 )));
6814 };
6815
6816 if source_pid == target_kernel_pid {
6817 let Some(root) = vm.active_processes.get_mut(process_id) else {
6818 return Ok(Value::Null);
6819 };
6820 let Some(source) = Self::active_process_by_path_mut(root, &source_path) else {
6821 return Ok(Value::Null);
6822 };
6823 source.pending_self_signal_exit = None;
6824 if !matches!(
6825 canonical_signal_name(signal),
6826 Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
6827 ) {
6828 source.pending_self_signal_exit = Some(signal);
6829 }
6830 return Ok(json!({
6831 "self": true,
6832 "action": "default",
6833 }));
6834 }
6835
6836 let signal_key = target_path.last().map(String::as_str).unwrap_or(process_id);
6837 let registration = vm
6838 .signal_states
6839 .get(signal_key)
6840 .and_then(|handlers| handlers.get(&(signal as u32)))
6841 .cloned();
6842
6843 let action = match registration
6844 .as_ref()
6845 .map(|registration| ®istration.action)
6846 {
6847 Some(SignalDispositionAction::Ignore) => "ignore",
6848 Some(SignalDispositionAction::User) => {
6849 let Some(root) = vm.active_processes.get_mut(process_id) else {
6850 return Ok(Value::Null);
6851 };
6852 let Some(target) = Self::active_process_by_owned_path_mut(root, &target_path)
6853 else {
6854 return Err(SidecarError::InvalidState(format!(
6855 "ESRCH: unknown process pid {target_pid}"
6856 )));
6857 };
6858 if let Some(session) = target.execution.javascript_v8_session_handle().filter(
6859 |_| matches!(&target.execution, ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime())
6860 || matches!(&target.execution, ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime()),
6861 ) {
6862 dispatch_v8_session_signal_async(session, signal);
6863 } else if !dispatch_v8_process_signal(target, signal)? {
6864 return Err(SidecarError::InvalidState(format!(
6865 "unsupported guest signal delivery for pid {target_pid}"
6866 )));
6867 }
6868 "user"
6869 }
6870 Some(SignalDispositionAction::Default) | None
6871 if matches!(
6872 canonical_signal_name(signal),
6873 Some("SIGWINCH" | "SIGCHLD" | "SIGURG")
6874 ) =>
6875 {
6876 "ignore"
6877 }
6878 Some(SignalDispositionAction::Default) | None => {
6879 let Some(root) = vm.active_processes.get_mut(process_id) else {
6880 return Ok(Value::Null);
6881 };
6882 let Some(target) = Self::active_process_by_owned_path_mut(root, &target_path)
6883 else {
6884 return Err(SidecarError::InvalidState(format!(
6885 "ESRCH: unknown process pid {target_pid}"
6886 )));
6887 };
6888 apply_active_process_default_signal(&mut vm.kernel, target, signal)?;
6889 "default"
6890 }
6891 };
6892
6893 let target_path_label = Self::child_process_path_label(
6894 process_id,
6895 &target_path.iter().map(String::as_str).collect::<Vec<_>>(),
6896 );
6897 emit_security_audit_event(
6898 &self.bridge,
6899 vm_id,
6900 "security.process.kill",
6901 audit_fields([
6902 (String::from("source"), String::from("guest_process")),
6903 (String::from("source_pid"), source_pid.to_string()),
6904 (String::from("target_pid"), target_pid.to_string()),
6905 (String::from("process_id"), process_id.to_owned()),
6906 (
6907 String::from("target_process_path"),
6908 target_path_label.clone(),
6909 ),
6910 (String::from("signal"), signal_name.to_owned()),
6911 ]),
6912 );
6913
6914 Ok(json!({
6915 "self": false,
6916 "action": action,
6917 "signal": signal_name,
6918 "number": signal,
6919 "targetProcessPath": target_path_label,
6920 }))
6921 }
6922
6923 pub(crate) fn poll_javascript_child_process(
6924 &mut self,
6925 vm_id: &str,
6926 process_id: &str,
6927 child_process_id: &str,
6928 wait_ms: u64,
6929 ) -> Result<Value, SidecarError> {
6930 self.poll_descendant_javascript_child_process(
6931 vm_id,
6932 process_id,
6933 &[],
6934 child_process_id,
6935 wait_ms,
6936 )
6937 }
6938
6939 pub(crate) fn write_javascript_child_process_stdin(
6940 &mut self,
6941 vm_id: &str,
6942 process_id: &str,
6943 child_process_id: &str,
6944 chunk: &[u8],
6945 ) -> Result<(), SidecarError> {
6946 let Some(vm) = self.vms.get_mut(vm_id) else {
6947 return Err(javascript_child_process_gone_error(
6948 process_id,
6949 &[child_process_id],
6950 ));
6951 };
6952 let Some(child) = vm
6953 .active_processes
6954 .get_mut(process_id)
6955 .ok_or_else(|| missing_process_error(vm_id, process_id))?
6956 .child_processes
6957 .get_mut(child_process_id)
6958 else {
6959 return Err(javascript_child_process_gone_error(
6960 process_id,
6961 &[child_process_id],
6962 ));
6963 };
6964 if let Err(error) = child.execution.write_stdin(chunk) {
6965 if is_broken_pipe_error(&error) {
6966 return Ok(());
6967 }
6968 return Err(error);
6969 }
6970 write_kernel_process_stdin(&mut vm.kernel, child, chunk)
6971 }
6972
6973 pub(crate) fn close_javascript_child_process_stdin(
6974 &mut self,
6975 vm_id: &str,
6976 process_id: &str,
6977 child_process_id: &str,
6978 ) -> Result<(), SidecarError> {
6979 let Some(vm) = self.vms.get_mut(vm_id) else {
6980 return Err(javascript_child_process_gone_error(
6981 process_id,
6982 &[child_process_id],
6983 ));
6984 };
6985 let Some(child) = vm
6986 .active_processes
6987 .get_mut(process_id)
6988 .ok_or_else(|| missing_process_error(vm_id, process_id))?
6989 .child_processes
6990 .get_mut(child_process_id)
6991 else {
6992 return Err(javascript_child_process_gone_error(
6993 process_id,
6994 &[child_process_id],
6995 ));
6996 };
6997 child.execution.close_stdin()?;
6998 close_kernel_process_stdin(&mut vm.kernel, child)
6999 }
7000
7001 pub(crate) fn kill_javascript_child_process(
7002 &mut self,
7003 vm_id: &str,
7004 process_id: &str,
7005 child_process_id: &str,
7006 signal: &str,
7007 ) -> Result<(), SidecarError> {
7008 let signal_name = signal.to_owned();
7009 let signal = parse_signal(signal)?;
7010 let Some(vm) = self.vms.get_mut(vm_id) else {
7011 return Ok(());
7012 };
7013 let process = vm
7014 .active_processes
7015 .get_mut(process_id)
7016 .ok_or_else(|| missing_process_error(vm_id, process_id))?;
7017 let source_pid = process.kernel_pid;
7018 let child = process
7019 .child_processes
7020 .get_mut(child_process_id)
7021 .ok_or_else(|| {
7022 SidecarError::InvalidState(format!(
7023 "unknown child process {child_process_id} during kill"
7024 ))
7025 })?;
7026 terminate_tracked_child_process_for_signal(&mut vm.kernel, child, signal)?;
7027 emit_security_audit_event(
7028 &self.bridge,
7029 vm_id,
7030 "security.process.kill",
7031 audit_fields([
7032 (String::from("source"), String::from("guest_child_process")),
7033 (String::from("source_pid"), source_pid.to_string()),
7034 (String::from("target_pid"), child.kernel_pid.to_string()),
7035 (String::from("process_id"), process_id.to_owned()),
7036 (
7037 String::from("child_process_id"),
7038 child_process_id.to_owned(),
7039 ),
7040 (String::from("signal"), signal_name),
7041 ]),
7042 );
7043 Ok(())
7044 }
7045
7046 pub(crate) fn signal_vm_kernel_pid(
7052 &mut self,
7053 vm_id: &str,
7054 target_kernel_pid: u32,
7055 signal_name: &str,
7056 ) -> Result<(), SidecarError> {
7057 let signal = parse_signal(signal_name)?;
7058 let located = {
7059 let Some(vm) = self.vms.get(vm_id) else {
7060 return Err(SidecarError::InvalidState(String::from(
7061 "ESRCH: unknown VM during process.kill",
7062 )));
7063 };
7064 let alive = vm
7065 .kernel
7066 .list_processes()
7067 .get(&target_kernel_pid)
7068 .is_some_and(|info| info.status != ProcessStatus::Exited);
7069 if !alive {
7070 return Err(SidecarError::InvalidState(format!(
7071 "ESRCH: no such process {target_kernel_pid}"
7072 )));
7073 }
7074 vm.active_processes.iter().find_map(|(process_id, root)| {
7075 Self::active_process_path_by_kernel_pid(root, target_kernel_pid)
7076 .map(|path| (process_id.clone(), path))
7077 })
7078 };
7079
7080 match located {
7081 Some((process_id, path)) if path.is_empty() => {
7082 self.kill_process_internal(vm_id, &process_id, signal_name)
7083 }
7084 Some((process_id, path)) => {
7085 let Some(vm) = self.vms.get_mut(vm_id) else {
7086 return Ok(());
7087 };
7088 let Some(root) = vm.active_processes.get_mut(&process_id) else {
7089 return Ok(());
7090 };
7091 let Some(target) = Self::active_process_by_owned_path_mut(root, &path) else {
7092 return Err(SidecarError::InvalidState(format!(
7093 "ESRCH: no such process {target_kernel_pid}"
7094 )));
7095 };
7096 terminate_tracked_child_process_for_signal(&mut vm.kernel, target, signal)?;
7097 emit_security_audit_event(
7098 &self.bridge,
7099 vm_id,
7100 "security.process.kill",
7101 audit_fields([
7102 (String::from("source"), String::from("guest_process")),
7103 (String::from("target_pid"), target_kernel_pid.to_string()),
7104 (String::from("process_id"), process_id),
7105 (String::from("signal"), signal_name.to_owned()),
7106 ]),
7107 );
7108 Ok(())
7109 }
7110 None => {
7111 let Some(vm) = self.vms.get_mut(vm_id) else {
7112 return Ok(());
7113 };
7114 let target_pid = i32::try_from(target_kernel_pid).map_err(|_| {
7115 SidecarError::InvalidState(format!(
7116 "EINVAL: invalid process pid {target_kernel_pid}"
7117 ))
7118 })?;
7119 vm.kernel
7120 .signal_process(EXECUTION_DRIVER_NAME, target_pid, signal)
7121 .map_err(kernel_error)?;
7122 emit_security_audit_event(
7123 &self.bridge,
7124 vm_id,
7125 "security.process.kill",
7126 audit_fields([
7127 (String::from("source"), String::from("guest_process")),
7128 (String::from("target_pid"), target_kernel_pid.to_string()),
7129 (String::from("signal"), signal_name.to_owned()),
7130 ]),
7131 );
7132 Ok(())
7133 }
7134 }
7135 }
7136
7137 pub(crate) fn signal_vm_process_group(
7142 &mut self,
7143 vm_id: &str,
7144 caller_kernel_pid: u32,
7145 pgid: u32,
7146 signal_name: &str,
7147 ) -> Result<bool, SidecarError> {
7148 parse_signal(signal_name)?;
7149 let members = {
7150 let Some(vm) = self.vms.get(vm_id) else {
7151 return Err(SidecarError::InvalidState(String::from(
7152 "ESRCH: unknown VM during process.kill",
7153 )));
7154 };
7155 vm.kernel
7156 .list_processes()
7157 .into_iter()
7158 .filter(|(_, info)| info.pgid == pgid && info.status != ProcessStatus::Exited)
7159 .map(|(pid, _)| pid)
7160 .collect::<Vec<_>>()
7161 };
7162 if members.is_empty() {
7163 return Err(SidecarError::InvalidState(format!(
7164 "ESRCH: no such process group {pgid}"
7165 )));
7166 }
7167
7168 let mut caller_is_member = false;
7169 for member_pid in members {
7170 if member_pid == caller_kernel_pid {
7171 caller_is_member = true;
7172 continue;
7173 }
7174 match self.signal_vm_kernel_pid(vm_id, member_pid, signal_name) {
7175 Ok(()) => {}
7176 Err(error) if sidecar_error_is_esrch(&error) => {}
7179 Err(error) => return Err(error),
7180 }
7181 }
7182 Ok(caller_is_member)
7183 }
7184}
7185
7186fn terminate_tracked_child_process_for_signal(
7191 kernel: &mut SidecarKernel,
7192 child: &mut ActiveProcess,
7193 signal: i32,
7194) -> Result<(), SidecarError> {
7195 let should_terminate_shared_runtime = child.execution.uses_shared_v8_runtime()
7196 && signal != 0
7197 && !matches!(
7198 signal,
7199 libc::SIGHUP
7200 | libc::SIGINT
7201 | libc::SIGTERM
7202 | libc::SIGCHLD
7203 | libc::SIGWINCH
7204 | libc::SIGSTOP
7205 | libc::SIGCONT
7206 );
7207 if should_terminate_shared_runtime {
7208 child.execution.terminate()?;
7209 child.pending_self_signal_exit = Some(signal);
7210 child.queue_pending_execution_event(ActiveExecutionEvent::Exited(128 + signal))?;
7211 } else {
7212 kernel
7213 .kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, signal)
7214 .map_err(kernel_error)?;
7215 }
7216 Ok(())
7217}
7218
7219fn sidecar_error_is_esrch(error: &SidecarError) -> bool {
7220 error.to_string().contains("ESRCH")
7221}
7222
7223fn apply_active_process_default_signal(
7224 kernel: &mut SidecarKernel,
7225 process: &mut ActiveProcess,
7226 signal: i32,
7227) -> Result<(), SidecarError> {
7228 if matches!(signal, libc::SIGSTOP | libc::SIGCONT) {
7229 return kernel
7230 .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, signal)
7231 .map_err(kernel_error);
7232 }
7233
7234 if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) {
7235 close_kernel_process_stdin(kernel, process)?;
7236 }
7237
7238 if process.execution.uses_shared_v8_runtime() {
7239 process.execution.terminate()?;
7240 if signal != 0 && matches!(process.execution, ActiveExecution::Wasm(_)) {
7241 process.queue_pending_execution_event(ActiveExecutionEvent::Exited(128 + signal))?;
7242 }
7243 return Ok(());
7244 }
7245
7246 kernel
7247 .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, signal)
7248 .map_err(kernel_error)
7249}
7250
7251fn map_wasm_signal_registration(
7252 registration: secure_exec_execution::wasm::WasmSignalHandlerRegistration,
7253) -> SignalHandlerRegistration {
7254 SignalHandlerRegistration {
7255 action: match registration.action {
7256 secure_exec_execution::wasm::WasmSignalDispositionAction::Default => {
7257 crate::protocol::SignalDispositionAction::Default
7258 }
7259 secure_exec_execution::wasm::WasmSignalDispositionAction::Ignore => {
7260 crate::protocol::SignalDispositionAction::Ignore
7261 }
7262 secure_exec_execution::wasm::WasmSignalDispositionAction::User => {
7263 crate::protocol::SignalDispositionAction::User
7264 }
7265 },
7266 mask: registration.mask,
7267 flags: registration.flags,
7268 }
7269}
7270
7271fn parse_process_signal_state_request(
7272 args: &[Value],
7273) -> Result<(u32, SignalHandlerRegistration), SidecarError> {
7274 let signal = javascript_sync_rpc_arg_u32(args, 0, "process.signal_state signal")?;
7275 let action = javascript_sync_rpc_arg_str(args, 1, "process.signal_state action")?;
7276 let mask_json = javascript_sync_rpc_arg_str(args, 2, "process.signal_state mask")?;
7277 let flags = javascript_sync_rpc_arg_u32(args, 3, "process.signal_state flags")?;
7278 let mask: Vec<u32> = serde_json::from_str(mask_json).map_err(|error| {
7279 SidecarError::InvalidState(format!(
7280 "process.signal_state mask must be valid JSON: {error}"
7281 ))
7282 })?;
7283 let action = match action.trim().to_ascii_lowercase().as_str() {
7284 "default" => SignalDispositionAction::Default,
7285 "ignore" => SignalDispositionAction::Ignore,
7286 "user" => SignalDispositionAction::User,
7287 other => {
7288 return Err(SidecarError::InvalidState(format!(
7289 "unsupported process.signal_state action {other}"
7290 )));
7291 }
7292 };
7293
7294 Ok((
7295 signal,
7296 SignalHandlerRegistration {
7297 action,
7298 mask,
7299 flags,
7300 },
7301 ))
7302}
7303
7304fn apply_process_signal_state_update(
7305 signal_states: &mut BTreeMap<String, BTreeMap<u32, SignalHandlerRegistration>>,
7306 process_id: &str,
7307 signal: u32,
7308 registration: SignalHandlerRegistration,
7309) {
7310 if registration.action == SignalDispositionAction::Default
7311 && registration.mask.is_empty()
7312 && registration.flags == 0
7313 {
7314 let remove_process_entry = signal_states
7315 .get_mut(process_id)
7316 .map(|handlers| {
7317 handlers.remove(&signal);
7318 handlers.is_empty()
7319 })
7320 .unwrap_or(false);
7321 if remove_process_entry {
7322 signal_states.remove(process_id);
7323 }
7324 return;
7325 }
7326
7327 signal_states
7328 .entry(process_id.to_owned())
7329 .or_default()
7330 .insert(signal, registration);
7331}
7332
7333fn map_node_signal_registration(
7334 registration: NodeSignalHandlerRegistration,
7335) -> SignalHandlerRegistration {
7336 SignalHandlerRegistration {
7337 action: match registration.action {
7338 NodeSignalDispositionAction::Default => SignalDispositionAction::Default,
7339 NodeSignalDispositionAction::Ignore => SignalDispositionAction::Ignore,
7340 NodeSignalDispositionAction::User => SignalDispositionAction::User,
7341 },
7342 mask: registration.mask,
7343 flags: registration.flags,
7344 }
7345}
7346
7347fn javascript_child_process_sync_input_bytes(
7348 value: Option<&Value>,
7349) -> Result<Option<Vec<u8>>, SidecarError> {
7350 let Some(value) = value else {
7351 return Ok(None);
7352 };
7353
7354 match value {
7355 Value::Null => Ok(None),
7356 Value::String(text) => Ok(Some(text.as_bytes().to_vec())),
7357 other => javascript_sync_rpc_bytes_arg(
7358 std::slice::from_ref(other),
7359 0,
7360 "child_process.spawn_sync input",
7361 )
7362 .map(Some),
7363 }
7364}
7365
7366fn resolve_execute_request(
7371 vm: &VmState,
7372 payload: &ExecuteRequest,
7373) -> Result<ResolvedChildProcessExecution, SidecarError> {
7374 let payload_env: BTreeMap<String, String> = payload
7375 .env
7376 .iter()
7377 .map(|(k, v)| (k.clone(), v.clone()))
7378 .collect();
7379 if let Some(command) = payload.command.as_deref() {
7380 return resolve_command_execution(
7381 vm,
7382 command,
7383 &payload.args,
7384 &payload_env,
7385 payload.cwd.as_deref(),
7386 payload.wasm_permission_tier,
7387 );
7388 }
7389
7390 let runtime = payload.runtime.clone().ok_or_else(|| {
7391 SidecarError::InvalidState(String::from("execute requires either command or runtime"))
7392 })?;
7393 let entrypoint = payload.entrypoint.clone().ok_or_else(|| {
7394 SidecarError::InvalidState(String::from(
7395 "execute requires either command or entrypoint",
7396 ))
7397 })?;
7398 let (guest_cwd, host_cwd, allow_host_path_overrides) =
7399 resolve_execution_cwds(vm, payload.cwd.as_deref());
7400 let mut env = vm.guest_env.clone();
7401 env.extend(payload_env.clone());
7402
7403 let requested_host_entrypoint = resolve_host_entrypoint_within_vm_host_cwd(vm, &entrypoint);
7404 if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
7405 let requested_cwd = payload.cwd.as_deref().unwrap_or(guest_cwd.as_str());
7406 return Err(SidecarError::InvalidState(format!(
7407 "execution cwd {requested_cwd} is outside sandbox root {}",
7408 vm.host_cwd.to_string_lossy()
7409 )));
7410 }
7411 let host_entrypoint_override = allow_host_path_overrides
7412 .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, &entrypoint))
7413 .flatten();
7414
7415 let guest_entrypoint = host_entrypoint_override
7416 .as_ref()
7417 .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
7418 .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, &entrypoint));
7419 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
7420
7421 Ok(ResolvedChildProcessExecution {
7422 command: match runtime {
7423 GuestRuntimeKind::JavaScript => String::from(JAVASCRIPT_COMMAND),
7424 GuestRuntimeKind::Python => String::from(PYTHON_COMMAND),
7425 GuestRuntimeKind::WebAssembly => String::from(WASM_COMMAND),
7426 },
7427 process_args: std::iter::once(entrypoint.clone())
7428 .chain(payload.args.iter().cloned())
7429 .collect(),
7430 runtime,
7431 entrypoint: host_entrypoint_override
7432 .map(|(_, host_entrypoint)| host_entrypoint)
7433 .unwrap_or(entrypoint),
7434 execution_args: payload.args.clone(),
7435 env,
7436 guest_cwd,
7437 host_cwd,
7438 wasm_permission_tier: payload.wasm_permission_tier,
7439 tool_command: false,
7440 })
7441}
7442
7443fn resolve_command_execution(
7444 vm: &VmState,
7445 command: &str,
7446 args: &[String],
7447 extra_env: &BTreeMap<String, String>,
7448 cwd: Option<&str>,
7449 explicit_wasm_permission_tier: Option<WasmPermissionTier>,
7450) -> Result<ResolvedChildProcessExecution, SidecarError> {
7451 let (guest_cwd, host_cwd, allow_host_path_overrides) = resolve_execution_cwds(vm, cwd);
7452 let mut env = vm.guest_env.clone();
7453 env.extend(extra_env.clone());
7454 let args = apply_shell_cwd_prefix(command, args.to_vec(), &guest_cwd);
7455
7456 if is_tool_command(vm, command) {
7457 let command = normalized_tool_command_name(command).unwrap_or_else(|| command.to_owned());
7458 return Ok(ResolvedChildProcessExecution {
7459 command: command.clone(),
7460 process_args: std::iter::once(command.clone())
7461 .chain(args.iter().cloned())
7462 .collect(),
7463 runtime: GuestRuntimeKind::JavaScript,
7464 entrypoint: command,
7465 execution_args: args,
7466 env,
7467 guest_cwd,
7468 host_cwd,
7469 wasm_permission_tier: None,
7470 tool_command: true,
7471 });
7472 }
7473
7474 if is_node_runtime_command(command) {
7475 if let Some(cli) = resolve_host_node_cli_entrypoint(command) {
7476 env.insert(
7477 String::from("AGENTOS_NODE_EVAL"),
7478 build_host_node_cli_eval(&cli),
7479 );
7480 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
7481 add_runtime_guest_path_mapping(&mut env, &cli.guest_root, &cli.package_root);
7482 add_runtime_host_access_path(
7483 &mut env,
7484 "AGENTOS_EXTRA_FS_READ_PATHS",
7485 &cli.package_root,
7486 true,
7487 );
7488
7489 return Ok(ResolvedChildProcessExecution {
7490 command: String::from(JAVASCRIPT_COMMAND),
7491 process_args: std::iter::once(command.to_owned())
7492 .chain(args.iter().cloned())
7493 .collect(),
7494 runtime: GuestRuntimeKind::JavaScript,
7495 entrypoint: String::from("-e"),
7496 execution_args: std::iter::once(cli.guest_entrypoint.clone())
7497 .chain(args.iter().cloned())
7498 .collect(),
7499 env,
7500 guest_cwd,
7501 host_cwd,
7502 wasm_permission_tier: None,
7503 tool_command: false,
7504 });
7505 }
7506
7507 if args.is_empty() {
7508 env.insert(String::from("AGENTOS_NODE_EVAL"), String::new());
7509 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
7510
7511 return Ok(ResolvedChildProcessExecution {
7512 command: String::from(JAVASCRIPT_COMMAND),
7513 process_args: vec![command.to_owned()],
7514 runtime: GuestRuntimeKind::JavaScript,
7515 entrypoint: String::from("-e"),
7516 execution_args: Vec::new(),
7517 env,
7518 guest_cwd,
7519 host_cwd,
7520 wasm_permission_tier: None,
7521 tool_command: false,
7522 });
7523 }
7524
7525 if let Some((entrypoint, execution_args)) =
7526 resolve_special_node_cli_invocation(&args, &mut env)
7527 {
7528 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?;
7529
7530 return Ok(ResolvedChildProcessExecution {
7531 command: String::from(JAVASCRIPT_COMMAND),
7532 process_args: std::iter::once(command.to_owned())
7533 .chain(args.iter().cloned())
7534 .collect(),
7535 runtime: GuestRuntimeKind::JavaScript,
7536 entrypoint,
7537 execution_args,
7538 env,
7539 guest_cwd,
7540 host_cwd,
7541 wasm_permission_tier: None,
7542 tool_command: false,
7543 });
7544 }
7545
7546 let Some(entrypoint_specifier) = args.first() else {
7547 return Err(SidecarError::InvalidState(format!(
7548 "{command} execution requires an entrypoint"
7549 )));
7550 };
7551
7552 let (entrypoint, execution_args, guest_entrypoint) = {
7553 let requested_host_entrypoint =
7554 resolve_host_entrypoint_within_vm_host_cwd(vm, entrypoint_specifier);
7555 if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
7556 let requested_cwd = cwd.unwrap_or(guest_cwd.as_str());
7557 return Err(SidecarError::InvalidState(format!(
7558 "execution cwd {requested_cwd} is outside sandbox root {}",
7559 vm.host_cwd.to_string_lossy()
7560 )));
7561 }
7562 let host_entrypoint_override = allow_host_path_overrides
7563 .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, entrypoint_specifier))
7564 .flatten();
7565 let guest_entrypoint = host_entrypoint_override
7566 .as_ref()
7567 .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
7568 .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, entrypoint_specifier));
7569 let entrypoint = host_entrypoint_override.map_or_else(
7570 || {
7571 guest_entrypoint.as_ref().map_or_else(
7572 || entrypoint_specifier.clone(),
7573 |guest_entrypoint| {
7574 resolve_vm_guest_path_to_host(vm, guest_entrypoint)
7575 .to_string_lossy()
7576 .into_owned()
7577 },
7578 )
7579 },
7580 |(_, host_entrypoint)| host_entrypoint,
7581 );
7582 (
7583 entrypoint,
7584 args.iter().skip(1).cloned().collect(),
7585 guest_entrypoint,
7586 )
7587 };
7588
7589 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
7590
7591 return Ok(ResolvedChildProcessExecution {
7592 command: String::from(JAVASCRIPT_COMMAND),
7593 process_args: std::iter::once(command.to_owned())
7594 .chain(args.iter().cloned())
7595 .collect(),
7596 runtime: GuestRuntimeKind::JavaScript,
7597 entrypoint,
7598 execution_args,
7599 env,
7600 guest_cwd,
7601 host_cwd,
7602 wasm_permission_tier: None,
7603 tool_command: false,
7604 });
7605 }
7606
7607 if command.ends_with(".js") || command.ends_with(".mjs") || command.ends_with(".cjs") {
7608 let requested_host_entrypoint = resolve_host_entrypoint_within_vm_host_cwd(vm, command);
7609 if requested_host_entrypoint.is_some() && !allow_host_path_overrides {
7610 let requested_cwd = cwd.unwrap_or(guest_cwd.as_str());
7611 return Err(SidecarError::InvalidState(format!(
7612 "execution cwd {requested_cwd} is outside sandbox root {}",
7613 vm.host_cwd.to_string_lossy()
7614 )));
7615 }
7616 let host_entrypoint_override = allow_host_path_overrides
7617 .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, command))
7618 .flatten();
7619 let guest_entrypoint = host_entrypoint_override
7620 .as_ref()
7621 .map(|(guest_entrypoint, _)| guest_entrypoint.clone())
7622 .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, command));
7623 let entrypoint = host_entrypoint_override.map_or_else(
7624 || {
7625 guest_entrypoint.as_ref().map_or_else(
7626 || command.to_owned(),
7627 |guest_entrypoint| {
7628 resolve_vm_guest_path_to_host(vm, guest_entrypoint)
7629 .to_string_lossy()
7630 .into_owned()
7631 },
7632 )
7633 },
7634 |(_, host_entrypoint)| host_entrypoint,
7635 );
7636 prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?;
7637
7638 return Ok(ResolvedChildProcessExecution {
7639 command: String::from(JAVASCRIPT_COMMAND),
7640 process_args: std::iter::once(command.to_owned())
7641 .chain(args.iter().cloned())
7642 .collect(),
7643 runtime: GuestRuntimeKind::JavaScript,
7644 entrypoint,
7645 execution_args: args.to_vec(),
7646 env,
7647 guest_cwd,
7648 host_cwd,
7649 wasm_permission_tier: None,
7650 tool_command: false,
7651 });
7652 }
7653
7654 let guest_entrypoint = resolve_guest_command_entrypoint(
7655 vm,
7656 &guest_cwd,
7657 command,
7658 env.get("PATH").map(String::as_str),
7659 )
7660 .ok_or_else(|| {
7661 SidecarError::InvalidState(format!(
7662 "command not found on native sidecar path: {command}"
7663 ))
7664 })?;
7665 let wasm_permission_tier = explicit_wasm_permission_tier
7666 .or_else(|| vm.command_permissions.get(command).copied())
7667 .or_else(|| {
7668 Path::new(&guest_entrypoint)
7669 .file_name()
7670 .and_then(|name| name.to_str())
7671 .and_then(|name| vm.command_permissions.get(name).copied())
7672 });
7673
7674 let host_entrypoint = resolve_vm_guest_path_to_host(vm, &guest_entrypoint);
7675 if let Some((javascript_guest_entrypoint, javascript_host_entrypoint)) =
7676 resolve_javascript_command_entrypoint(vm, &guest_entrypoint, &host_entrypoint)
7677 {
7678 prepare_guest_runtime_env(
7679 vm,
7680 &mut env,
7681 &guest_cwd,
7682 &host_cwd,
7683 Some(javascript_guest_entrypoint),
7684 )?;
7685
7686 return Ok(ResolvedChildProcessExecution {
7687 command: command.to_owned(),
7688 process_args: std::iter::once(command.to_owned())
7689 .chain(args.iter().cloned())
7690 .collect(),
7691 runtime: GuestRuntimeKind::JavaScript,
7692 entrypoint: javascript_host_entrypoint.to_string_lossy().into_owned(),
7693 execution_args: args.to_vec(),
7694 env,
7695 guest_cwd,
7696 host_cwd,
7697 wasm_permission_tier: None,
7698 tool_command: false,
7699 });
7700 }
7701 prepare_guest_runtime_env(
7702 vm,
7703 &mut env,
7704 &guest_cwd,
7705 &host_cwd,
7706 Some(guest_entrypoint.clone()),
7707 )?;
7708
7709 Ok(ResolvedChildProcessExecution {
7710 command: command.to_owned(),
7711 process_args: std::iter::once(command.to_owned())
7712 .chain(args.iter().cloned())
7713 .collect(),
7714 runtime: GuestRuntimeKind::WebAssembly,
7715 entrypoint: host_entrypoint.to_string_lossy().into_owned(),
7716 execution_args: args.to_vec(),
7717 env,
7718 guest_cwd,
7719 host_cwd,
7720 wasm_permission_tier,
7721 tool_command: false,
7722 })
7723}
7724
7725const MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH: usize = 4;
7726
7727fn resolve_javascript_command_entrypoint(
7728 vm: &VmState,
7729 guest_entrypoint: &str,
7730 host_entrypoint: &Path,
7731) -> Option<(String, PathBuf)> {
7732 resolve_javascript_command_entrypoint_inner(
7733 vm,
7734 guest_entrypoint,
7735 host_entrypoint,
7736 MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH,
7737 )
7738}
7739
7740fn resolve_javascript_command_entrypoint_inner(
7741 vm: &VmState,
7742 guest_entrypoint: &str,
7743 host_entrypoint: &Path,
7744 redirects_remaining: usize,
7745) -> Option<(String, PathBuf)> {
7746 if redirects_remaining > 0 {
7747 let symlink_target = fs::symlink_metadata(host_entrypoint)
7748 .ok()
7749 .filter(|metadata| metadata.file_type().is_symlink())
7750 .and_then(|_| fs::read_link(host_entrypoint).ok());
7751 if let Some(symlink_target) = symlink_target {
7752 let guest_parent = Path::new(guest_entrypoint)
7753 .parent()
7754 .and_then(|path| path.to_str())
7755 .unwrap_or("/");
7756 let symlink_guest_entrypoint = if symlink_target.is_absolute() {
7757 normalize_path(&symlink_target.to_string_lossy())
7758 } else {
7759 normalize_path(&format!(
7760 "{guest_parent}/{}",
7761 symlink_target.to_string_lossy().replace('\\', "/")
7762 ))
7763 };
7764 let symlink_host_entrypoint =
7765 resolve_vm_guest_path_to_host(vm, &symlink_guest_entrypoint);
7766 return resolve_javascript_command_entrypoint_inner(
7767 vm,
7768 &symlink_guest_entrypoint,
7769 &symlink_host_entrypoint,
7770 redirects_remaining - 1,
7771 );
7772 }
7773 }
7774
7775 let script = load_executable_script_preview(host_entrypoint)?;
7776 let interpreter = parse_script_interpreter_name(&script);
7777
7778 if interpreter.is_none() && is_probable_javascript_entrypoint(host_entrypoint, &script) {
7779 return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf()));
7780 }
7781
7782 let interpreter = interpreter?;
7783 if interpreter == "node" {
7784 return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf()));
7785 }
7786
7787 if redirects_remaining == 0 || !matches!(interpreter.as_str(), "sh" | "bash" | "dash") {
7788 return None;
7789 }
7790
7791 let shim_target = parse_node_shell_shim_target(&script)?;
7792 let guest_parent = Path::new(guest_entrypoint)
7793 .parent()
7794 .and_then(|path| path.to_str())
7795 .unwrap_or("/");
7796 let shim_guest_entrypoint = normalize_path(&format!("{guest_parent}/{shim_target}"));
7797 let shim_host_entrypoint = resolve_vm_guest_path_to_host(vm, &shim_guest_entrypoint);
7798 resolve_javascript_command_entrypoint_inner(
7799 vm,
7800 &shim_guest_entrypoint,
7801 &shim_host_entrypoint,
7802 redirects_remaining - 1,
7803 )
7804}
7805
7806fn load_executable_script_preview(path: &Path) -> Option<String> {
7807 let bytes = fs::read(path).ok()?;
7808 let preview_len = bytes.len().min(16 * 1024);
7809 Some(String::from_utf8_lossy(&bytes[..preview_len]).into_owned())
7810}
7811
7812fn parse_script_interpreter_name(script: &str) -> Option<String> {
7813 let shebang = script.lines().next()?.strip_prefix("#!")?.trim();
7814 let mut tokens = shebang.split_whitespace();
7815 let command = tokens.next()?;
7816 let command_name = Path::new(command).file_name()?.to_str()?;
7817 if command_name == "env" {
7818 for token in tokens {
7819 if token.starts_with('-') {
7820 continue;
7821 }
7822 return Path::new(token)
7823 .file_name()
7824 .and_then(|name| name.to_str())
7825 .map(ToOwned::to_owned);
7826 }
7827 return None;
7828 }
7829
7830 Some(command_name.to_owned())
7831}
7832
7833fn parse_node_shell_shim_target(script: &str) -> Option<String> {
7834 for line in script.lines() {
7835 let trimmed = line.trim();
7836 if !trimmed.starts_with("exec ") {
7837 continue;
7838 }
7839
7840 let mut remaining = trimmed;
7841 while let Some(start) = remaining.find("\"$basedir/") {
7842 let after_prefix = &remaining[start + "\"$basedir/".len()..];
7843 let end = after_prefix.find('"')?;
7844 let candidate = &after_prefix[..end];
7845 remaining = &after_prefix[end + 1..];
7846
7847 if candidate.is_empty() || candidate == "node" || candidate.ends_with("/node") {
7848 continue;
7849 }
7850
7851 return Some(candidate.to_owned());
7852 }
7853 }
7854
7855 None
7856}
7857
7858fn is_probable_javascript_entrypoint(path: &Path, script: &str) -> bool {
7859 let extension = path
7860 .extension()
7861 .and_then(|value| value.to_str())
7862 .unwrap_or_default();
7863 if matches!(extension, "js" | "cjs" | "mjs") {
7864 return true;
7865 }
7866
7867 if !path
7868 .components()
7869 .any(|component| component.as_os_str() == "node_modules")
7870 {
7871 return false;
7872 }
7873
7874 let preview = script.trim_start_matches('\u{feff}').trim_start();
7875 !preview.is_empty()
7876 && !preview.starts_with("#!")
7877 && (preview.starts_with("\"use strict\"")
7878 || preview.starts_with("'use strict'")
7879 || preview.starts_with("import ")
7880 || preview.starts_with("export ")
7881 || preview.starts_with("const ")
7882 || preview.starts_with("let ")
7883 || preview.starts_with("var ")
7884 || preview.starts_with("Object.defineProperty(exports")
7885 || preview.starts_with("module.exports")
7886 || preview.starts_with("require("))
7887}
7888
7889fn resolve_guest_execution_cwd(vm: &VmState, value: Option<&str>) -> String {
7890 value
7891 .map(normalize_path)
7892 .unwrap_or_else(|| vm.guest_cwd.clone())
7893}
7894
7895fn resolve_execution_cwds(vm: &VmState, value: Option<&str>) -> (String, PathBuf, bool) {
7896 if let Some(raw_cwd) = value {
7897 let normalized_vm_host_cwd = normalize_host_path(&vm.host_cwd);
7898 let requested_host_cwd = normalize_host_path(Path::new(raw_cwd));
7899 if path_is_within_root(&requested_host_cwd, &normalized_vm_host_cwd) {
7900 let relative = requested_host_cwd
7901 .strip_prefix(&normalized_vm_host_cwd)
7902 .unwrap_or_else(|_| Path::new(""));
7903 let relative = relative.to_string_lossy().replace('\\', "/");
7904 let guest_cwd = if relative.is_empty() {
7905 String::from("/")
7906 } else {
7907 normalize_path(&format!("/{relative}"))
7908 };
7909 return (guest_cwd, requested_host_cwd, true);
7910 }
7911 }
7912
7913 let guest_cwd = resolve_guest_execution_cwd(vm, value);
7914 let host_cwd = if value.is_none() {
7915 vm.host_cwd.clone()
7916 } else {
7917 resolve_vm_guest_path_to_host(vm, &guest_cwd)
7918 };
7919 (guest_cwd, host_cwd, value.is_none())
7920}
7921
7922fn resolve_vm_guest_path_to_host(vm: &VmState, guest_path: &str) -> PathBuf {
7923 host_mount_path_for_guest_path(vm, guest_path)
7924 .unwrap_or_else(|| shadow_path_for_guest(vm, guest_path))
7925}
7926
7927fn shadow_path_for_guest(vm: &VmState, guest_path: &str) -> PathBuf {
7928 let normalized = normalize_path(guest_path);
7929 let relative = normalized.trim_start_matches('/');
7930 if relative.is_empty() {
7931 return vm.cwd.clone();
7932 }
7933 vm.cwd.join(relative)
7934}
7935
7936fn apply_shell_cwd_prefix(command: &str, mut args: Vec<String>, guest_cwd: &str) -> Vec<String> {
7937 if guest_cwd == "/" || !is_shell_command(command) {
7938 return args;
7939 }
7940
7941 let Some(flag) = args.first() else {
7942 return args;
7943 };
7944 if !matches!(flag.as_str(), "-c" | "-lc") || args.len() < 2 {
7945 return args;
7946 }
7947
7948 let command_text = args[1].clone();
7949 let quoted_cwd = shell_single_quote(guest_cwd);
7950 args[1] = format!("cd {quoted_cwd} && {command_text}");
7951 args
7952}
7953
7954fn is_shell_command(command: &str) -> bool {
7955 Path::new(command)
7956 .file_name()
7957 .and_then(|name| name.to_str())
7958 .unwrap_or(command)
7959 .trim_end_matches(".exe")
7960 .eq("sh")
7961 || Path::new(command)
7962 .file_name()
7963 .and_then(|name| name.to_str())
7964 .unwrap_or(command)
7965 .trim_end_matches(".exe")
7966 .eq("bash")
7967}
7968
7969fn shell_single_quote(value: &str) -> String {
7970 if value.is_empty() {
7971 return String::from("''");
7972 }
7973 format!("'{}'", value.replace('\'', "'\"'\"'"))
7974}
7975
7976pub(crate) fn sync_active_process_host_writes_to_kernel(
7977 vm: &mut VmState,
7978) -> Result<(), SidecarError> {
7979 if vm.root_filesystem_mode != RootFilesystemMode::ReadOnly {
7980 let shadow_root = vm.cwd.clone();
7981 sync_host_directory_tree_to_kernel(vm, &shadow_root, "/")?;
7982 }
7983
7984 let normalized_vm_root = normalize_host_path(&vm.cwd);
7985 let extra_roots = collect_active_process_host_sync_roots(vm, &normalized_vm_root);
7986 for (host_cwd, guest_cwd) in extra_roots {
7987 sync_host_directory_tree_to_kernel(vm, &host_cwd, &guest_cwd)?;
7988 }
7989
7990 Ok(())
7991}
7992
7993fn collect_active_process_host_sync_roots(
7994 vm: &VmState,
7995 normalized_vm_root: &Path,
7996) -> Vec<(PathBuf, String)> {
7997 let mut roots = Vec::new();
7998 let mut seen = BTreeSet::new();
7999
8000 for process in vm.active_processes.values() {
8001 collect_process_host_sync_roots(process, normalized_vm_root, &mut seen, &mut roots);
8002 }
8003
8004 roots
8005}
8006
8007fn collect_process_host_sync_roots(
8008 process: &ActiveProcess,
8009 normalized_vm_root: &Path,
8010 seen: &mut BTreeSet<(PathBuf, String)>,
8011 roots: &mut Vec<(PathBuf, String)>,
8012) {
8013 let normalized_host_cwd = normalize_host_path(&process.host_cwd);
8014 if !path_is_within_root(&normalized_host_cwd, normalized_vm_root) {
8015 let guest_cwd = normalize_path(&process.guest_cwd);
8016 if seen.insert((normalized_host_cwd.clone(), guest_cwd.clone())) {
8017 roots.push((normalized_host_cwd, guest_cwd));
8018 }
8019 }
8020
8021 for child in process.child_processes.values() {
8022 collect_process_host_sync_roots(child, normalized_vm_root, seen, roots);
8023 }
8024}
8025
8026fn sync_process_host_writes_to_kernel(
8027 vm: &mut VmState,
8028 process: &ActiveProcess,
8029) -> Result<(), SidecarError> {
8030 if vm.root_filesystem_mode != RootFilesystemMode::ReadOnly {
8031 let shadow_root = vm.cwd.clone();
8032 sync_host_directory_tree_to_kernel(vm, &shadow_root, "/")?;
8033 }
8034
8035 if !path_is_within_root(
8036 &normalize_host_path(&process.host_cwd),
8037 &normalize_host_path(&vm.cwd),
8038 ) {
8039 sync_host_directory_tree_to_kernel(vm, &process.host_cwd, &process.guest_cwd)?;
8040 }
8041
8042 Ok(())
8043}
8044
8045fn sync_host_directory_tree_to_kernel(
8046 vm: &mut VmState,
8047 host_root: &Path,
8048 guest_root: &str,
8049) -> Result<(), SidecarError> {
8050 let normalized_host_root = normalize_host_path(host_root);
8051 let normalized_guest_root = normalize_path(guest_root);
8052 let mut synced_file_times = BTreeMap::new();
8053 sync_host_directory_tree_to_kernel_inner(
8054 vm,
8055 &normalized_host_root,
8056 &normalized_host_root,
8057 &normalized_guest_root,
8058 &mut synced_file_times,
8059 )
8060}
8061
8062fn sync_host_directory_tree_to_kernel_inner(
8063 vm: &mut VmState,
8064 host_root: &Path,
8065 current_host_dir: &Path,
8066 guest_root: &str,
8067 synced_file_times: &mut BTreeMap<(u64, u64), (u64, u64)>,
8068) -> Result<(), SidecarError> {
8069 let entries = match fs::read_dir(current_host_dir) {
8070 Ok(entries) => entries,
8071 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
8072 Err(error) => {
8073 return Err(SidecarError::Io(format!(
8074 "failed to read host shadow directory {}: {error}",
8075 current_host_dir.display()
8076 )));
8077 }
8078 };
8079
8080 for entry in entries {
8081 let entry = entry.map_err(|error| {
8082 SidecarError::Io(format!(
8083 "failed to read host shadow entry in {}: {error}",
8084 current_host_dir.display()
8085 ))
8086 })?;
8087 let host_path = entry.path();
8088 let file_type = entry.file_type().map_err(|error| {
8089 SidecarError::Io(format!(
8090 "failed to stat host shadow entry {}: {error}",
8091 host_path.display()
8092 ))
8093 })?;
8094 let relative_path = host_path
8095 .strip_prefix(host_root)
8096 .map_err(|error| {
8097 SidecarError::InvalidState(format!(
8098 "failed to relativize host shadow path {} against {}: {error}",
8099 host_path.display(),
8100 host_root.display()
8101 ))
8102 })?
8103 .to_string_lossy()
8104 .replace('\\', "/");
8105 let guest_path = if guest_root == "/" {
8106 normalize_path(&format!("/{relative_path}"))
8107 } else {
8108 normalize_path(&format!(
8109 "{}/{}",
8110 guest_root.trim_end_matches('/'),
8111 relative_path
8112 ))
8113 };
8114
8115 if should_skip_shadow_sync_path(vm, &guest_path) {
8116 continue;
8117 }
8118
8119 if file_type.is_dir() {
8120 let metadata = entry.metadata().map_err(|error| {
8121 SidecarError::Io(format!(
8122 "failed to read host shadow metadata {}: {error}",
8123 host_path.display()
8124 ))
8125 })?;
8126 if !is_shadow_bootstrap_dir(&guest_path)
8127 && !vm.kernel.exists(&guest_path).unwrap_or(false)
8128 {
8129 vm.kernel.mkdir(&guest_path, true).map_err(|error| {
8130 SidecarError::InvalidState(format!(
8131 "failed to sync host shadow directory {} to guest {}: {}",
8132 host_path.display(),
8133 guest_path,
8134 kernel_error(error)
8135 ))
8136 })?;
8137 vm.kernel
8138 .chmod(&guest_path, host_shadow_mode(&metadata))
8139 .map_err(|error| {
8140 SidecarError::InvalidState(format!(
8141 "failed to sync host shadow directory mode {} to guest {}: {}",
8142 host_path.display(),
8143 guest_path,
8144 kernel_error(error)
8145 ))
8146 })?;
8147 }
8148 sync_host_directory_tree_to_kernel_inner(
8149 vm,
8150 host_root,
8151 &host_path,
8152 guest_root,
8153 synced_file_times,
8154 )?;
8155 continue;
8156 }
8157
8158 if file_type.is_file() {
8159 let metadata = entry.metadata().map_err(|error| {
8160 SidecarError::Io(format!(
8161 "failed to read host shadow metadata {}: {error}",
8162 host_path.display()
8163 ))
8164 })?;
8165 let timestamp_key = (metadata.dev(), metadata.ino());
8166 let (atime_ms, mtime_ms) =
8167 *synced_file_times.entry(timestamp_key).or_insert_with(|| {
8168 (
8169 metadata_time_ms(metadata.atime(), metadata.atime_nsec()),
8170 metadata_time_ms(metadata.mtime(), metadata.mtime_nsec()),
8171 )
8172 });
8173 let desired_mode = host_shadow_mode(&metadata);
8174 let bytes = read_host_shadow_file(&host_path, desired_mode).map_err(|error| {
8175 SidecarError::Io(format!(
8176 "failed to read host shadow file {}: {error}",
8177 host_path.display()
8178 ))
8179 })?;
8180 vm.kernel.write_file(&guest_path, bytes).map_err(|error| {
8181 SidecarError::InvalidState(format!(
8182 "failed to sync host shadow file {} to guest {}: {}",
8183 host_path.display(),
8184 guest_path,
8185 kernel_error(error)
8186 ))
8187 })?;
8188 vm.kernel
8189 .chmod(&guest_path, desired_mode)
8190 .map_err(|error| {
8191 SidecarError::InvalidState(format!(
8192 "failed to sync host shadow file mode {} to guest {}: {}",
8193 host_path.display(),
8194 guest_path,
8195 kernel_error(error)
8196 ))
8197 })?;
8198 vm.kernel
8199 .utimes(&guest_path, atime_ms, mtime_ms)
8200 .map_err(|error| {
8201 SidecarError::InvalidState(format!(
8202 "failed to sync host shadow file times {} to guest {}: {}",
8203 host_path.display(),
8204 guest_path,
8205 kernel_error(error)
8206 ))
8207 })?;
8208 continue;
8209 }
8210
8211 if file_type.is_symlink() {
8212 let target = match fs::read_link(&host_path) {
8213 Ok(target) => target,
8214 Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
8215 Err(error) => {
8216 return Err(SidecarError::Io(format!(
8217 "failed to read host shadow symlink {}: {error}",
8218 host_path.display()
8219 )));
8220 }
8221 };
8222 replace_kernel_symlink(vm, &guest_path, &target.to_string_lossy())?;
8223 }
8224 }
8225
8226 Ok(())
8227}
8228
8229fn replace_kernel_symlink(
8230 vm: &mut VmState,
8231 guest_path: &str,
8232 target: &str,
8233) -> Result<(), SidecarError> {
8234 if vm.kernel.symlink(target, guest_path).is_ok() {
8235 return Ok(());
8236 }
8237
8238 if let Ok(existing_target) = vm.kernel.read_link(guest_path) {
8239 if existing_target == target {
8240 return Ok(());
8241 }
8242 }
8243
8244 let _ = vm.kernel.remove_file(guest_path);
8245 let _ = vm.kernel.remove_dir(guest_path);
8246 vm.kernel
8247 .symlink(target, guest_path)
8248 .map_err(kernel_error)?;
8249 Ok(())
8250}
8251
8252fn host_shadow_mode(metadata: &fs::Metadata) -> u32 {
8253 metadata.permissions().mode() & 0o7777
8254}
8255
8256fn read_host_shadow_file(host_path: &Path, mode: u32) -> std::io::Result<Vec<u8>> {
8262 match fs::read(host_path) {
8263 Ok(bytes) => Ok(bytes),
8264 Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => {
8265 fs::set_permissions(host_path, fs::Permissions::from_mode(mode | 0o400))?;
8266 let result = fs::read(host_path);
8267 fs::set_permissions(host_path, fs::Permissions::from_mode(mode))?;
8268 result
8269 }
8270 Err(error) => Err(error),
8271 }
8272}
8273
8274fn metadata_time_ms(seconds: i64, nanos: i64) -> u64 {
8275 let seconds = seconds.max(0) as u64;
8276 let nanos = nanos.max(0) as u64;
8277 seconds
8278 .saturating_mul(1_000)
8279 .saturating_add(nanos / 1_000_000)
8280}
8281
8282fn is_shadow_bootstrap_dir(path: &str) -> bool {
8283 matches!(
8284 path,
8285 "/dev"
8286 | "/proc"
8287 | "/tmp"
8288 | "/bin"
8289 | "/lib"
8290 | "/sbin"
8291 | "/boot"
8292 | "/etc"
8293 | "/root"
8294 | "/run"
8295 | "/srv"
8296 | "/sys"
8297 | "/opt"
8298 | "/mnt"
8299 | "/media"
8300 | "/home"
8301 | "/home/agentos"
8302 | "/usr"
8303 | "/usr/bin"
8304 | "/usr/games"
8305 | "/usr/include"
8306 | "/usr/lib"
8307 | "/usr/libexec"
8308 | "/usr/man"
8309 | "/usr/local"
8310 | "/usr/local/bin"
8311 | "/usr/sbin"
8312 | "/usr/share"
8313 | "/usr/share/man"
8314 | "/var"
8315 | "/var/cache"
8316 | "/var/empty"
8317 | "/var/lib"
8318 | "/var/lock"
8319 | "/var/log"
8320 | "/var/run"
8321 | "/var/spool"
8322 | "/var/tmp"
8323 | "/etc/agentos"
8324 | "/workspace"
8325 )
8326}
8327
8328#[cfg(test)]
8329mod shadow_sync_tests {
8330 use super::{is_protected_agentos_shadow_sync_path, is_shadow_bootstrap_dir};
8331
8332 #[test]
8333 fn shadow_bootstrap_sync_skips_virtual_home_tree() {
8334 assert!(is_shadow_bootstrap_dir("/home"));
8335 assert!(is_shadow_bootstrap_dir("/home/agentos"));
8336 }
8337
8338 #[test]
8339 fn protected_agentos_paths_are_not_shadow_synced() {
8340 assert!(is_protected_agentos_shadow_sync_path("/etc/agentos"));
8341 assert!(is_protected_agentos_shadow_sync_path(
8342 "/etc/agentos/instructions.md"
8343 ));
8344 assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos-copy"));
8345 assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos.md"));
8346 }
8347}
8348
8349fn is_kernel_owned_shadow_sync_path(path: &str) -> bool {
8350 matches!(path, "/dev" | "/proc" | "/sys")
8351 || path.starts_with("/dev/")
8352 || path.starts_with("/proc/")
8353 || path.starts_with("/sys/")
8354}
8355
8356pub(crate) fn is_protected_agentos_shadow_sync_path(path: &str) -> bool {
8357 path == "/etc/agentos" || path.starts_with("/etc/agentos/")
8358}
8359
8360fn should_skip_shadow_sync_path(vm: &VmState, guest_path: &str) -> bool {
8361 is_kernel_owned_shadow_sync_path(guest_path)
8362 || is_protected_agentos_shadow_sync_path(guest_path)
8363 || host_mount_path_for_guest_path_from_mounts(&vm.configuration.mounts, guest_path)
8364 .is_some()
8365}
8366
8367fn resolve_path_like_guest_specifier(cwd: &str, specifier: &str) -> String {
8368 if specifier.starts_with("file://") {
8369 normalize_path(specifier.trim_start_matches("file://"))
8370 } else if specifier.starts_with("file:") {
8371 normalize_path(specifier.trim_start_matches("file:"))
8372 } else if specifier.starts_with('/') {
8373 normalize_path(specifier)
8374 } else {
8375 normalize_path(&format!("{cwd}/{specifier}"))
8376 }
8377}
8378
8379fn guest_entrypoint_for_specifier(cwd: &str, specifier: &str) -> Option<String> {
8380 is_path_like_specifier(specifier).then(|| resolve_path_like_guest_specifier(cwd, specifier))
8381}
8382
8383fn is_node_runtime_command(command: &str) -> bool {
8384 matches!(command, "node" | "npm" | "npx")
8385 || Path::new(command)
8386 .file_name()
8387 .and_then(|name| name.to_str())
8388 .is_some_and(|name| matches!(name, "node" | "npm" | "npx"))
8389}
8390
8391fn resolve_special_node_cli_invocation(
8392 args: &[String],
8393 env: &mut BTreeMap<String, String>,
8394) -> Option<(String, Vec<String>)> {
8395 let first = args.first()?;
8396 match first.as_str() {
8397 "-e" | "--eval" => {
8398 env.insert(
8399 String::from("AGENTOS_NODE_EVAL"),
8400 args.get(1).cloned().unwrap_or_default(),
8401 );
8402 Some((first.clone(), args.iter().skip(2).cloned().collect()))
8403 }
8404 "-v" | "--version" => {
8405 env.insert(
8406 String::from("AGENTOS_NODE_EVAL"),
8407 String::from("console.log(process.version);"),
8408 );
8409 Some((String::from("-e"), args.to_vec()))
8410 }
8411 _ => None,
8412 }
8413}
8414
8415fn node_runtime_command_name(command: &str) -> Option<&str> {
8416 let name = Path::new(command)
8417 .file_name()
8418 .and_then(|name| name.to_str())?;
8419 matches!(name, "node" | "npm" | "npx").then_some(name)
8420}
8421
8422struct ResolvedHostNodeCliEntrypoint {
8423 command_name: String,
8424 guest_root: String,
8425 guest_entrypoint: String,
8426 package_root: PathBuf,
8427}
8428
8429fn resolve_host_node_cli_entrypoint(command: &str) -> Option<ResolvedHostNodeCliEntrypoint> {
8430 let command_name = node_runtime_command_name(command)?;
8431 if !matches!(command_name, "npm" | "npx") {
8432 return None;
8433 }
8434
8435 let path = std::env::var_os("PATH")?;
8436 for root in std::env::split_paths(&path) {
8437 let candidate = root.join(command_name);
8438 if !candidate.is_file() {
8439 continue;
8440 }
8441 let entrypoint = candidate.canonicalize().ok().unwrap_or(candidate);
8442 let package_root = entrypoint.parent()?.parent()?.to_path_buf();
8443 let guest_root = format!("/__secure_exec/node-runtime/{command_name}");
8444 let relative_entrypoint = entrypoint.strip_prefix(&package_root).ok()?;
8445 let guest_entrypoint = normalize_path(&format!(
8446 "{guest_root}/{}",
8447 relative_entrypoint.to_string_lossy().replace('\\', "/")
8448 ));
8449 return Some(ResolvedHostNodeCliEntrypoint {
8450 command_name: command_name.to_owned(),
8451 guest_root,
8452 guest_entrypoint,
8453 package_root,
8454 });
8455 }
8456
8457 None
8458}
8459
8460fn build_host_node_cli_eval(cli: &ResolvedHostNodeCliEntrypoint) -> String {
8461 let guest_npm_main = normalize_path(&format!("{}/lib/npm.js", cli.guest_root));
8462 let guest_npm_cli = normalize_path(&format!("{}/bin/npm-cli.js", cli.guest_root));
8463 let guest_package_json = normalize_path(&format!("{}/package.json", cli.guest_root));
8464 let guest_display_module = normalize_path(&format!("{}/lib/utils/display.js", cli.guest_root));
8465 let guest_log_file_module =
8466 normalize_path(&format!("{}/lib/utils/log-file.js", cli.guest_root));
8467 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); } } }";
8468 let display_stub = format!(
8469 "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 }};",
8470 display_module = serde_json::to_string(&guest_display_module)
8471 .unwrap_or_else(|_| format!("\"{guest_display_module}\"")),
8472 log_file_module = serde_json::to_string(&guest_log_file_module)
8473 .unwrap_or_else(|_| format!("\"{guest_log_file_module}\"")),
8474 );
8475 let registry_fetch_stub = "const { createRequire: __agentOSCreateRequire } = require('module'); const __agentOSNpmRequire = __agentOSCreateRequire(require.resolve(__AGENTOS_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)); }";
8476 match cli.command_name.as_str() {
8477 "npx" => format!(
8478 "{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); }});",
8479 debug_preamble = debug_preamble,
8480 display_stub = display_stub,
8481 registry_fetch_stub = registry_fetch_stub.replace(
8482 "__AGENTOS_NPM_MAIN__",
8483 &serde_json::to_string(&guest_npm_main)
8484 .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8485 ),
8486 npm_main = serde_json::to_string(&guest_npm_main)
8487 .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8488 npm_cli = serde_json::to_string(&guest_npm_cli)
8489 .unwrap_or_else(|_| format!("\"{guest_npm_cli}\"")),
8490 package_json = serde_json::to_string(&guest_package_json)
8491 .unwrap_or_else(|_| format!("\"{guest_package_json}\"")),
8492 ),
8493 _ => format!(
8494 "{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); }});",
8495 debug_preamble = debug_preamble,
8496 display_stub = display_stub,
8497 registry_fetch_stub = registry_fetch_stub.replace(
8498 "__AGENTOS_NPM_MAIN__",
8499 &serde_json::to_string(&guest_npm_main)
8500 .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8501 ),
8502 npm_main = serde_json::to_string(&guest_npm_main)
8503 .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")),
8504 package_json = serde_json::to_string(&guest_package_json)
8505 .unwrap_or_else(|_| format!("\"{guest_package_json}\"")),
8506 ),
8507 }
8508}
8509
8510fn resolve_guest_command_entrypoint(
8511 vm: &VmState,
8512 guest_cwd: &str,
8513 command: &str,
8514 path_env: Option<&str>,
8515) -> Option<String> {
8516 if !is_path_like_specifier(command) {
8517 if let Some(entrypoint) = vm.command_guest_paths.get(command) {
8518 return Some(entrypoint.clone());
8519 }
8520
8521 for search_dir in guest_command_search_dirs(vm, guest_cwd, path_env) {
8522 let candidate = normalize_path(&format!("{search_dir}/{command}"));
8523 if let Some(entrypoint) = resolve_guest_command_path_candidate(vm, &candidate) {
8524 return Some(entrypoint);
8525 }
8526 }
8527
8528 return None;
8529 }
8530
8531 let normalized = resolve_path_like_guest_specifier(guest_cwd, command);
8532 resolve_guest_command_path_candidate(vm, &normalized).or_else(|| {
8533 let parent_dir = Path::new(&normalized).parent()?.to_str()?;
8537 if !guest_command_search_dirs(vm, guest_cwd, path_env)
8538 .iter()
8539 .any(|search_dir| normalize_path(search_dir) == normalize_path(parent_dir))
8540 {
8541 return None;
8542 }
8543
8544 let file_name = Path::new(&normalized).file_name()?.to_str()?;
8545 vm.command_guest_paths.get(file_name).cloned()
8546 })
8547}
8548
8549fn guest_command_search_dirs(vm: &VmState, guest_cwd: &str, path_env: Option<&str>) -> Vec<String> {
8550 let mut search_dirs = Vec::new();
8551 let mut seen = BTreeSet::new();
8552
8553 if let Some(path) = path_env.or_else(|| vm.guest_env.get("PATH").map(String::as_str)) {
8554 for segment in path.split(':') {
8555 let trimmed = segment.trim();
8556 if trimmed.is_empty() {
8557 continue;
8558 }
8559 let normalized = if trimmed.starts_with('/') {
8560 normalize_path(trimmed)
8561 } else {
8562 normalize_path(&format!("{guest_cwd}/{trimmed}"))
8563 };
8564 if seen.insert(normalized.clone()) {
8565 search_dirs.push(normalized);
8566 }
8567 }
8568 }
8569
8570 for fallback in ["/bin", "/usr/bin", "/usr/local/bin"] {
8571 let normalized = String::from(fallback);
8572 if seen.insert(normalized.clone()) {
8573 search_dirs.push(normalized);
8574 }
8575 }
8576
8577 search_dirs
8578}
8579
8580fn resolve_guest_command_path_candidate(vm: &VmState, candidate: &str) -> Option<String> {
8581 if candidate.starts_with("/bin/")
8582 || candidate.starts_with("/usr/bin/")
8583 || candidate.starts_with("/usr/local/bin/")
8584 || candidate.starts_with("/__secure_exec/commands/")
8585 {
8586 if let Some(file_name) = Path::new(candidate)
8587 .file_name()
8588 .and_then(|name| name.to_str())
8589 {
8590 if let Some(guest_entrypoint) = vm.command_guest_paths.get(file_name) {
8591 return Some(guest_entrypoint.clone());
8592 }
8593 }
8594 }
8595
8596 if vm
8597 .kernel
8598 .exists(candidate)
8599 .ok()
8600 .is_some_and(|exists| exists)
8601 {
8602 return Some(normalize_path(candidate));
8603 }
8604
8605 resolve_vm_guest_path_to_host(vm, candidate)
8606 .is_file()
8607 .then(|| normalize_path(candidate))
8608}
8609
8610fn resolve_host_entrypoint_within_vm_host_cwd(
8611 vm: &VmState,
8612 specifier: &str,
8613) -> Option<(String, String)> {
8614 let candidate = Path::new(specifier);
8615 if !candidate.is_absolute() {
8616 return None;
8617 }
8618
8619 let normalized_entrypoint = normalize_host_path(candidate);
8620 let normalized_host_cwd = normalize_host_path(&vm.host_cwd);
8621 if !path_is_within_root(&normalized_entrypoint, &normalized_host_cwd) {
8622 return None;
8623 }
8624
8625 let relative = normalized_entrypoint
8626 .strip_prefix(&normalized_host_cwd)
8627 .ok()?
8628 .to_string_lossy()
8629 .replace('\\', "/");
8630 let guest_entrypoint = if relative.is_empty() {
8631 String::from("/")
8632 } else {
8633 normalize_path(&format!("/{relative}"))
8634 };
8635 Some((
8636 guest_entrypoint,
8637 normalized_entrypoint.to_string_lossy().into_owned(),
8638 ))
8639}
8640
8641fn prepare_guest_runtime_env(
8642 vm: &VmState,
8643 env: &mut BTreeMap<String, String>,
8644 guest_cwd: &str,
8645 host_cwd: &Path,
8646 guest_entrypoint: Option<String>,
8647) -> Result<(), SidecarError> {
8648 let user = vm.kernel.user_profile();
8649 let path_mappings = runtime_guest_path_mappings(vm);
8650 let read_paths = expand_host_access_paths(
8651 std::iter::once(vm.cwd.clone())
8652 .chain(
8653 path_mappings
8654 .iter()
8655 .map(|mapping| PathBuf::from(&mapping.host_path)),
8656 )
8657 .chain(std::iter::once(host_cwd.to_path_buf()))
8658 .collect::<Vec<_>>()
8659 .as_slice(),
8660 );
8661 let write_paths = dedupe_host_paths(
8662 std::iter::once(vm.cwd.clone())
8663 .chain(std::iter::once(host_cwd.to_path_buf()))
8664 .chain(runtime_guest_writable_host_paths(vm))
8665 .collect::<Vec<_>>()
8666 .as_slice(),
8667 );
8668 let allowed_node_builtins = configured_allowed_node_builtins(vm);
8669 let loopback_exempt_ports = configured_loopback_exempt_ports(vm);
8670
8671 env.insert(
8672 String::from("AGENTOS_GUEST_PATH_MAPPINGS"),
8673 serde_json::to_string(&path_mappings).map_err(|error| {
8674 SidecarError::InvalidState(format!("failed to encode guest path mappings: {error}"))
8675 })?,
8676 );
8677 env.entry(String::from(EXECUTION_SANDBOX_ROOT_ENV))
8678 .or_insert_with(|| normalize_host_path(&vm.cwd).to_string_lossy().into_owned());
8679 env.insert(
8680 String::from("AGENTOS_EXTRA_FS_READ_PATHS"),
8681 serde_json::to_string(
8682 &read_paths
8683 .iter()
8684 .map(|path| path.to_string_lossy().into_owned())
8685 .collect::<Vec<_>>(),
8686 )
8687 .map_err(|error| {
8688 SidecarError::InvalidState(format!("failed to encode read paths: {error}"))
8689 })?,
8690 );
8691 env.insert(
8692 String::from("AGENTOS_EXTRA_FS_WRITE_PATHS"),
8693 serde_json::to_string(
8694 &write_paths
8695 .iter()
8696 .map(|path| path.to_string_lossy().into_owned())
8697 .collect::<Vec<_>>(),
8698 )
8699 .map_err(|error| {
8700 SidecarError::InvalidState(format!("failed to encode write paths: {error}"))
8701 })?,
8702 );
8703 env.insert(
8704 String::from("AGENTOS_ALLOWED_NODE_BUILTINS"),
8705 serde_json::to_string(&allowed_node_builtins).map_err(|error| {
8706 SidecarError::InvalidState(format!("failed to encode allowed builtins: {error}"))
8707 })?,
8708 );
8709 env.insert(
8712 String::from("AGENTOS_JS_PLATFORM"),
8713 js_runtime_platform_env(vm).to_owned(),
8714 );
8715 if let Some(resolution) = js_runtime_module_resolution_env(vm) {
8717 env.insert(
8718 String::from("AGENTOS_JS_MODULE_RESOLUTION"),
8719 resolution.to_owned(),
8720 );
8721 }
8722 if let Some(allowlist) = js_runtime_enforced_builtins(vm) {
8726 env.insert(
8727 String::from("AGENTOS_JS_BUILTIN_ALLOWLIST"),
8728 serde_json::to_string(&allowlist).map_err(|error| {
8729 SidecarError::InvalidState(format!(
8730 "failed to encode jsRuntime builtin allow-list: {error}"
8731 ))
8732 })?,
8733 );
8734 }
8735 env.entry(String::from("HOME"))
8742 .or_insert_with(|| user.homedir.clone());
8743 env.entry(String::from("USER"))
8744 .or_insert_with(|| user.username.clone());
8745 env.entry(String::from("LOGNAME"))
8746 .or_insert_with(|| user.username.clone());
8747 env.entry(String::from("SHELL"))
8748 .or_insert_with(|| user.shell.clone());
8749 env.entry(String::from("PATH")).or_insert_with(|| {
8750 vm.guest_env
8751 .get("PATH")
8752 .cloned()
8753 .unwrap_or_else(|| crate::vm::DEFAULT_GUEST_PATH_ENV.to_owned())
8754 });
8755 env.entry(String::from("TMPDIR"))
8756 .or_insert_with(|| String::from("/tmp"));
8757 env.insert(String::from("PWD"), guest_cwd.to_owned());
8758 if !loopback_exempt_ports.is_empty() {
8759 env.insert(
8760 String::from(LOOPBACK_EXEMPT_PORTS_ENV),
8761 serde_json::to_string(&loopback_exempt_ports).map_err(|error| {
8762 SidecarError::InvalidState(format!("failed to encode loopback exemptions: {error}"))
8763 })?,
8764 );
8765 }
8766 if let Some(guest_entrypoint) = guest_entrypoint {
8767 env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint);
8768 }
8769 Ok(())
8770}
8771
8772fn virtual_os_cpu_count(resource_limits: &ResourceLimits) -> usize {
8773 resource_limits.virtual_cpu_count.unwrap_or(1).max(1)
8774}
8775
8776fn virtual_os_totalmem_bytes(resource_limits: &ResourceLimits) -> u64 {
8777 resource_limits
8778 .max_wasm_memory_bytes
8779 .unwrap_or(1024 * 1024 * 1024)
8780}
8781
8782fn virtual_os_freemem_bytes(resource_limits: &ResourceLimits) -> u64 {
8783 resource_limits
8784 .max_wasm_memory_bytes
8785 .unwrap_or(512 * 1024 * 1024)
8786}
8787
8788fn javascript_execution_limits(vm: &VmState) -> JavascriptExecutionLimits {
8793 JavascriptExecutionLimits {
8794 v8_heap_limit_mb: vm.limits.js_runtime.v8_heap_limit_mb,
8795 sync_rpc_wait_timeout_ms: vm.limits.js_runtime.sync_rpc_wait_timeout_ms,
8796 }
8797}
8798
8799fn guest_runtime_identity(
8805 vm: &VmState,
8806 virtual_pid: Option<u64>,
8807 virtual_ppid: Option<u64>,
8808) -> GuestRuntimeConfig {
8809 let user = vm.kernel.user_profile();
8810 let resource_limits = vm.kernel.resource_limits();
8811 GuestRuntimeConfig {
8812 virtual_uid: Some(u64::from(user.uid)),
8813 virtual_gid: Some(u64::from(user.gid)),
8814 virtual_pid,
8815 virtual_ppid,
8816 virtual_exec_path: None,
8817 os_cpu_count: Some(virtual_os_cpu_count(resource_limits) as u64),
8818 os_totalmem: Some(virtual_os_totalmem_bytes(resource_limits)),
8819 os_freemem: Some(virtual_os_freemem_bytes(resource_limits)),
8820 os_homedir: Some(user.homedir.clone()),
8821 os_hostname: None,
8822 os_shell: Some(user.shell.clone()),
8823 os_user: Some(user.username.clone()),
8824 snapshot_userland_code: vm
8829 .configuration
8830 .js_runtime
8831 .as_ref()
8832 .and_then(|cfg| cfg.snapshot_userland_code.clone()),
8833 }
8834}
8835
8836fn guest_virtual_home(vm: &VmState) -> String {
8841 let homedir = vm.kernel.user_profile().homedir;
8842 if homedir.starts_with('/') {
8843 homedir
8844 } else {
8845 String::from("/root")
8846 }
8847}
8848
8849fn python_execution_limits(vm: &VmState) -> PythonExecutionLimits {
8851 PythonExecutionLimits {
8852 output_buffer_max_bytes: Some(vm.limits.python.output_buffer_max_bytes),
8853 execution_timeout_ms: Some(vm.limits.python.execution_timeout_ms),
8854 max_old_space_mb: Some(vm.limits.python.max_old_space_mb),
8855 vfs_rpc_timeout_ms: Some(vm.limits.python.vfs_rpc_timeout_ms),
8856 }
8857}
8858
8859fn wasm_execution_limits(vm: &VmState) -> WasmExecutionLimits {
8864 let resource_limits = vm.kernel.resource_limits();
8865 WasmExecutionLimits {
8866 max_fuel: resource_limits.max_wasm_fuel,
8867 max_memory_bytes: resource_limits.max_wasm_memory_bytes,
8868 max_stack_bytes: resource_limits
8869 .max_wasm_stack_bytes
8870 .map(|value| value as u64),
8871 }
8872}
8873
8874fn js_runtime_platform(vm: &VmState) -> vm_config::JsRuntimePlatform {
8877 vm.configuration
8878 .js_runtime
8879 .as_ref()
8880 .map(|cfg| cfg.platform)
8881 .unwrap_or(vm_config::JsRuntimePlatform::Node)
8882}
8883
8884fn js_runtime_platform_env(vm: &VmState) -> &'static str {
8887 match js_runtime_platform(vm) {
8888 vm_config::JsRuntimePlatform::Node => "node",
8889 vm_config::JsRuntimePlatform::Browser => "browser",
8890 vm_config::JsRuntimePlatform::Neutral => "neutral",
8891 vm_config::JsRuntimePlatform::Bare => "bare",
8892 }
8893}
8894
8895fn js_runtime_module_resolution_env(vm: &VmState) -> Option<&'static str> {
8898 let resolution = vm
8899 .configuration
8900 .js_runtime
8901 .as_ref()
8902 .map(|cfg| cfg.module_resolution)
8903 .unwrap_or(vm_config::JsModuleResolution::Node);
8904 match resolution {
8905 vm_config::JsModuleResolution::Node => None,
8906 vm_config::JsModuleResolution::Relative => Some("relative"),
8907 vm_config::JsModuleResolution::None => Some("none"),
8908 }
8909}
8910
8911fn js_runtime_enforced_builtins(vm: &VmState) -> Option<Vec<String>> {
8915 if js_runtime_platform(vm) != vm_config::JsRuntimePlatform::Node {
8916 return Some(Vec::new());
8917 }
8918 vm.configuration
8919 .js_runtime
8920 .as_ref()
8921 .and_then(|cfg| cfg.allowed_builtins.clone())
8922}
8923
8924fn configured_allowed_node_builtins(vm: &VmState) -> Vec<String> {
8925 if js_runtime_platform(vm) != vm_config::JsRuntimePlatform::Node {
8927 return Vec::new();
8928 }
8929 let configured = match vm
8932 .configuration
8933 .js_runtime
8934 .as_ref()
8935 .and_then(|cfg| cfg.allowed_builtins.as_ref())
8936 {
8937 Some(list) => list.clone(),
8938 None => DEFAULT_ALLOWED_NODE_BUILTINS
8939 .iter()
8940 .map(|value| (*value).to_owned())
8941 .collect::<Vec<_>>(),
8942 };
8943 dedupe_strings(&configured)
8944}
8945
8946fn configured_loopback_exempt_ports(vm: &VmState) -> Vec<String> {
8947 if !vm.configuration.loopback_exempt_ports.is_empty() {
8948 return vm
8949 .configuration
8950 .loopback_exempt_ports
8951 .iter()
8952 .map(ToString::to_string)
8953 .collect();
8954 }
8955
8956 vm.create_loopback_exempt_ports
8957 .iter()
8958 .map(ToString::to_string)
8959 .collect()
8960}
8961
8962fn mount_config_host_path(config: &str) -> Option<String> {
8964 serde_json::from_str::<Value>(config)
8965 .ok()?
8966 .get("hostPath")
8967 .and_then(Value::as_str)
8968 .map(str::to_owned)
8969}
8970
8971fn runtime_guest_writable_host_paths(vm: &VmState) -> Vec<PathBuf> {
8972 vm.configuration
8973 .mounts
8974 .iter()
8975 .filter(|mount| !mount.read_only)
8976 .filter_map(|mount| {
8977 ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
8978 .then(|| mount_config_host_path(&mount.plugin.config))
8979 .flatten()
8980 .map(PathBuf::from)
8981 })
8982 .collect()
8983}
8984
8985fn runtime_guest_path_mappings(vm: &VmState) -> Vec<RuntimeGuestPathMapping> {
8986 let mut mappings = vm
8987 .configuration
8988 .mounts
8989 .iter()
8990 .filter_map(|mount| {
8991 ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
8992 .then(|| {
8993 mount_config_host_path(&mount.plugin.config).map(|host_path| {
8994 RuntimeGuestPathMapping {
8995 guest_path: normalize_path(&mount.guest_path),
8996 host_path,
8997 read_only: mount.read_only,
8998 }
8999 })
9000 })
9001 .flatten()
9002 })
9003 .collect::<Vec<_>>();
9004 let mut command_root_mappings = vm
9005 .command_guest_paths
9006 .values()
9007 .filter_map(|guest_path| {
9008 Path::new(guest_path)
9009 .parent()
9010 .and_then(|parent| parent.to_str())
9011 .map(normalize_path)
9012 })
9013 .collect::<BTreeSet<_>>()
9014 .into_iter()
9015 .map(|guest_path| RuntimeGuestPathMapping {
9016 host_path: resolve_vm_guest_path_to_host(vm, &guest_path)
9017 .to_string_lossy()
9018 .into_owned(),
9019 guest_path,
9020 read_only: false,
9021 })
9022 .collect::<Vec<_>>();
9023 mappings.append(&mut command_root_mappings);
9024 let mut extra_node_modules_roots = mappings
9025 .iter()
9026 .filter(|mapping| mapping.guest_path.starts_with("/root/node_modules/"))
9027 .filter_map(|mapping| {
9028 host_node_modules_root(Path::new(&mapping.host_path)).map(|host_root| {
9029 RuntimeGuestPathMapping {
9030 guest_path: String::from("/root/node_modules"),
9031 host_path: host_root.to_string_lossy().into_owned(),
9032 read_only: mapping.read_only,
9033 }
9034 })
9035 })
9036 .collect::<Vec<_>>();
9037 mappings.append(&mut extra_node_modules_roots);
9038 mappings.push(RuntimeGuestPathMapping {
9039 guest_path: String::from("/"),
9040 host_path: vm.cwd.to_string_lossy().into_owned(),
9041 read_only: false,
9042 });
9043 mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.guest_path.len()));
9044 mappings.dedup_by(|left, right| {
9045 left.guest_path == right.guest_path && left.host_path == right.host_path
9046 });
9047 mappings
9048}
9049
9050fn build_module_reader(
9061 vm: &VmState,
9062 resolved: &ResolvedChildProcessExecution,
9063) -> Option<crate::plugins::host_dir::HostDirModuleReader> {
9064 let mut pairs: Vec<(String, PathBuf)> = vm
9065 .configuration
9066 .mounts
9067 .iter()
9068 .filter(|mount| mount.read_only)
9069 .filter(|mount| (mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
9070 .filter_map(|mount| {
9071 mount_config_host_path(&mount.plugin.config)
9072 .map(|host_path| (normalize_path(&mount.guest_path), PathBuf::from(host_path)))
9073 })
9074 .collect();
9075
9076 let guest_entrypoint = resolved
9077 .env
9078 .get("AGENTOS_GUEST_ENTRYPOINT")
9079 .map(|path| normalize_path(path));
9080 if let Some(guest_entrypoint) = guest_entrypoint.as_deref() {
9081 let entrypoint_in_read_only_mount = pairs.iter().any(|(guest_path, _)| {
9082 guest_entrypoint == guest_path
9083 || guest_entrypoint.starts_with(&format!("{guest_path}/"))
9084 });
9085 if !entrypoint_in_read_only_mount {
9086 return None;
9087 }
9088 }
9089
9090 let extra_roots: Vec<(String, PathBuf)> = pairs
9094 .iter()
9095 .filter(|(guest_path, _)| guest_path.starts_with("/root/node_modules/"))
9096 .filter_map(|(_, host_path)| {
9097 host_node_modules_root(host_path).map(|root| (String::from("/root/node_modules"), root))
9098 })
9099 .collect();
9100 pairs.extend(extra_roots);
9101
9102 crate::plugins::host_dir::HostDirModuleReader::from_mounts(pairs)
9103}
9104
9105fn host_node_modules_root(path: &Path) -> Option<PathBuf> {
9106 if let Some(root) = path
9107 .ancestors()
9108 .filter(|candidate| {
9109 candidate.file_name().and_then(|name| name.to_str()) == Some("node_modules")
9110 })
9111 .last()
9112 .map(Path::to_path_buf)
9113 {
9114 return Some(root);
9115 }
9116
9117 fs::canonicalize(path)
9118 .ok()?
9119 .ancestors()
9120 .filter(|candidate| {
9121 candidate.file_name().and_then(|name| name.to_str()) == Some("node_modules")
9122 })
9123 .last()
9124 .map(Path::to_path_buf)
9125}
9126
9127#[cfg(test)]
9128mod runtime_guest_path_mapping_tests {
9129 use super::{host_node_modules_root, javascript_sync_rpc_option_bool};
9130 use serde_json::json;
9131 use std::fs;
9132 use std::time::{SystemTime, UNIX_EPOCH};
9133
9134 #[test]
9135 fn host_node_modules_root_prefers_workspace_root_over_pnpm_package_node_modules() {
9136 let unique = SystemTime::now()
9137 .duration_since(UNIX_EPOCH)
9138 .expect("clock should be monotonic")
9139 .as_nanos();
9140 let temp = std::env::temp_dir().join(format!("secure-exec-sidecar-node-modules-{unique}"));
9141 let workspace_node_modules = temp.join("node_modules");
9142 let package_root = workspace_node_modules
9143 .join(".pnpm")
9144 .join("example@1.0.0")
9145 .join("node_modules")
9146 .join("@scope")
9147 .join("pkg");
9148 fs::create_dir_all(&package_root).expect("package root should be created");
9149
9150 let resolved =
9151 host_node_modules_root(&package_root).expect("node_modules root should resolve");
9152
9153 assert_eq!(resolved, workspace_node_modules);
9154
9155 fs::remove_dir_all(&temp).expect("temp tree should be removed");
9156 }
9157
9158 #[test]
9159 fn host_node_modules_root_preserves_symlinked_workspace_node_modules_path() {
9160 let unique = SystemTime::now()
9161 .duration_since(UNIX_EPOCH)
9162 .expect("clock should be monotonic")
9163 .as_nanos();
9164 let temp =
9165 std::env::temp_dir().join(format!("secure-exec-sidecar-node-modules-symlink-{unique}"));
9166 let workspace_node_modules = temp.join("node_modules");
9167 let package_link = workspace_node_modules.join("@scope").join("pkg");
9168 let real_package = temp.join("registry").join("agent").join("pkg");
9169 fs::create_dir_all(package_link.parent().expect("package parent should exist"))
9170 .expect("scoped parent should be created");
9171 fs::create_dir_all(&real_package).expect("real package root should be created");
9172 std::os::unix::fs::symlink(&real_package, &package_link)
9173 .expect("package symlink should be created");
9174
9175 let resolved =
9176 host_node_modules_root(&package_link).expect("node_modules root should resolve");
9177
9178 assert_eq!(resolved, workspace_node_modules);
9179
9180 fs::remove_dir_all(&temp).expect("temp tree should be removed");
9181 }
9182
9183 #[test]
9184 fn javascript_sync_rpc_option_bool_accepts_boolean_recursive_argument() {
9185 assert_eq!(
9186 javascript_sync_rpc_option_bool(&[json!("/workspace"), json!(true)], 1, "recursive"),
9187 Some(true)
9188 );
9189 assert_eq!(
9190 javascript_sync_rpc_option_bool(
9191 &[json!("/workspace"), json!({ "recursive": false })],
9192 1,
9193 "recursive"
9194 ),
9195 Some(false)
9196 );
9197 }
9198}
9199
9200#[cfg(test)]
9201mod kernel_poll_sync_rpc_tests {
9202 use super::{
9203 service_javascript_kernel_poll_sync_rpc, ActiveExecution, ActiveProcess,
9204 JavascriptSyncRpcRequest, KernelPollFdResponse, SidecarKernel, ToolExecution,
9205 EXECUTION_DRIVER_NAME, JAVASCRIPT_COMMAND,
9206 };
9207 use secure_exec_kernel::command_registry::CommandDriver;
9208 use secure_exec_kernel::kernel::{KernelVmConfig, SpawnOptions};
9209 use secure_exec_kernel::mount_table::MountTable;
9210 use secure_exec_kernel::permissions::Permissions;
9211 use secure_exec_kernel::poll::{POLLHUP, POLLIN};
9212 use secure_exec_kernel::vfs::MemoryFileSystem;
9213 use serde_json::{json, Value};
9214 #[test]
9215 fn javascript_kernel_poll_sync_rpc_reports_multiple_kernel_fds() {
9216 let mut config = KernelVmConfig::new("vm-js-kernel-poll");
9217 config.permissions = Permissions::allow_all();
9218 let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config);
9219 kernel
9220 .register_driver(CommandDriver::new(
9221 EXECUTION_DRIVER_NAME,
9222 [JAVASCRIPT_COMMAND],
9223 ))
9224 .expect("register execution driver");
9225
9226 let kernel_handle = kernel
9227 .spawn_process(
9228 JAVASCRIPT_COMMAND,
9229 Vec::new(),
9230 SpawnOptions {
9231 requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)),
9232 ..SpawnOptions::default()
9233 },
9234 )
9235 .expect("spawn javascript kernel process");
9236 let pid = kernel_handle.pid();
9237
9238 let (stdin_read_fd, stdin_write_fd) = kernel
9239 .open_pipe(EXECUTION_DRIVER_NAME, pid)
9240 .expect("open kernel stdin pipe");
9241 kernel
9242 .fd_dup2(EXECUTION_DRIVER_NAME, pid, stdin_read_fd, 0)
9243 .expect("dup stdin pipe onto fd 0");
9244 kernel
9245 .fd_close(EXECUTION_DRIVER_NAME, pid, stdin_read_fd)
9246 .expect("close original stdin read fd");
9247
9248 let process = ActiveProcess::new(
9249 pid,
9250 kernel_handle,
9251 super::GuestRuntimeKind::JavaScript,
9252 ActiveExecution::Tool(ToolExecution::default()),
9253 );
9254
9255 kernel
9256 .fd_write(EXECUTION_DRIVER_NAME, pid, stdin_write_fd, b"poll-ready")
9257 .expect("write kernel stdin payload");
9258 kernel
9259 .fd_close(EXECUTION_DRIVER_NAME, pid, stdin_write_fd)
9260 .expect("close kernel stdin writer");
9261
9262 let response = service_javascript_kernel_poll_sync_rpc(
9263 &mut kernel,
9264 &process,
9265 &JavascriptSyncRpcRequest {
9266 id: 1,
9267 method: String::from("__kernel_poll"),
9268 args: vec![
9269 json!([
9270 { "fd": 0, "events": POLLIN.bits() },
9271 { "fd": 1, "events": POLLIN.bits() }
9272 ]),
9273 json!(250),
9274 ],
9275 },
9276 )
9277 .expect("poll kernel fds");
9278
9279 assert_eq!(response["readyCount"], Value::from(1));
9280 let fds: Vec<KernelPollFdResponse> =
9281 serde_json::from_value(response["fds"].clone()).expect("kernel poll fd response");
9282 assert_eq!(
9283 fds,
9284 vec![
9285 KernelPollFdResponse {
9286 fd: 0,
9287 events: POLLIN.bits(),
9288 revents: (POLLIN | POLLHUP).bits(),
9289 },
9290 KernelPollFdResponse {
9291 fd: 1,
9292 events: POLLIN.bits(),
9293 revents: 0,
9294 },
9295 ]
9296 );
9297
9298 process.kernel_handle.finish(0);
9299 kernel.waitpid(pid).expect("wait javascript kernel process");
9300 }
9301}
9302
9303fn dedupe_strings(values: &[String]) -> Vec<String> {
9304 let mut seen = BTreeSet::new();
9305 let mut deduped = Vec::new();
9306 for value in values {
9307 if seen.insert(value.clone()) {
9308 deduped.push(value.clone());
9309 }
9310 }
9311 deduped
9312}
9313
9314fn dedupe_host_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
9315 let mut seen = BTreeSet::new();
9316 let mut deduped = Vec::new();
9317 for path in paths {
9318 let normalized = normalize_host_path(path);
9319 let key = normalized.to_string_lossy().into_owned();
9320 if seen.insert(key) {
9321 deduped.push(normalized);
9322 }
9323 }
9324 deduped
9325}
9326
9327fn expand_host_access_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
9328 let mut expanded = Vec::new();
9329 let mut seen = BTreeSet::new();
9330
9331 let mut add_path = |candidate: PathBuf| {
9332 let normalized = normalize_host_path(&candidate);
9333 let key = normalized.to_string_lossy().into_owned();
9334 if seen.insert(key) {
9335 expanded.push(normalized);
9336 }
9337 };
9338
9339 for host_path in paths {
9340 add_path(host_path.clone());
9341 if let Ok(realpath) = fs::canonicalize(host_path) {
9342 add_path(realpath);
9343 }
9344
9345 if host_path.file_name().and_then(|name| name.to_str()) != Some("node_modules") {
9346 continue;
9347 }
9348
9349 let mut current = host_path.parent();
9350 while let Some(parent) = current {
9351 let candidate = parent.join("node_modules");
9352 if candidate.exists() {
9353 add_path(candidate.clone());
9354 if let Ok(realpath) = fs::canonicalize(&candidate) {
9355 add_path(realpath);
9356 }
9357 }
9358 current = parent.parent();
9359 }
9360 }
9361
9362 expanded
9363}
9364
9365fn prepare_javascript_shadow(
9366 vm: &mut VmState,
9367 resolved: &ResolvedChildProcessExecution,
9368) -> Result<(), SidecarError> {
9369 let guest_entrypoint = resolved
9370 .env
9371 .get("AGENTOS_GUEST_ENTRYPOINT")
9372 .cloned()
9373 .or_else(|| {
9381 resolve_host_entrypoint_within_vm_host_cwd(vm, &resolved.entrypoint)
9382 .map(|(guest_entrypoint, _)| guest_entrypoint)
9383 })
9384 .or_else(|| {
9385 resolved
9386 .entrypoint
9387 .starts_with('/')
9388 .then(|| normalize_path(&resolved.entrypoint))
9389 });
9390 let Some(guest_entrypoint) = guest_entrypoint else {
9391 return Ok(());
9392 };
9393 if host_mount_path_for_guest_path(vm, &guest_entrypoint).is_some() {
9394 return Ok(());
9395 }
9396 if vm.kernel.lstat(&guest_entrypoint).is_err() {
9397 let host_entrypoint = {
9398 let candidate = Path::new(&resolved.entrypoint);
9399 if candidate.is_absolute() {
9400 candidate.to_path_buf()
9401 } else {
9402 resolved.host_cwd.join(candidate)
9403 }
9404 };
9405 if host_entrypoint.exists() {
9406 materialize_host_path_to_shadow(vm, &guest_entrypoint, &host_entrypoint)?;
9407 return sync_shadow_entrypoint_into_kernel(vm, &guest_entrypoint);
9412 }
9413 }
9414 materialize_guest_path_to_shadow(vm, &guest_entrypoint)
9415}
9416
9417fn sync_shadow_entrypoint_into_kernel(
9422 vm: &mut VmState,
9423 guest_entrypoint: &str,
9424) -> Result<(), SidecarError> {
9425 if vm.kernel.exists(guest_entrypoint).unwrap_or(false) {
9426 return Ok(());
9427 }
9428 let shadow_path = shadow_path_for_guest(vm, guest_entrypoint);
9429 let bytes = match fs::read(&shadow_path) {
9430 Ok(bytes) => bytes,
9431 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
9432 Err(error) => {
9433 return Err(SidecarError::Io(format!(
9434 "failed to read staged shadow entrypoint {}: {error}",
9435 shadow_path.display()
9436 )));
9437 }
9438 };
9439 if let Some(parent) = guest_parent_path(guest_entrypoint) {
9440 if !vm.kernel.exists(&parent).unwrap_or(false) {
9441 vm.kernel.mkdir(&parent, true).map_err(kernel_error)?;
9442 }
9443 }
9444 vm.kernel
9445 .write_file(guest_entrypoint, bytes)
9446 .map_err(kernel_error)?;
9447 Ok(())
9448}
9449
9450fn guest_parent_path(guest_path: &str) -> Option<String> {
9451 let parent = Path::new(guest_path).parent()?;
9452 let parent = parent.to_string_lossy();
9453 if parent.is_empty() || parent == "/" {
9454 None
9455 } else {
9456 Some(parent.into_owned())
9457 }
9458}
9459
9460fn materialize_host_path_to_shadow(
9461 vm: &VmState,
9462 guest_path: &str,
9463 host_path: &Path,
9464) -> Result<(), SidecarError> {
9465 let shadow_path = shadow_path_for_guest(vm, guest_path);
9466 let metadata = fs::symlink_metadata(host_path)
9467 .map_err(|error| SidecarError::Io(format!("failed to stat host entrypoint: {error}")))?;
9468
9469 if metadata.file_type().is_symlink() {
9470 if let Some(parent) = shadow_path.parent() {
9471 fs::create_dir_all(parent).map_err(|error| {
9472 SidecarError::Io(format!("failed to create shadow symlink parent: {error}"))
9473 })?;
9474 }
9475 let _ = fs::remove_file(&shadow_path);
9476 let _ = fs::remove_dir_all(&shadow_path);
9477 let target = fs::read_link(host_path)
9478 .map_err(|error| SidecarError::Io(format!("failed to read host symlink: {error}")))?;
9479 std::os::unix::fs::symlink(&target, &shadow_path)
9480 .map_err(|error| SidecarError::Io(format!("failed to mirror host symlink: {error}")))?;
9481 return Ok(());
9482 }
9483
9484 if metadata.is_dir() {
9485 fs::create_dir_all(&shadow_path).map_err(|error| {
9486 SidecarError::Io(format!("failed to create shadow directory: {error}"))
9487 })?;
9488 fs::set_permissions(
9489 &shadow_path,
9490 fs::Permissions::from_mode(metadata.permissions().mode() & 0o7777),
9491 )
9492 .map_err(|error| {
9493 SidecarError::Io(format!(
9494 "failed to set shadow directory mode on {}: {error}",
9495 shadow_path.display()
9496 ))
9497 })?;
9498 return Ok(());
9499 }
9500
9501 if let Some(parent) = shadow_path.parent() {
9502 fs::create_dir_all(parent).map_err(|error| {
9503 SidecarError::Io(format!("failed to create shadow parent: {error}"))
9504 })?;
9505 }
9506 let bytes = fs::read(host_path)
9507 .map_err(|error| SidecarError::Io(format!("failed to read host entrypoint: {error}")))?;
9508 fs::write(&shadow_path, bytes).map_err(|error| {
9509 SidecarError::Io(format!(
9510 "failed to mirror host file into shadow root: {error}"
9511 ))
9512 })?;
9513 fs::set_permissions(
9514 &shadow_path,
9515 fs::Permissions::from_mode(metadata.permissions().mode() & 0o7777),
9516 )
9517 .map_err(|error| {
9518 SidecarError::Io(format!(
9519 "failed to set shadow file mode on {}: {error}",
9520 shadow_path.display()
9521 ))
9522 })?;
9523 Ok(())
9524}
9525
9526fn materialize_guest_path_to_shadow(
9527 vm: &mut VmState,
9528 guest_path: &str,
9529) -> Result<(), SidecarError> {
9530 let stat = vm.kernel.lstat(guest_path).map_err(kernel_error)?;
9531 let shadow_path = shadow_path_for_guest(vm, guest_path);
9532
9533 if stat.is_symbolic_link {
9534 if let Some(parent) = shadow_path.parent() {
9535 fs::create_dir_all(parent).map_err(|error| {
9536 SidecarError::Io(format!("failed to create shadow symlink parent: {error}"))
9537 })?;
9538 }
9539 let _ = fs::remove_file(&shadow_path);
9540 let _ = fs::remove_dir_all(&shadow_path);
9541 let target = vm.kernel.read_link(guest_path).map_err(kernel_error)?;
9542 std::os::unix::fs::symlink(&target, &shadow_path)
9543 .map_err(|error| SidecarError::Io(format!("failed to mirror symlink: {error}")))?;
9544 return Ok(());
9545 }
9546
9547 if stat.is_directory {
9548 fs::create_dir_all(&shadow_path).map_err(|error| {
9549 SidecarError::Io(format!("failed to create shadow directory: {error}"))
9550 })?;
9551 fs::set_permissions(&shadow_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err(
9552 |error| {
9553 SidecarError::Io(format!(
9554 "failed to set shadow directory mode on {}: {error}",
9555 shadow_path.display()
9556 ))
9557 },
9558 )?;
9559 return Ok(());
9560 }
9561
9562 if let Some(parent) = shadow_path.parent() {
9563 fs::create_dir_all(parent).map_err(|error| {
9564 SidecarError::Io(format!("failed to create shadow parent: {error}"))
9565 })?;
9566 }
9567 let bytes = vm.kernel.read_file(guest_path).map_err(kernel_error)?;
9568 fs::write(&shadow_path, bytes).map_err(|error| {
9569 SidecarError::Io(format!(
9570 "failed to mirror guest file into shadow root: {error}"
9571 ))
9572 })?;
9573 fs::set_permissions(&shadow_path, fs::Permissions::from_mode(stat.mode & 0o7777)).map_err(
9574 |error| {
9575 SidecarError::Io(format!(
9576 "failed to set shadow file mode on {}: {error}",
9577 shadow_path.display()
9578 ))
9579 },
9580 )?;
9581 Ok(())
9582}
9583
9584fn load_javascript_entrypoint_source(
9585 vm: &mut VmState,
9586 host_cwd: &Path,
9587 entrypoint: &str,
9588 env: &BTreeMap<String, String>,
9589) -> Option<String> {
9590 let mut read_guest_file = |path: &str| {
9591 vm.kernel
9592 .read_file(path)
9593 .ok()
9594 .and_then(|bytes| String::from_utf8(bytes).ok())
9595 };
9596
9597 if let Some(source) = env
9598 .get("AGENTOS_GUEST_ENTRYPOINT")
9599 .filter(|path| path.starts_with('/'))
9600 .and_then(|path| read_guest_file(path))
9601 {
9602 return Some(source);
9603 }
9604
9605 if entrypoint.starts_with('/') {
9606 if let Some(source) = read_guest_file(entrypoint) {
9607 return Some(source);
9608 }
9609 }
9610
9611 let host_entrypoint = if Path::new(entrypoint).is_absolute() {
9612 PathBuf::from(entrypoint)
9613 } else {
9614 host_cwd.join(entrypoint)
9615 };
9616 let normalized_entrypoint = normalize_host_path(&host_entrypoint);
9617 let sandbox_root = normalize_host_path(&vm.cwd);
9618 let host_cwd = normalize_host_path(&vm.host_cwd);
9619 if !path_is_within_root(&normalized_entrypoint, &sandbox_root)
9620 && !path_is_within_root(&normalized_entrypoint, &host_cwd)
9621 {
9622 return None;
9623 }
9624
9625 fs::read_to_string(&normalized_entrypoint).ok()
9626}
9627
9628fn emit_dns_resolution_event<B>(
9629 bridge: &SharedBridge<B>,
9630 vm_id: &str,
9631 hostname: &str,
9632 source: KernelDnsResolutionSource,
9633 addresses: &[IpAddr],
9634 dns: &VmDnsConfig,
9635) where
9636 B: NativeSidecarBridge + Send + 'static,
9637 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9638{
9639 let _ = emit_structured_event(
9640 bridge,
9641 vm_id,
9642 "network.dns.resolved",
9643 audit_fields([
9644 ("hostname", hostname.to_owned()),
9645 ("source", source.as_str().to_owned()),
9646 (
9647 "addresses",
9648 addresses
9649 .iter()
9650 .map(ToString::to_string)
9651 .collect::<Vec<_>>()
9652 .join(","),
9653 ),
9654 ("address_count", addresses.len().to_string()),
9655 ("resolver_count", dns.name_servers.len().to_string()),
9656 (
9657 "resolvers",
9658 dns.name_servers
9659 .iter()
9660 .map(ToString::to_string)
9661 .collect::<Vec<_>>()
9662 .join(","),
9663 ),
9664 ]),
9665 );
9666}
9667
9668fn emit_dns_record_resolution_event<B>(
9669 bridge: &SharedBridge<B>,
9670 vm_id: &str,
9671 hostname: &str,
9672 resolution: &DnsRecordResolution,
9673 dns: &VmDnsConfig,
9674) where
9675 B: NativeSidecarBridge + Send + 'static,
9676 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9677{
9678 if let Some(addresses) = dns_resolution_ip_addrs(resolution.records()) {
9679 emit_dns_resolution_event(
9680 bridge,
9681 vm_id,
9682 hostname,
9683 resolution.source(),
9684 &addresses,
9685 dns,
9686 );
9687 return;
9688 }
9689
9690 let _ = emit_structured_event(
9691 bridge,
9692 vm_id,
9693 "network.dns.resolved",
9694 audit_fields([
9695 ("hostname", hostname.to_owned()),
9696 ("source", resolution.source().as_str().to_owned()),
9697 (
9698 "addresses",
9699 resolution
9700 .records()
9701 .iter()
9702 .map(summarize_dns_record)
9703 .collect::<Vec<_>>()
9704 .join(","),
9705 ),
9706 ("address_count", resolution.records().len().to_string()),
9707 ("resolver_count", dns.name_servers.len().to_string()),
9708 (
9709 "resolvers",
9710 dns.name_servers
9711 .iter()
9712 .map(ToString::to_string)
9713 .collect::<Vec<_>>()
9714 .join(","),
9715 ),
9716 ]),
9717 );
9718}
9719
9720fn emit_dns_resolution_failure_event<B>(
9721 bridge: &SharedBridge<B>,
9722 vm_id: &str,
9723 hostname: &str,
9724 dns: &VmDnsConfig,
9725 error: &SidecarError,
9726) where
9727 B: NativeSidecarBridge + Send + 'static,
9728 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
9729{
9730 let _ = emit_structured_event(
9731 bridge,
9732 vm_id,
9733 "network.dns.resolve_failed",
9734 audit_fields([
9735 ("hostname", hostname.to_owned()),
9736 ("reason", error.to_string()),
9737 ("resolver_count", dns.name_servers.len().to_string()),
9738 (
9739 "resolvers",
9740 dns.name_servers
9741 .iter()
9742 .map(ToString::to_string)
9743 .collect::<Vec<_>>()
9744 .join(","),
9745 ),
9746 ]),
9747 );
9748}
9749
9750fn parse_dns_record_type(rrtype: &str) -> Result<RecordType, SidecarError> {
9751 match rrtype {
9752 "A" => Ok(RecordType::A),
9753 "AAAA" => Ok(RecordType::AAAA),
9754 "MX" => Ok(RecordType::MX),
9755 "TXT" => Ok(RecordType::TXT),
9756 "SRV" => Ok(RecordType::SRV),
9757 "CNAME" => Ok(RecordType::CNAME),
9758 "PTR" => Ok(RecordType::PTR),
9759 "NS" => Ok(RecordType::NS),
9760 "SOA" => Ok(RecordType::SOA),
9761 "NAPTR" => Ok(RecordType::NAPTR),
9762 "CAA" => Ok(RecordType::CAA),
9763 "ANY" => Ok(RecordType::ANY),
9764 other => Err(SidecarError::Execution(format!(
9765 "ERR_NOT_IMPLEMENTED: dns rrtype {other} is not supported by the secure-exec dns bridge"
9766 ))),
9767 }
9768}
9769
9770fn dns_resolution_to_node_value(
9771 resolution: &DnsRecordResolution,
9772 requested_type: &str,
9773) -> Result<Value, SidecarError> {
9774 let safe_ips = dns_resolution_safe_ip_set(resolution.records(), resolution.hostname())?;
9775 match requested_type {
9776 "A" | "AAAA" => Ok(Value::Array(
9777 resolution
9778 .records()
9779 .iter()
9780 .filter_map(|record| dns_record_ip_string(record, &safe_ips))
9781 .map(Value::String)
9782 .collect(),
9783 )),
9784 "MX" => Ok(Value::Array(
9785 resolution
9786 .records()
9787 .iter()
9788 .filter_map(|record| match record.data() {
9789 RData::MX(mx) => Some(json!({
9790 "priority": mx.preference,
9791 "exchange": normalize_dns_name_for_node(&mx.exchange),
9792 "type": "MX",
9793 })),
9794 _ => None,
9795 })
9796 .collect(),
9797 )),
9798 "TXT" => Ok(Value::Array(
9799 resolution
9800 .records()
9801 .iter()
9802 .filter_map(|record| match record.data() {
9803 RData::TXT(txt) => Some(Value::Array(
9804 txt.txt_data
9805 .iter()
9806 .map(|entry| Value::String(String::from_utf8_lossy(entry).into_owned()))
9807 .collect(),
9808 )),
9809 _ => None,
9810 })
9811 .collect(),
9812 )),
9813 "SRV" => Ok(Value::Array(
9814 resolution
9815 .records()
9816 .iter()
9817 .filter_map(|record| match record.data() {
9818 RData::SRV(srv) => Some(json!({
9819 "priority": srv.priority,
9820 "weight": srv.weight,
9821 "port": srv.port,
9822 "name": normalize_dns_name_for_node(&srv.target),
9823 "type": "SRV",
9824 })),
9825 _ => None,
9826 })
9827 .collect(),
9828 )),
9829 "CNAME" => Ok(Value::Array(
9830 resolution
9831 .records()
9832 .iter()
9833 .filter_map(|record| match record.data() {
9834 RData::CNAME(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9835 _ => None,
9836 })
9837 .collect(),
9838 )),
9839 "PTR" => Ok(Value::Array(
9840 resolution
9841 .records()
9842 .iter()
9843 .filter_map(|record| match record.data() {
9844 RData::PTR(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9845 _ => None,
9846 })
9847 .collect(),
9848 )),
9849 "NS" => Ok(Value::Array(
9850 resolution
9851 .records()
9852 .iter()
9853 .filter_map(|record| match record.data() {
9854 RData::NS(name) => Some(Value::String(normalize_dns_name_for_node(&name.0))),
9855 _ => None,
9856 })
9857 .collect(),
9858 )),
9859 "SOA" => resolution
9860 .records()
9861 .iter()
9862 .find_map(|record| match record.data() {
9863 RData::SOA(soa) => Some(json!({
9864 "nsname": normalize_dns_name_for_node(&soa.mname),
9865 "hostmaster": normalize_dns_name_for_node(&soa.rname),
9866 "serial": soa.serial,
9867 "refresh": soa.refresh,
9868 "retry": soa.retry,
9869 "expire": soa.expire,
9870 "minttl": soa.minimum,
9871 })),
9872 _ => None,
9873 })
9874 .ok_or_else(|| {
9875 SidecarError::Execution(String::from("failed to resolve DNS SOA record"))
9876 }),
9877 "NAPTR" => Ok(Value::Array(
9878 resolution
9879 .records()
9880 .iter()
9881 .filter_map(|record| match record.data() {
9882 RData::NAPTR(naptr) => Some(json!({
9883 "flags": String::from_utf8_lossy(&naptr.flags).into_owned(),
9884 "service": String::from_utf8_lossy(&naptr.services).into_owned(),
9885 "regexp": String::from_utf8_lossy(&naptr.regexp).into_owned(),
9886 "replacement": normalize_dns_name_for_node(&naptr.replacement),
9887 "order": naptr.order,
9888 "preference": naptr.preference,
9889 })),
9890 _ => None,
9891 })
9892 .collect(),
9893 )),
9894 "CAA" => Ok(Value::Array(
9895 resolution
9896 .records()
9897 .iter()
9898 .filter_map(|record| match record.data() {
9899 RData::CAA(caa) => {
9900 let mut value = serde_json::Map::new();
9901 value.insert(
9902 "critical".to_owned(),
9903 Value::from(u8::from(caa.issuer_critical)),
9904 );
9905 value.insert("type".to_owned(), Value::String(String::from("CAA")));
9906 if caa.tag.eq_ignore_ascii_case("iodef") {
9907 value.insert(
9908 "iodef".to_owned(),
9909 Value::String(
9910 caa.value_as_iodef()
9911 .map(|url| url.to_string())
9912 .unwrap_or_else(|_| {
9913 String::from_utf8_lossy(&caa.value).into_owned()
9914 }),
9915 ),
9916 );
9917 } else if let Ok((issuer, _params)) = caa.value_as_issue() {
9918 let field = if caa.tag.eq_ignore_ascii_case("issuewild") {
9919 "issuewild"
9920 } else {
9921 "issue"
9922 };
9923 value.insert(
9924 field.to_owned(),
9925 Value::String(
9926 issuer.as_ref().map(ToString::to_string).unwrap_or_else(|| {
9927 String::from_utf8_lossy(&caa.value).into_owned()
9928 }),
9929 ),
9930 );
9931 } else {
9932 value.insert(
9933 caa.tag.to_ascii_lowercase(),
9934 Value::String(String::from_utf8_lossy(&caa.value).into_owned()),
9935 );
9936 }
9937 Some(Value::Object(value))
9938 }
9939 _ => None,
9940 })
9941 .collect(),
9942 )),
9943 "ANY" => Ok(Value::Array(
9944 resolution
9945 .records()
9946 .iter()
9947 .filter_map(|record| dns_any_record_to_value(record, &safe_ips))
9948 .collect(),
9949 )),
9950 other => Err(SidecarError::Execution(format!(
9951 "ERR_NOT_IMPLEMENTED: dns rrtype {other} is not supported by the secure-exec dns bridge"
9952 ))),
9953 }
9954}
9955
9956fn dns_resolution_safe_ip_set(
9957 records: &[Record],
9958 hostname: &str,
9959) -> Result<BTreeSet<IpAddr>, SidecarError> {
9960 let ips = records
9961 .iter()
9962 .filter_map(dns_record_ip_addr)
9963 .collect::<Vec<_>>();
9964 if ips.is_empty() {
9965 return Ok(BTreeSet::new());
9966 }
9967 Ok(filter_dns_safe_ip_addrs(ips, hostname)?
9968 .into_iter()
9969 .collect())
9970}
9971
9972fn dns_resolution_ip_addrs(records: &[Record]) -> Option<Vec<IpAddr>> {
9973 let ips = records
9974 .iter()
9975 .filter_map(dns_record_ip_addr)
9976 .collect::<Vec<_>>();
9977 if ips.is_empty() {
9978 return None;
9979 }
9980 Some(ips)
9981}
9982
9983fn dns_record_ip_addr(record: &Record) -> Option<IpAddr> {
9984 match record.data() {
9985 RData::A(address) => Some(IpAddr::V4(**address)),
9986 RData::AAAA(address) => Some(IpAddr::V6(**address)),
9987 _ => None,
9988 }
9989}
9990
9991fn dns_record_ip_string(record: &Record, safe_ips: &BTreeSet<IpAddr>) -> Option<String> {
9992 let ip = dns_record_ip_addr(record)?;
9993 safe_ips.contains(&ip).then(|| ip.to_string())
9994}
9995
9996fn dns_any_record_to_value(record: &Record, safe_ips: &BTreeSet<IpAddr>) -> Option<Value> {
9997 let value = match record.data() {
9998 RData::A(_) | RData::AAAA(_) => json!({
9999 "address": dns_record_ip_string(record, safe_ips)?,
10000 "ttl": record.ttl(),
10001 "type": record.record_type().to_string(),
10002 }),
10003 RData::MX(mx) => json!({
10004 "exchange": normalize_dns_name_for_node(&mx.exchange),
10005 "priority": mx.preference,
10006 "type": "MX",
10007 }),
10008 RData::TXT(txt) => json!({
10009 "entries": txt
10010 .txt_data
10011 .iter()
10012 .map(|entry| String::from_utf8_lossy(entry).into_owned())
10013 .collect::<Vec<_>>(),
10014 "type": "TXT",
10015 }),
10016 RData::SRV(srv) => json!({
10017 "name": normalize_dns_name_for_node(&srv.target),
10018 "port": srv.port,
10019 "priority": srv.priority,
10020 "weight": srv.weight,
10021 "type": "SRV",
10022 }),
10023 RData::CNAME(name) => json!({
10024 "value": normalize_dns_name_for_node(&name.0),
10025 "type": "CNAME",
10026 }),
10027 RData::PTR(name) => json!({
10028 "value": normalize_dns_name_for_node(&name.0),
10029 "type": "PTR",
10030 }),
10031 RData::NS(name) => json!({
10032 "value": normalize_dns_name_for_node(&name.0),
10033 "type": "NS",
10034 }),
10035 RData::SOA(soa) => json!({
10036 "nsname": normalize_dns_name_for_node(&soa.mname),
10037 "hostmaster": normalize_dns_name_for_node(&soa.rname),
10038 "serial": soa.serial,
10039 "refresh": soa.refresh,
10040 "retry": soa.retry,
10041 "expire": soa.expire,
10042 "minttl": soa.minimum,
10043 "type": "SOA",
10044 }),
10045 RData::NAPTR(naptr) => json!({
10046 "flags": String::from_utf8_lossy(&naptr.flags).into_owned(),
10047 "service": String::from_utf8_lossy(&naptr.services).into_owned(),
10048 "regexp": String::from_utf8_lossy(&naptr.regexp).into_owned(),
10049 "replacement": normalize_dns_name_for_node(&naptr.replacement),
10050 "order": naptr.order,
10051 "preference": naptr.preference,
10052 "type": "NAPTR",
10053 }),
10054 RData::CAA(caa) => {
10055 let mut value = serde_json::Map::new();
10056 value.insert(
10057 "critical".to_owned(),
10058 Value::from(u8::from(caa.issuer_critical)),
10059 );
10060 value.insert("type".to_owned(), Value::String(String::from("CAA")));
10061 if caa.tag.eq_ignore_ascii_case("iodef") {
10062 value.insert(
10063 "iodef".to_owned(),
10064 Value::String(
10065 caa.value_as_iodef()
10066 .map(|url| url.to_string())
10067 .unwrap_or_else(|_| String::from_utf8_lossy(&caa.value).into_owned()),
10068 ),
10069 );
10070 } else if let Ok((issuer, _params)) = caa.value_as_issue() {
10071 let field = if caa.tag.eq_ignore_ascii_case("issuewild") {
10072 "issuewild"
10073 } else {
10074 "issue"
10075 };
10076 value.insert(
10077 field.to_owned(),
10078 Value::String(
10079 issuer
10080 .as_ref()
10081 .map(ToString::to_string)
10082 .unwrap_or_else(|| String::from_utf8_lossy(&caa.value).into_owned()),
10083 ),
10084 );
10085 }
10086 Value::Object(value)
10087 }
10088 _ => return None,
10089 };
10090 Some(value)
10091}
10092
10093fn normalize_dns_name_for_node(name: &impl ToString) -> String {
10094 name.to_string().trim_end_matches('.').to_owned()
10095}
10096
10097fn summarize_dns_record(record: &Record) -> String {
10098 match record.data() {
10099 RData::A(_) | RData::AAAA(_) => record.data().to_string(),
10100 _ => format!("{} {}", record.record_type(), record.data()),
10101 }
10102}
10103
10104fn find_socket_state_entry(
10112 vm: Option<&VmState>,
10113 kind: SocketQueryKind,
10114 request: &FindListenerRequest,
10115) -> Result<Option<SocketStateEntry>, SidecarError> {
10116 let vm = vm.ok_or_else(|| SidecarError::InvalidState(String::from("unknown sidecar VM")))?;
10117
10118 for (process_id, process) in &vm.active_processes {
10119 if let Some(path) = request.path.as_deref() {
10120 if matches!(kind, SocketQueryKind::TcpListener) {
10121 for listener in process.unix_listeners.values() {
10122 if listener.path() != path {
10123 continue;
10124 }
10125 return Ok(Some(SocketStateEntry {
10126 process_id: process_id.to_owned(),
10127 host: None,
10128 port: None,
10129 path: Some(path.to_owned()),
10130 }));
10131 }
10132 }
10133 }
10134
10135 if request.path.is_none() {
10136 if let Some(entry) =
10137 find_kernel_socket_state_entry(&vm.kernel, process_id, process, kind, request)?
10138 {
10139 return Ok(Some(entry));
10140 }
10141
10142 match kind {
10143 SocketQueryKind::TcpListener => {
10144 for server in process.http_servers.values() {
10145 let local_addr = server.guest_local_addr;
10146 let local_host = local_addr.ip().to_string();
10147 if !socket_host_matches(request.host.as_deref(), &local_host) {
10148 continue;
10149 }
10150 if let Some(port) = request.port {
10151 if local_addr.port() != port {
10152 continue;
10153 }
10154 }
10155 return Ok(Some(SocketStateEntry {
10156 process_id: process_id.to_owned(),
10157 host: Some(local_host),
10158 port: Some(local_addr.port()),
10159 path: None,
10160 }));
10161 }
10162
10163 for listener in process.tcp_listeners.values() {
10164 if listener.kernel_socket_id.is_some() {
10165 continue;
10166 }
10167 let local_addr = listener.guest_local_addr();
10168 let local_host = local_addr.ip().to_string();
10169 if !socket_host_matches(request.host.as_deref(), &local_host) {
10170 continue;
10171 }
10172 if let Some(port) = request.port {
10173 if local_addr.port() != port {
10174 continue;
10175 }
10176 }
10177 return Ok(Some(SocketStateEntry {
10178 process_id: process_id.to_owned(),
10179 host: Some(local_host),
10180 port: Some(local_addr.port()),
10181 path: None,
10182 }));
10183 }
10184 }
10185 SocketQueryKind::UdpBound => {
10186 for socket in process.udp_sockets.values() {
10187 if socket.kernel_socket_id.is_some() {
10188 continue;
10189 }
10190 let Some(local_addr) = socket.local_addr() else {
10191 continue;
10192 };
10193 let local_host = local_addr.ip().to_string();
10194 if !socket_host_matches(request.host.as_deref(), &local_host) {
10195 continue;
10196 }
10197 if let Some(port) = request.port {
10198 if local_addr.port() != port {
10199 continue;
10200 }
10201 }
10202 return Ok(Some(SocketStateEntry {
10203 process_id: process_id.to_owned(),
10204 host: Some(local_host),
10205 port: Some(local_addr.port()),
10206 path: None,
10207 }));
10208 }
10209 }
10210 }
10211 }
10212
10213 let child_pid = process.execution.child_pid();
10214 let inodes = socket_inodes_for_pid(child_pid)?;
10215 if inodes.is_empty() {
10216 continue;
10217 }
10218
10219 if let Some(path) = request.path.as_deref() {
10220 if let Some(listener) = find_unix_socket_for_pid(child_pid, &inodes, path, process_id)?
10221 {
10222 return Ok(Some(listener));
10223 }
10224 continue;
10225 }
10226
10227 let table_paths = match kind {
10228 SocketQueryKind::TcpListener => [
10229 format!("/proc/{child_pid}/net/tcp"),
10230 format!("/proc/{child_pid}/net/tcp6"),
10231 ],
10232 SocketQueryKind::UdpBound => [
10233 format!("/proc/{child_pid}/net/udp"),
10234 format!("/proc/{child_pid}/net/udp6"),
10235 ],
10236 };
10237 for table_path in table_paths {
10238 if let Some(entry) = find_inet_socket_for_pid(
10239 &table_path,
10240 &inodes,
10241 kind,
10242 request.host.as_deref(),
10243 request.port,
10244 process_id,
10245 )? {
10246 return Ok(Some(entry));
10247 }
10248 }
10249 }
10250
10251 Ok(None)
10252}
10253
10254fn require_vm_inspection_permission<B>(
10255 bridge: &SharedBridge<B>,
10256 vm_id: &str,
10257 capability: &str,
10258 domain: &str,
10259 resource: &str,
10260) -> Result<(), SidecarError>
10261where
10262 B: NativeSidecarBridge + Send + 'static,
10263 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
10264{
10265 let decision = bridge.static_permission_decision(vm_id, capability, domain, Some(resource));
10266 if decision.as_ref().is_some_and(|decision| decision.allow) {
10267 return Ok(());
10268 }
10269
10270 let reason = decision
10271 .and_then(|decision| decision.reason)
10272 .unwrap_or_else(|| format!("{capability} permission required"));
10273 Err(SidecarError::Execution(format!(
10274 "EACCES: permission denied, {resource}: {reason}"
10275 )))
10276}
10277
10278fn socket_query_resource(kind: SocketQueryKind, request: &FindListenerRequest) -> String {
10279 if let Some(path) = request.path.as_deref() {
10280 return format!("unix://{path}");
10281 }
10282
10283 let host = request.host.as_deref().unwrap_or("*");
10284 let port = request
10285 .port
10286 .map_or_else(|| String::from("*"), |port| port.to_string());
10287 match kind {
10288 SocketQueryKind::TcpListener => format!("tcp://{host}:{port}"),
10289 SocketQueryKind::UdpBound => format!("udp://{host}:{port}"),
10290 }
10291}
10292
10293fn snapshot_vm_processes(vm: &VmState) -> Vec<ProcessSnapshotEntry> {
10294 let process_table = vm.kernel.list_processes();
10295 snapshot_vm_processes_inner(vm, &process_table)
10296}
10297
10298fn snapshot_vm_processes_inner(
10299 vm: &VmState,
10300 process_table: &BTreeMap<u32, secure_exec_kernel::process_table::ProcessInfo>,
10301) -> Vec<ProcessSnapshotEntry> {
10302 let mut entries = Vec::new();
10303
10304 for (process_id, process) in &vm.active_processes {
10305 collect_process_snapshot_entries(process_id, process, process_table, &mut entries);
10306 }
10307
10308 for exited in &vm.exited_process_snapshots {
10309 entries.push(exited.process.clone());
10310 }
10311
10312 entries
10313}
10314
10315fn prune_exited_process_snapshots(vm: &mut VmState) {
10316 let cutoff = Instant::now() - EXITED_PROCESS_SNAPSHOT_RETENTION;
10317 while vm
10318 .exited_process_snapshots
10319 .front()
10320 .is_some_and(|snapshot| snapshot.captured_at < cutoff)
10321 {
10322 vm.exited_process_snapshots.pop_front();
10323 }
10324}
10325
10326fn build_process_snapshot_entry(
10327 process_id: &str,
10328 process: &ActiveProcess,
10329 info: &secure_exec_kernel::process_table::ProcessInfo,
10330 exit_code: Option<i32>,
10331) -> ProcessSnapshotEntry {
10332 ProcessSnapshotEntry {
10333 process_id: process_id.to_owned(),
10334 pid: info.pid,
10335 ppid: info.ppid,
10336 pgid: info.pgid,
10337 sid: info.sid,
10338 driver: info.driver.clone(),
10339 command: info.command.clone(),
10340 args: Vec::new(),
10341 cwd: process.guest_cwd.clone(),
10342 status: if exit_code.is_some() {
10343 ProcessSnapshotStatus::Exited
10344 } else {
10345 match info.status {
10346 ProcessStatus::Running => ProcessSnapshotStatus::Running,
10347 ProcessStatus::Stopped => ProcessSnapshotStatus::Stopped,
10348 ProcessStatus::Exited => ProcessSnapshotStatus::Exited,
10349 }
10350 },
10351 exit_code: exit_code.or(info.exit_code),
10352 }
10353}
10354
10355fn collect_process_snapshot_entries(
10356 process_id: &str,
10357 process: &ActiveProcess,
10358 process_table: &BTreeMap<u32, secure_exec_kernel::process_table::ProcessInfo>,
10359 entries: &mut Vec<ProcessSnapshotEntry>,
10360) {
10361 if let Some(info) = process_table.get(&process.kernel_pid) {
10362 entries.push(build_process_snapshot_entry(
10363 process_id, process, info, None,
10364 ));
10365 }
10366
10367 for (child_id, child) in &process.child_processes {
10368 let child_process_id = format!("{process_id}/{child_id}");
10369 collect_process_snapshot_entries(&child_process_id, child, process_table, entries);
10370 }
10371}
10372
10373fn find_kernel_socket_state_entry(
10374 kernel: &SidecarKernel,
10375 process_id: &str,
10376 process: &ActiveProcess,
10377 kind: SocketQueryKind,
10378 request: &FindListenerRequest,
10379) -> Result<Option<SocketStateEntry>, SidecarError> {
10380 let entry = match kind {
10381 SocketQueryKind::TcpListener => process
10382 .tcp_listeners
10383 .values()
10384 .filter_map(|listener| listener.kernel_socket_id)
10385 .find_map(|socket_id| {
10386 kernel_socket_state_entry(kernel, process_id, socket_id, kind, request)
10387 }),
10388 SocketQueryKind::UdpBound => process
10389 .udp_sockets
10390 .values()
10391 .filter_map(|socket| socket.kernel_socket_id)
10392 .find_map(|socket_id| {
10393 kernel_socket_state_entry(kernel, process_id, socket_id, kind, request)
10394 }),
10395 };
10396
10397 if entry.is_some() {
10398 return Ok(entry);
10399 }
10400
10401 for child in process.child_processes.values() {
10402 if let Some(entry) =
10403 find_kernel_socket_state_entry(kernel, process_id, child, kind, request)?
10404 {
10405 return Ok(Some(entry));
10406 }
10407 }
10408
10409 Ok(None)
10410}
10411
10412fn kernel_socket_state_entry(
10413 kernel: &SidecarKernel,
10414 process_id: &str,
10415 socket_id: SocketId,
10416 kind: SocketQueryKind,
10417 request: &FindListenerRequest,
10418) -> Option<SocketStateEntry> {
10419 let record = kernel.socket_get(socket_id)?;
10420 let local_address = record.local_address()?;
10421 match kind {
10422 SocketQueryKind::TcpListener if record.state() == SocketState::Listening => {}
10423 SocketQueryKind::TcpListener => return None,
10424 SocketQueryKind::UdpBound => {}
10425 }
10426
10427 if !socket_host_matches(request.host.as_deref(), local_address.host()) {
10428 return None;
10429 }
10430 if request
10431 .port
10432 .is_some_and(|port| local_address.port() != port)
10433 {
10434 return None;
10435 }
10436
10437 Some(SocketStateEntry {
10438 process_id: process_id.to_owned(),
10439 host: Some(local_address.host().to_owned()),
10440 port: Some(local_address.port()),
10441 path: None,
10442 })
10443}
10444
10445fn socket_inodes_for_pid(pid: u32) -> Result<BTreeSet<u64>, SidecarError> {
10446 let fd_dir = PathBuf::from(format!("/proc/{pid}/fd"));
10447 let entries = match fs::read_dir(&fd_dir) {
10448 Ok(entries) => entries,
10449 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(BTreeSet::new()),
10450 Err(error) => {
10451 return Err(SidecarError::Io(format!(
10452 "failed to read socket descriptors for process {pid}: {error}"
10453 )));
10454 }
10455 };
10456
10457 let mut inodes = BTreeSet::new();
10458 for entry in entries {
10459 let entry = entry.map_err(|error| {
10460 SidecarError::Io(format!(
10461 "failed to inspect fd entry for process {pid}: {error}"
10462 ))
10463 })?;
10464 let target = match fs::read_link(entry.path()) {
10465 Ok(target) => target,
10466 Err(_) => continue,
10467 };
10468 if let Some(inode) = parse_socket_inode(&target) {
10469 inodes.insert(inode);
10470 }
10471 }
10472
10473 Ok(inodes)
10474}
10475
10476fn parse_socket_inode(target: &Path) -> Option<u64> {
10477 let value = target.to_string_lossy();
10478 let trimmed = value.strip_prefix("socket:[")?.strip_suffix(']')?;
10479 trimmed.parse().ok()
10480}
10481
10482fn unix_socket_path(addr: &UnixSocketAddr) -> Option<String> {
10483 addr.as_pathname()
10484 .map(|path| path.to_string_lossy().into_owned())
10485}
10486
10487fn find_unix_socket_for_pid(
10488 pid: u32,
10489 inodes: &BTreeSet<u64>,
10490 path: &str,
10491 process_id: &str,
10492) -> Result<Option<SocketStateEntry>, SidecarError> {
10493 let table_path = format!("/proc/{pid}/net/unix");
10494 let contents = match fs::read_to_string(&table_path) {
10495 Ok(contents) => contents,
10496 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
10497 Err(error) => {
10498 return Err(SidecarError::Io(format!(
10499 "failed to inspect unix sockets for process {pid}: {error}"
10500 )));
10501 }
10502 };
10503
10504 for line in contents.lines().skip(1) {
10505 let columns = line.split_whitespace().collect::<Vec<_>>();
10506 if columns.len() < 8 {
10507 continue;
10508 }
10509 let Ok(inode) = columns[6].parse::<u64>() else {
10510 continue;
10511 };
10512 if !inodes.contains(&inode) || columns[7] != path {
10513 continue;
10514 }
10515 return Ok(Some(SocketStateEntry {
10516 process_id: process_id.to_owned(),
10517 host: None,
10518 port: None,
10519 path: Some(path.to_owned()),
10520 }));
10521 }
10522
10523 Ok(None)
10524}
10525
10526fn find_inet_socket_for_pid(
10527 table_path: &str,
10528 inodes: &BTreeSet<u64>,
10529 kind: SocketQueryKind,
10530 requested_host: Option<&str>,
10531 requested_port: Option<u16>,
10532 process_id: &str,
10533) -> Result<Option<SocketStateEntry>, SidecarError> {
10534 for entry in parse_proc_net_entries(table_path)? {
10535 if !inodes.contains(&entry.inode) {
10536 continue;
10537 }
10538 if matches!(kind, SocketQueryKind::TcpListener) && entry.state != "0A" {
10539 continue;
10540 }
10541 if !socket_host_matches(requested_host, &entry.local_host) {
10542 continue;
10543 }
10544 if let Some(port) = requested_port {
10545 if entry.local_port != port {
10546 continue;
10547 }
10548 }
10549 return Ok(Some(SocketStateEntry {
10550 process_id: process_id.to_owned(),
10551 host: Some(entry.local_host),
10552 port: Some(entry.local_port),
10553 path: None,
10554 }));
10555 }
10556
10557 Ok(None)
10558}
10559
10560fn is_unspecified_socket_host(host: &str) -> bool {
10561 host == "0.0.0.0" || host == "::"
10562}
10563
10564fn is_loopback_socket_host(host: &str) -> bool {
10565 host == "127.0.0.1" || host == "::1" || host.eq_ignore_ascii_case("localhost")
10566}
10567
10568pub(crate) fn vm_network_resource_counts(vm: &VmState) -> NetworkResourceCounts {
10569 let snapshot = vm.kernel.resource_snapshot();
10570 let mut counts = NetworkResourceCounts {
10571 sockets: snapshot.sockets,
10572 connections: snapshot.socket_connections,
10573 };
10574 for process in vm.active_processes.values() {
10575 let process_counts = process.sidecar_only_network_resource_counts();
10576 counts.sockets += process_counts.sockets;
10577 counts.connections += process_counts.connections;
10578 }
10579 counts
10580}
10581
10582#[allow(clippy::too_many_arguments)]
10583fn collect_javascript_socket_port_state(
10584 kernel: &SidecarKernel,
10585 process_id: &str,
10586 process: &ActiveProcess,
10587 tcp_guest_to_host: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10588 http_loopback_targets: &mut BTreeMap<
10589 (JavascriptSocketFamily, u16),
10590 JavascriptHttpLoopbackTarget,
10591 >,
10592 udp_guest_to_host: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10593 udp_host_to_guest: &mut BTreeMap<(JavascriptSocketFamily, u16), u16>,
10594 used_tcp_ports: &mut BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10595 used_udp_ports: &mut BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10596) {
10597 for (family, port) in process.tcp_port_reservations.values() {
10598 used_tcp_ports.entry(*family).or_default().insert(*port);
10599 }
10600
10601 let mut record_tcp_listener = |guest_addr: SocketAddr, host_port: u16| {
10602 let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10603 used_tcp_ports
10604 .entry(family)
10605 .or_default()
10606 .insert(guest_addr.port());
10607 tcp_guest_to_host.insert((family, guest_addr.port()), host_port);
10610 };
10611
10612 for listener in process.tcp_listeners.values() {
10613 let local_addr = listener
10614 .kernel_socket_id
10615 .and_then(|socket_id| kernel.socket_get(socket_id))
10616 .and_then(|record| record.local_address().cloned())
10617 .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
10618 .unwrap_or_else(|| listener.guest_local_addr());
10619 record_tcp_listener(local_addr, local_addr.port());
10620 }
10621
10622 for (server_id, server) in &process.http_servers {
10623 let host_port = match server.listener.local_addr() {
10624 Ok(addr) => addr.port(),
10625 Err(_) => continue,
10626 };
10627 record_tcp_listener(server.guest_local_addr, host_port);
10628 let family = JavascriptSocketFamily::from_ip(server.guest_local_addr.ip());
10629 http_loopback_targets.insert(
10630 (family, server.guest_local_addr.port()),
10631 JavascriptHttpLoopbackTarget {
10632 process_id: process_id.to_owned(),
10633 server_id: *server_id,
10634 },
10635 );
10636 }
10637
10638 if let Ok(http2) = process.http2.shared.lock() {
10639 for server in http2.servers.values() {
10640 record_tcp_listener(server.guest_local_addr, server.actual_local_addr.port());
10641 }
10642 }
10643
10644 for socket in process.tcp_sockets.values() {
10645 let guest_addr = socket
10646 .kernel_socket_id
10647 .and_then(|socket_id| kernel.socket_get(socket_id))
10648 .and_then(|record| record.local_address().cloned())
10649 .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
10650 .unwrap_or(socket.guest_local_addr);
10651 let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10652 used_tcp_ports
10653 .entry(family)
10654 .or_default()
10655 .insert(guest_addr.port());
10656 }
10657
10658 for socket in process.udp_sockets.values() {
10659 let guest_addr = socket
10660 .kernel_socket_id
10661 .and_then(|socket_id| kernel.socket_get(socket_id))
10662 .and_then(|record| record.local_address().cloned())
10663 .and_then(|address| {
10664 resolve_udp_bind_addr(address.host(), address.port(), socket.family).ok()
10665 })
10666 .or_else(|| socket.local_addr());
10667 let Some(guest_addr) = guest_addr else {
10668 continue;
10669 };
10670 let family = JavascriptSocketFamily::from_ip(guest_addr.ip());
10671 used_udp_ports
10672 .entry(family)
10673 .or_default()
10674 .insert(guest_addr.port());
10675 if let Some(host_addr) = socket
10676 .socket
10677 .as_ref()
10678 .and_then(|socket| socket.local_addr().ok())
10679 {
10680 if is_loopback_ip(guest_addr.ip()) {
10681 udp_guest_to_host.insert((family, guest_addr.port()), host_addr.port());
10682 udp_host_to_guest.insert((family, host_addr.port()), guest_addr.port());
10683 }
10684 } else if socket.kernel_socket_id.is_some() && is_loopback_ip(guest_addr.ip()) {
10685 udp_guest_to_host.insert((family, guest_addr.port()), guest_addr.port());
10686 udp_host_to_guest.insert((family, guest_addr.port()), guest_addr.port());
10687 }
10688 }
10689
10690 for (child_process_id, child) in &process.child_processes {
10691 let child_id = format!("{process_id}/{child_process_id}");
10692 collect_javascript_socket_port_state(
10693 kernel,
10694 &child_id,
10695 child,
10696 tcp_guest_to_host,
10697 http_loopback_targets,
10698 udp_guest_to_host,
10699 udp_host_to_guest,
10700 used_tcp_ports,
10701 used_udp_ports,
10702 );
10703 }
10704}
10705
10706pub(crate) fn build_javascript_socket_path_context(
10707 vm: &VmState,
10708) -> Result<JavascriptSocketPathContext, SidecarError> {
10709 let mut loopback_exempt_ports = vm.create_loopback_exempt_ports.clone();
10710 loopback_exempt_ports.extend(vm.configuration.loopback_exempt_ports.iter().copied());
10711 let mut tcp_loopback_guest_to_host_ports = BTreeMap::new();
10712 let mut http_loopback_targets = BTreeMap::new();
10713 let mut udp_loopback_guest_to_host_ports = BTreeMap::new();
10714 let mut udp_loopback_host_to_guest_ports = BTreeMap::new();
10715 let mut used_tcp_guest_ports = BTreeMap::new();
10716 let mut used_udp_guest_ports = BTreeMap::new();
10717 for (process_id, process) in &vm.active_processes {
10718 collect_javascript_socket_port_state(
10719 &vm.kernel,
10720 process_id,
10721 process,
10722 &mut tcp_loopback_guest_to_host_ports,
10723 &mut http_loopback_targets,
10724 &mut udp_loopback_guest_to_host_ports,
10725 &mut udp_loopback_host_to_guest_ports,
10726 &mut used_tcp_guest_ports,
10727 &mut used_udp_guest_ports,
10728 );
10729 }
10730 Ok(JavascriptSocketPathContext {
10731 sandbox_root: vm.cwd.clone(),
10732 mounts: vm.configuration.mounts.clone(),
10733 listen_policy: vm.listen_policy,
10734 loopback_exempt_ports,
10735 tcp_loopback_guest_to_host_ports,
10736 http_loopback_targets,
10737 udp_loopback_guest_to_host_ports,
10738 udp_loopback_host_to_guest_ports,
10739 used_tcp_guest_ports,
10740 used_udp_guest_ports,
10741 })
10742}
10743
10744fn check_network_resource_limit(
10745 limit: Option<usize>,
10746 current: usize,
10747 additional: usize,
10748 label: &str,
10749) -> Result<(), SidecarError> {
10750 if let Some(limit) = limit {
10751 if current.saturating_add(additional) > limit {
10752 return Err(SidecarError::Execution(format!(
10753 "EAGAIN: maximum {label} count reached"
10754 )));
10755 }
10756 }
10757 Ok(())
10758}
10759
10760fn normalize_tcp_listen_host(
10761 host: Option<&str>,
10762) -> Result<(JavascriptSocketFamily, &'static str, &'static str), SidecarError> {
10763 match host.unwrap_or("127.0.0.1") {
10764 "127.0.0.1" | "localhost" => Ok((JavascriptSocketFamily::Ipv4, "127.0.0.1", "127.0.0.1")),
10765 "::1" => Ok((JavascriptSocketFamily::Ipv6, "::1", "::1")),
10766 "0.0.0.0" => Ok((JavascriptSocketFamily::Ipv4, "127.0.0.1", "0.0.0.0")),
10767 "::" => Ok((JavascriptSocketFamily::Ipv6, "::1", "::")),
10768 other => Err(SidecarError::Execution(format!(
10769 "EACCES: TCP listeners must bind to loopback or unspecified addresses, got {other}"
10770 ))),
10771 }
10772}
10773
10774fn normalize_udp_bind_host(
10775 host: Option<&str>,
10776 family: JavascriptUdpFamily,
10777) -> Result<(&'static str, &'static str, JavascriptSocketFamily), SidecarError> {
10778 match (family, host) {
10779 (JavascriptUdpFamily::Ipv4, None) | (JavascriptUdpFamily::Ipv4, Some("0.0.0.0")) => {
10780 Ok(("127.0.0.1", "0.0.0.0", JavascriptSocketFamily::Ipv4))
10781 }
10782 (JavascriptUdpFamily::Ipv4, Some("127.0.0.1"))
10783 | (JavascriptUdpFamily::Ipv4, Some("localhost")) => {
10784 Ok(("127.0.0.1", "127.0.0.1", JavascriptSocketFamily::Ipv4))
10785 }
10786 (JavascriptUdpFamily::Ipv6, None) | (JavascriptUdpFamily::Ipv6, Some("::")) => {
10787 Ok(("::1", "::", JavascriptSocketFamily::Ipv6))
10788 }
10789 (JavascriptUdpFamily::Ipv6, Some("::1"))
10790 | (JavascriptUdpFamily::Ipv6, Some("localhost")) => {
10791 Ok(("::1", "::1", JavascriptSocketFamily::Ipv6))
10792 }
10793 (JavascriptUdpFamily::Ipv4, Some(other)) => Err(SidecarError::Execution(format!(
10794 "EACCES: udp4 sockets must bind to 127.0.0.1 or 0.0.0.0, got {other}"
10795 ))),
10796 (JavascriptUdpFamily::Ipv6, Some(other)) => Err(SidecarError::Execution(format!(
10797 "EACCES: udp6 sockets must bind to ::1 or ::, got {other}"
10798 ))),
10799 }
10800}
10801
10802fn allocate_guest_listen_port(
10803 requested_port: u16,
10804 family: JavascriptSocketFamily,
10805 used_ports: &BTreeMap<JavascriptSocketFamily, BTreeSet<u16>>,
10806 policy: VmListenPolicy,
10807) -> Result<u16, SidecarError> {
10808 let is_allowed = |port: u16| {
10809 port >= policy.port_min
10810 && port <= policy.port_max
10811 && (policy.allow_privileged || port >= 1024)
10812 };
10813 let used = used_ports.get(&family);
10814
10815 if requested_port != 0 {
10816 if !is_allowed(requested_port) {
10817 let reason = if requested_port < 1024 && !policy.allow_privileged {
10818 format!(
10819 "EACCES: privileged listen port {requested_port} requires {}=true",
10820 VM_LISTEN_ALLOW_PRIVILEGED_METADATA_KEY
10821 )
10822 } else {
10823 format!(
10824 "EACCES: listen port {requested_port} is outside the allowed range {}-{}",
10825 policy.port_min, policy.port_max
10826 )
10827 };
10828 return Err(SidecarError::Execution(reason));
10829 }
10830 if used.is_some_and(|ports| ports.contains(&requested_port)) {
10831 return Err(sidecar_net_error(std::io::Error::from_raw_os_error(
10832 libc::EADDRINUSE,
10833 )));
10834 }
10835 return Ok(requested_port);
10836 }
10837
10838 let allocation_start = policy
10839 .port_min
10840 .max(if policy.allow_privileged { 1 } else { 1024 });
10841 for candidate in allocation_start..=policy.port_max {
10842 if used.is_some_and(|ports| ports.contains(&candidate)) {
10843 continue;
10844 }
10845 return Ok(candidate);
10846 }
10847
10848 Err(sidecar_net_error(std::io::Error::from_raw_os_error(
10849 libc::EADDRINUSE,
10850 )))
10851}
10852
10853fn socket_host_matches(requested: Option<&str>, actual: &str) -> bool {
10854 match requested {
10855 None => true,
10856 Some(requested) if requested == actual => true,
10857 Some(requested)
10858 if is_unspecified_socket_host(requested) && is_unspecified_socket_host(actual) =>
10859 {
10860 true
10861 }
10862 Some(requested) if is_unspecified_socket_host(requested) => is_loopback_socket_host(actual),
10863 Some(requested) if requested.eq_ignore_ascii_case("localhost") => {
10864 is_loopback_socket_host(actual)
10865 }
10866 _ => false,
10867 }
10868}
10869
10870fn parse_proc_net_entries(table_path: &str) -> Result<Vec<ProcNetEntry>, SidecarError> {
10871 let contents = match fs::read_to_string(table_path) {
10872 Ok(contents) => contents,
10873 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
10874 Err(error) => {
10875 return Err(SidecarError::Io(format!(
10876 "failed to inspect socket table {table_path}: {error}"
10877 )));
10878 }
10879 };
10880
10881 let mut entries = Vec::new();
10882 for line in contents.lines().skip(1) {
10883 let columns = line.split_whitespace().collect::<Vec<_>>();
10884 if columns.len() < 10 {
10885 continue;
10886 }
10887 let Some((host, port)) = parse_proc_ip_port(columns[1]) else {
10888 continue;
10889 };
10890 let Ok(inode) = columns[9].parse::<u64>() else {
10891 continue;
10892 };
10893 entries.push(ProcNetEntry {
10894 local_host: host,
10895 local_port: port,
10896 state: columns[3].to_owned(),
10897 inode,
10898 });
10899 }
10900
10901 Ok(entries)
10902}
10903
10904fn parse_proc_ip_port(value: &str) -> Option<(String, u16)> {
10905 let (raw_ip, raw_port) = value.split_once(':')?;
10906 let port = u16::from_str_radix(raw_port, 16).ok()?;
10907 let host = match raw_ip.len() {
10908 8 => {
10909 let raw = u32::from_str_radix(raw_ip, 16).ok()?;
10910 Ipv4Addr::from(raw.to_le_bytes()).to_string()
10911 }
10912 32 => {
10913 let mut bytes = [0_u8; 16];
10914 for (index, chunk) in raw_ip.as_bytes().chunks(8).enumerate() {
10915 let word = u32::from_str_radix(std::str::from_utf8(chunk).ok()?, 16).ok()?;
10916 bytes[index * 4..(index + 1) * 4].copy_from_slice(&word.to_le_bytes());
10917 }
10918 Ipv6Addr::from(bytes).to_string()
10919 }
10920 _ => return None,
10921 };
10922 Some((host, port))
10923}
10924
10925fn python_file_entrypoint(entrypoint: &str) -> Option<PathBuf> {
10926 let path = Path::new(entrypoint);
10927 (path.extension().and_then(|extension| extension.to_str()) == Some("py"))
10928 .then(|| path.to_path_buf())
10929}
10930
10931fn add_runtime_guest_path_mapping(
10932 env: &mut BTreeMap<String, String>,
10933 guest_path: &str,
10934 host_path: &Path,
10935) {
10936 let mut mappings = env
10937 .get("AGENTOS_GUEST_PATH_MAPPINGS")
10938 .and_then(|value| serde_json::from_str::<Vec<Value>>(value).ok())
10939 .unwrap_or_default();
10940 mappings.retain(|mapping| {
10941 mapping
10942 .get("guestPath")
10943 .and_then(Value::as_str)
10944 .map(|existing| normalize_path(existing) != normalize_path(guest_path))
10945 .unwrap_or(true)
10946 });
10947 mappings.push(json!({
10948 "guestPath": normalize_path(guest_path),
10949 "hostPath": host_path.display().to_string(),
10950 }));
10951 if let Ok(serialized) = serde_json::to_string(&mappings) {
10952 env.insert(String::from("AGENTOS_GUEST_PATH_MAPPINGS"), serialized);
10953 }
10954}
10955
10956fn add_runtime_host_access_path(
10957 env: &mut BTreeMap<String, String>,
10958 key: &str,
10959 host_path: &Path,
10960 expand: bool,
10961) {
10962 let existing = env
10963 .get(key)
10964 .and_then(|value| serde_json::from_str::<Vec<String>>(value).ok())
10965 .unwrap_or_default()
10966 .into_iter()
10967 .map(PathBuf::from)
10968 .collect::<Vec<_>>();
10969 let mut paths = existing;
10970 paths.push(host_path.to_path_buf());
10971 let normalized = if expand {
10972 expand_host_access_paths(&paths)
10973 } else {
10974 dedupe_host_paths(&paths)
10975 };
10976 let serialized = normalized
10977 .iter()
10978 .map(|path| path.to_string_lossy().into_owned())
10979 .collect::<Vec<_>>();
10980 if let Ok(serialized) = serde_json::to_string(&serialized) {
10981 env.insert(key.to_owned(), serialized);
10982 }
10983}
10984
10985fn is_path_like_specifier(specifier: &str) -> bool {
10988 specifier.starts_with('/')
10989 || specifier.starts_with("./")
10990 || specifier.starts_with("../")
10991 || specifier.starts_with("file:")
10992}
10993
10994fn execution_wasm_permission_tier(tier: WasmPermissionTier) -> ExecutionWasmPermissionTier {
10995 match tier {
10996 WasmPermissionTier::Full => ExecutionWasmPermissionTier::Full,
10997 WasmPermissionTier::ReadWrite => ExecutionWasmPermissionTier::ReadWrite,
10998 WasmPermissionTier::ReadOnly => ExecutionWasmPermissionTier::ReadOnly,
10999 WasmPermissionTier::Isolated => ExecutionWasmPermissionTier::Isolated,
11000 }
11001}
11002
11003fn resolve_wasm_permission_tier(
11004 vm: &VmState,
11005 command_name: Option<&str>,
11006 explicit_tier: Option<WasmPermissionTier>,
11007 entrypoint: &str,
11008) -> WasmPermissionTier {
11009 explicit_tier
11010 .or_else(|| command_name.and_then(|command| vm.command_permissions.get(command).copied()))
11011 .or_else(|| {
11012 Path::new(entrypoint)
11013 .file_name()
11014 .and_then(|name| name.to_str())
11015 .and_then(|command| vm.command_permissions.get(command).copied())
11016 })
11017 .unwrap_or(WasmPermissionTier::Full)
11018}
11019
11020fn tokenize_shell_free_command(command: &str) -> Vec<String> {
11021 command
11022 .split_whitespace()
11023 .filter(|segment| !segment.is_empty())
11024 .map(str::to_owned)
11025 .collect()
11026}
11027
11028fn is_posix_shell_builtin(command: &str) -> bool {
11029 matches!(
11030 command,
11031 "." | ":"
11032 | "break"
11033 | "cd"
11034 | "continue"
11035 | "eval"
11036 | "exec"
11037 | "exit"
11038 | "export"
11039 | "readonly"
11040 | "return"
11041 | "set"
11042 | "shift"
11043 | "times"
11044 | "trap"
11045 | "umask"
11046 | "unset"
11047 )
11048}
11049
11050fn shell_first_token_requires_shell(token: &str) -> bool {
11056 token.contains('=') || is_shell_reserved_word(token)
11057}
11058
11059fn is_shell_reserved_word(token: &str) -> bool {
11060 matches!(
11061 token,
11062 "if" | "then"
11063 | "elif"
11064 | "else"
11065 | "fi"
11066 | "for"
11067 | "in"
11068 | "do"
11069 | "done"
11070 | "while"
11071 | "until"
11072 | "case"
11073 | "esac"
11074 | "{"
11075 | "}"
11076 | "!"
11077 )
11078}
11079
11080fn command_requires_shell(command: &str) -> bool {
11081 command.chars().any(|ch| {
11082 matches!(
11083 ch,
11084 '|' | '&'
11085 | ';'
11086 | '<'
11087 | '>'
11088 | '('
11089 | ')'
11090 | '$'
11091 | '`'
11092 | '*'
11093 | '?'
11094 | '['
11095 | ']'
11096 | '{'
11097 | '}'
11098 | '~'
11099 | '\''
11100 | '"'
11101 | '\\'
11102 | '\n'
11103 )
11104 })
11105}
11106
11107fn host_mount_path_for_guest_path(vm: &VmState, guest_path: &str) -> Option<PathBuf> {
11108 let normalized = normalize_path(guest_path);
11109
11110 let mut mounts = vm
11111 .configuration
11112 .mounts
11113 .iter()
11114 .filter_map(|mount| {
11115 ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
11116 .then(|| {
11117 mount_config_host_path(&mount.plugin.config)
11118 .map(|host_path| (mount.guest_path.as_str(), host_path))
11119 })
11120 .flatten()
11121 })
11122 .collect::<Vec<_>>();
11123 mounts.sort_by_key(|mount| std::cmp::Reverse(mount.0.len()));
11124
11125 for (guest_root, host_root) in mounts {
11126 if normalized != guest_root && !normalized.starts_with(&format!("{guest_root}/")) {
11127 continue;
11128 }
11129
11130 let suffix = normalized
11131 .strip_prefix(guest_root)
11132 .unwrap_or_default()
11133 .trim_start_matches('/');
11134 let mut path = PathBuf::from(host_root);
11135 if !suffix.is_empty() {
11136 path.push(suffix);
11137 }
11138 return Some(path);
11139 }
11140
11141 None
11142}
11143
11144fn host_runtime_path_for_guest_path_with_env(
11145 vm: &VmState,
11146 runtime_env: &BTreeMap<String, String>,
11147 guest_path: &str,
11148 default_host_cwd: &Path,
11149) -> Option<PathBuf> {
11150 if let Some(path) = host_mount_path_for_guest_path(vm, guest_path) {
11151 return Some(path);
11152 }
11153 if let Some(path) = host_path_from_runtime_guest_mappings(runtime_env, guest_path) {
11154 return Some(path);
11155 }
11156
11157 let normalized = normalize_path(guest_path);
11158 let virtual_home = guest_virtual_home(vm);
11159
11160 if normalized == virtual_home || normalized.starts_with(&format!("{virtual_home}/")) {
11161 let suffix = normalized
11162 .strip_prefix(&virtual_home)
11163 .unwrap_or_default()
11164 .trim_start_matches('/');
11165 let mut host_path = default_host_cwd.to_path_buf();
11166 if !suffix.is_empty() {
11167 host_path.push(suffix);
11168 }
11169 return Some(host_path);
11170 }
11171
11172 None
11173}
11174
11175#[derive(Deserialize, Serialize)]
11176struct RuntimeGuestPathMapping {
11177 #[serde(rename = "guestPath")]
11178 guest_path: String,
11179 #[serde(rename = "hostPath")]
11180 host_path: String,
11181 #[serde(rename = "readOnly", default)]
11182 read_only: bool,
11183}
11184
11185pub(crate) fn host_path_from_runtime_guest_mappings(
11186 runtime_env: &BTreeMap<String, String>,
11187 guest_path: &str,
11188) -> Option<PathBuf> {
11189 let mappings = runtime_env
11190 .get("AGENTOS_GUEST_PATH_MAPPINGS")
11191 .and_then(|value| serde_json::from_str::<Vec<RuntimeGuestPathMapping>>(value).ok())?;
11192 let normalized = normalize_path(guest_path);
11193
11194 let mut sorted_mappings = mappings
11195 .into_iter()
11196 .filter_map(|mapping| {
11197 (!mapping.guest_path.is_empty() && !mapping.host_path.is_empty()).then_some((
11198 normalize_path(&mapping.guest_path),
11199 PathBuf::from(mapping.host_path),
11200 ))
11201 })
11202 .collect::<Vec<_>>();
11203 sorted_mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.0.len()));
11204
11205 for (guest_root, mut host_root) in sorted_mappings {
11206 if guest_root != "/"
11207 && normalized != guest_root
11208 && !normalized.starts_with(&format!("{guest_root}/"))
11209 {
11210 continue;
11211 }
11212 if guest_root == "/" && !normalized.starts_with('/') {
11213 continue;
11214 }
11215
11216 if host_root.is_relative() {
11217 host_root = std::env::current_dir().ok()?.join(host_root);
11218 }
11219
11220 let suffix = if guest_root == "/" {
11221 normalized.trim_start_matches('/')
11222 } else {
11223 normalized
11224 .strip_prefix(&guest_root)
11225 .unwrap_or_default()
11226 .trim_start_matches('/')
11227 };
11228 if !suffix.is_empty() {
11229 host_root.push(suffix);
11230 }
11231 return Some(host_root);
11232 }
11233
11234 None
11235}
11236
11237fn guest_runtime_path_for_host_path(
11238 runtime_env: &BTreeMap<String, String>,
11239 virtual_home: &str,
11240 cwd: &Path,
11241 host_path: &str,
11242) -> Option<String> {
11243 let resolved = if host_path.starts_with("file://") {
11244 PathBuf::from(host_path.trim_start_matches("file://"))
11245 } else if host_path.starts_with("file:") {
11246 PathBuf::from(host_path.trim_start_matches("file:"))
11247 } else {
11248 let candidate = PathBuf::from(host_path);
11249 if candidate.is_absolute() {
11250 candidate
11251 } else if host_path.starts_with("./") || host_path.starts_with("../") {
11252 cwd.join(candidate)
11253 } else {
11254 return None;
11255 }
11256 };
11257 let normalized = normalize_host_path(&resolved);
11258
11259 if let Some(path) = guest_path_from_runtime_host_mappings(runtime_env, &normalized) {
11260 return Some(path);
11261 }
11262
11263 let normalized_cwd = normalize_host_path(cwd);
11264 if !path_is_within_root(&normalized, &normalized_cwd) {
11265 return None;
11266 }
11267
11268 let virtual_home = if virtual_home.starts_with('/') {
11269 virtual_home.to_string()
11270 } else {
11271 String::from("/root")
11272 };
11273 let suffix = normalized
11274 .strip_prefix(&normalized_cwd)
11275 .ok()?
11276 .to_string_lossy()
11277 .replace('\\', "/")
11278 .trim_start_matches('/')
11279 .to_owned();
11280
11281 Some(if suffix.is_empty() {
11282 virtual_home
11283 } else {
11284 normalize_path(&format!("{virtual_home}/{suffix}"))
11285 })
11286}
11287
11288fn guest_path_from_runtime_host_mappings(
11289 runtime_env: &BTreeMap<String, String>,
11290 host_path: &Path,
11291) -> Option<String> {
11292 let mappings = runtime_env
11293 .get("AGENTOS_GUEST_PATH_MAPPINGS")
11294 .and_then(|value| serde_json::from_str::<Vec<RuntimeGuestPathMapping>>(value).ok())?;
11295 let normalized = normalize_host_path(host_path);
11296
11297 let mut sorted_mappings = mappings
11298 .into_iter()
11299 .filter_map(|mapping| {
11300 (!mapping.guest_path.is_empty() && !mapping.host_path.is_empty()).then_some((
11301 normalize_path(&mapping.guest_path),
11302 normalize_host_path(Path::new(&mapping.host_path)),
11303 ))
11304 })
11305 .collect::<Vec<_>>();
11306 sorted_mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.1.as_os_str().len()));
11307
11308 for (guest_root, host_root) in sorted_mappings {
11309 if !path_is_within_root(&normalized, &host_root) {
11310 continue;
11311 }
11312 let suffix = normalized
11313 .strip_prefix(&host_root)
11314 .ok()?
11315 .to_string_lossy()
11316 .replace('\\', "/")
11317 .trim_start_matches('/')
11318 .to_owned();
11319
11320 return Some(if suffix.is_empty() {
11321 guest_root
11322 } else if guest_root == "/" {
11323 normalize_path(&format!("/{suffix}"))
11324 } else {
11325 normalize_path(&format!("{guest_root}/{suffix}"))
11326 });
11327 }
11328
11329 None
11330}
11331
11332fn host_mount_path_for_guest_path_from_mounts(
11333 mounts: &[crate::protocol::MountDescriptor],
11334 guest_path: &str,
11335) -> Option<PathBuf> {
11336 let normalized = normalize_path(guest_path);
11337
11338 let mut host_mounts = mounts
11339 .iter()
11340 .filter_map(|mount| {
11341 ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access"))
11342 .then(|| {
11343 mount_config_host_path(&mount.plugin.config)
11344 .map(|host_path| (mount.guest_path.as_str(), host_path))
11345 })
11346 .flatten()
11347 })
11348 .collect::<Vec<_>>();
11349 host_mounts.sort_by_key(|mount| std::cmp::Reverse(mount.0.len()));
11350
11351 for (guest_root, host_root) in host_mounts {
11352 if normalized != guest_root && !normalized.starts_with(&format!("{guest_root}/")) {
11353 continue;
11354 }
11355
11356 let suffix = normalized
11357 .strip_prefix(guest_root)
11358 .unwrap_or_default()
11359 .trim_start_matches('/');
11360 let mut path = PathBuf::from(host_root);
11361 if !suffix.is_empty() {
11362 path.push(suffix);
11363 }
11364 return Some(path);
11365 }
11366
11367 None
11368}
11369
11370#[cfg(test)]
11371mod host_mount_path_for_guest_path_from_mounts_tests {
11372 use super::host_mount_path_for_guest_path_from_mounts;
11373 use crate::protocol::{MountDescriptor, MountPluginDescriptor};
11374 use serde_json::json;
11375 use std::path::PathBuf;
11376
11377 #[test]
11378 fn resolves_module_access_mount_paths() {
11379 let mounts = vec![MountDescriptor {
11380 guest_path: String::from("/root/node_modules"),
11381 read_only: true,
11382 plugin: MountPluginDescriptor {
11383 id: String::from("module_access"),
11384 config: json!({
11385 "hostPath": "/tmp/workspace/node_modules",
11386 })
11387 .to_string(),
11388 },
11389 }];
11390
11391 let resolved =
11392 host_mount_path_for_guest_path_from_mounts(&mounts, "/root/node_modules/pkg/index.js")
11393 .expect("module_access mount should resolve");
11394
11395 assert_eq!(
11396 resolved,
11397 PathBuf::from("/tmp/workspace/node_modules/pkg/index.js")
11398 );
11399 }
11400}
11401
11402fn resolve_guest_socket_host_path(
11403 context: &JavascriptSocketPathContext,
11404 guest_path: &str,
11405) -> PathBuf {
11406 if let Some(path) = host_mount_path_for_guest_path_from_mounts(&context.mounts, guest_path) {
11407 return path;
11408 }
11409
11410 let normalized = normalize_path(guest_path);
11411 let mut host_path = context.sandbox_root.clone();
11412 let suffix = normalized.trim_start_matches('/');
11413 if !suffix.is_empty() {
11414 host_path.push(suffix);
11415 }
11416 host_path
11417}
11418
11419fn ensure_kernel_parent_directories(
11420 kernel: &mut SidecarKernel,
11421 path: &str,
11422) -> Result<(), SidecarError> {
11423 let parent = dirname(path);
11424 if parent != "/" && !kernel.exists(&parent).map_err(kernel_error)? {
11425 kernel.mkdir(&parent, true).map_err(kernel_error)?;
11426 }
11427 Ok(())
11428}
11429
11430pub(crate) fn sanitize_javascript_child_process_internal_bootstrap_env(
11434 env: &BTreeMap<String, String>,
11435) -> BTreeMap<String, String> {
11436 const ALLOWED_KEYS: &[&str] = &[
11437 "AGENTOS_ALLOWED_NODE_BUILTINS",
11438 "AGENTOS_GUEST_PATH_MAPPINGS",
11439 "AGENTOS_LOOPBACK_EXEMPT_PORTS",
11440 "AGENTOS_VIRTUAL_PROCESS_EXEC_PATH",
11441 "AGENTOS_VIRTUAL_PROCESS_UID",
11442 "AGENTOS_VIRTUAL_PROCESS_GID",
11443 "AGENTOS_VIRTUAL_PROCESS_VERSION",
11444 ];
11445
11446 env.iter()
11447 .filter(|(key, _)| {
11448 ALLOWED_KEYS.contains(&key.as_str()) || key.starts_with("AGENTOS_VIRTUAL_OS_")
11449 })
11450 .map(|(key, value)| (key.clone(), value.clone()))
11451 .collect()
11452}
11453
11454fn resolve_tcp_bind_addr(host: &str, port: u16) -> Result<SocketAddr, SidecarError> {
11459 (host, port)
11460 .to_socket_addrs()
11461 .map_err(sidecar_net_error)?
11462 .next()
11463 .ok_or_else(|| {
11464 SidecarError::Execution(format!("failed to resolve TCP bind address {host}:{port}"))
11465 })
11466}
11467
11468pub(crate) fn format_dns_resource(hostname: &str) -> String {
11469 format!("dns://{hostname}")
11470}
11471
11472pub(crate) fn format_tcp_resource(host: &str, port: u16) -> String {
11473 format!("tcp://{host}:{port}")
11474}
11475
11476fn is_loopback_ip(ip: IpAddr) -> bool {
11477 match ip {
11478 IpAddr::V4(ip) => ip.is_loopback(),
11479 IpAddr::V6(ip) => {
11480 ip.is_loopback()
11481 || ip
11482 .to_ipv4_mapped()
11483 .is_some_and(|mapped| mapped.is_loopback())
11484 }
11485 }
11486}
11487
11488fn loopback_cidr(ip: IpAddr) -> &'static str {
11489 match ip {
11490 IpAddr::V4(ip) if ip.is_loopback() => "127.0.0.0/8",
11491 IpAddr::V6(ip)
11492 if ip
11493 .to_ipv4_mapped()
11494 .is_some_and(|mapped| mapped.is_loopback()) =>
11495 {
11496 "127.0.0.0/8"
11497 }
11498 IpAddr::V6(_) => "::1/128",
11499 IpAddr::V4(_) => "127.0.0.0/8",
11500 }
11501}
11502
11503fn ipv4_compatible_embedded(ip: Ipv6Addr) -> Option<Ipv4Addr> {
11509 let segments = ip.segments();
11510 if segments[0..6].iter().any(|&s| s != 0) {
11511 return None;
11512 }
11513 let embedded = (u32::from(segments[6]) << 16) | u32::from(segments[7]);
11514 if embedded == 0 || embedded == 1 {
11517 return None;
11518 }
11519 Some(Ipv4Addr::from(embedded))
11520}
11521
11522fn restricted_non_loopback_ip_range(ip: IpAddr) -> Option<(&'static str, &'static str)> {
11523 match ip {
11524 IpAddr::V4(ip) => {
11525 if ip.is_unspecified() {
11526 return Some(("0.0.0.0/32", "unspecified"));
11529 }
11530 let [first, second, ..] = ip.octets();
11531 match (first, second) {
11532 (10, _) => Some(("10.0.0.0/8", "private")),
11533 (100, 64..=127) => Some(("100.64.0.0/10", "carrier-grade-nat")),
11534 (172, 16..=31) => Some(("172.16.0.0/12", "private")),
11535 (192, 168) => Some(("192.168.0.0/16", "private")),
11536 (169, 254) => Some(("169.254.0.0/16", "link-local")),
11537 (224..=239, _) => Some(("224.0.0.0/4", "multicast")),
11542 (240..=255, _) => Some(("240.0.0.0/4", "reserved")),
11543 _ => None,
11544 }
11545 }
11546 IpAddr::V6(ip) => {
11547 if let Some(mapped) = ip.to_ipv4_mapped() {
11548 return restricted_non_loopback_ip_range(IpAddr::V4(mapped));
11549 }
11550 if let Some(compat) = ipv4_compatible_embedded(ip) {
11557 return restricted_non_loopback_ip_range(IpAddr::V4(compat));
11558 }
11559
11560 if ip.is_unspecified() {
11561 return Some(("::/128", "unspecified"));
11564 }
11565
11566 let segments = ip.segments();
11567 if (segments[0] & 0xfe00) == 0xfc00 {
11568 return Some(("fc00::/7", "unique-local"));
11569 }
11570 if (segments[0] & 0xffc0) == 0xfe80 {
11571 return Some(("fe80::/10", "link-local"));
11572 }
11573 None
11574 }
11575 }
11576}
11577
11578fn blocked_dns_resolution_error(
11579 resource: &str,
11580 ip: IpAddr,
11581 cidr: &str,
11582 label: &str,
11583) -> SidecarError {
11584 SidecarError::Execution(format!(
11585 "EACCES: blocked outbound network access to {resource}: {ip} is within restricted {label} range {cidr}"
11586 ))
11587}
11588
11589fn blocked_loopback_connect_error(resource: &str, ip: IpAddr, port: u16) -> SidecarError {
11590 SidecarError::Execution(format!(
11591 "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}",
11592 loopback_cidr(ip)
11593 ))
11594}
11595
11596fn filter_dns_safe_ip_addrs(
11597 addresses: Vec<IpAddr>,
11598 hostname: &str,
11599) -> Result<Vec<IpAddr>, SidecarError> {
11600 let resource = format_dns_resource(hostname);
11601 let mut allowed = Vec::new();
11602 let mut blocked = None;
11603
11604 for ip in addresses {
11605 if let Some((cidr, label)) = restricted_non_loopback_ip_range(ip) {
11606 blocked.get_or_insert((ip, cidr, label));
11607 continue;
11608 }
11609 allowed.push(ip);
11610 }
11611
11612 if allowed.is_empty() {
11613 let (ip, cidr, label) = blocked.expect("blocked DNS results should capture a reason");
11614 return Err(blocked_dns_resolution_error(&resource, ip, cidr, label));
11615 }
11616
11617 Ok(allowed)
11618}
11619
11620fn loopback_connect_allowed(context: &JavascriptSocketPathContext, port: u16) -> bool {
11621 context.loopback_port_allowed(port)
11622}
11623
11624fn filter_tcp_connect_ip_addrs(
11625 addresses: Vec<IpAddr>,
11626 host: &str,
11627 port: u16,
11628 context: &JavascriptSocketPathContext,
11629) -> Result<Vec<IpAddr>, SidecarError> {
11630 let resource = format_tcp_resource(host, port);
11631 let mut allowed = Vec::new();
11632 let mut blocked = None;
11633
11634 for ip in addresses {
11635 if let Some((cidr, label)) = restricted_non_loopback_ip_range(ip) {
11636 blocked.get_or_insert_with(|| blocked_dns_resolution_error(&resource, ip, cidr, label));
11637 continue;
11638 }
11639 if is_loopback_ip(ip) && !loopback_connect_allowed(context, port) {
11640 blocked.get_or_insert_with(|| blocked_loopback_connect_error(&resource, ip, port));
11641 continue;
11642 }
11643 allowed.push(ip);
11644 }
11645
11646 if allowed.is_empty() {
11647 return Err(blocked.expect("blocked TCP connect results should capture a reason"));
11648 }
11649
11650 Ok(allowed)
11651}
11652
11653fn resolve_tcp_connect_addr<B>(
11654 bridge: &SharedBridge<B>,
11655 kernel: &SidecarKernel,
11656 vm_id: &str,
11657 dns: &VmDnsConfig,
11658 host: &str,
11659 port: u16,
11660 context: &JavascriptSocketPathContext,
11661) -> Result<ResolvedTcpConnectAddr, SidecarError>
11662where
11663 B: NativeSidecarBridge + Send + 'static,
11664 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11665{
11666 let allowed = filter_tcp_connect_ip_addrs(
11667 resolve_dns_ip_addrs(
11668 bridge,
11669 kernel,
11670 vm_id,
11671 dns,
11672 host,
11673 DnsLookupPolicy::SkipPermissions,
11674 )?,
11675 host,
11676 port,
11677 context,
11678 )?;
11679 let ip = allowed
11680 .iter()
11681 .copied()
11682 .find(|candidate| {
11683 let family = JavascriptSocketFamily::from_ip(*candidate);
11684 context.translate_tcp_loopback_port(family, port).is_some()
11685 })
11686 .or_else(|| allowed.iter().copied().find(IpAddr::is_ipv4))
11689 .or_else(|| allowed.first().copied())
11690 .ok_or_else(|| {
11691 SidecarError::Execution(format!("failed to resolve TCP address {host}:{port}"))
11692 })?;
11693 let family = JavascriptSocketFamily::from_ip(ip);
11694 let translated_loopback_port = context.translate_tcp_loopback_port(family, port);
11695 let use_kernel_loopback = is_loopback_ip(ip) && translated_loopback_port == Some(port);
11696 let actual_port = if is_loopback_ip(ip) {
11697 translated_loopback_port.unwrap_or(port)
11698 } else {
11699 port
11700 };
11701 Ok(ResolvedTcpConnectAddr {
11702 actual_addr: SocketAddr::new(ip, actual_port),
11703 guest_remote_addr: SocketAddr::new(ip, port),
11704 use_kernel_loopback,
11705 })
11706}
11707
11708fn resolve_dns_ip_addrs<B>(
11709 bridge: &SharedBridge<B>,
11710 kernel: &SidecarKernel,
11711 vm_id: &str,
11712 dns: &VmDnsConfig,
11713 hostname: &str,
11714 policy: DnsLookupPolicy,
11715) -> Result<Vec<IpAddr>, SidecarError>
11716where
11717 B: NativeSidecarBridge + Send + 'static,
11718 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11719{
11720 let resolution = match kernel.resolve_dns(hostname, policy) {
11721 Ok(resolution) => resolution,
11722 Err(error) => {
11723 let sidecar_error = kernel_error(error.clone());
11724 if error.code() != "EACCES" {
11725 emit_dns_resolution_failure_event(bridge, vm_id, hostname, dns, &sidecar_error);
11726 }
11727 return Err(sidecar_error);
11728 }
11729 };
11730 emit_dns_resolution_event(
11731 bridge,
11732 vm_id,
11733 hostname,
11734 resolution.source(),
11735 resolution.addresses(),
11736 dns,
11737 );
11738 Ok(resolution.addresses().to_vec())
11739}
11740
11741fn resolve_dns_records<B>(
11742 bridge: &SharedBridge<B>,
11743 kernel: &SidecarKernel,
11744 vm_id: &str,
11745 dns: &VmDnsConfig,
11746 hostname: &str,
11747 record_type: RecordType,
11748 policy: DnsLookupPolicy,
11749) -> Result<DnsRecordResolution, SidecarError>
11750where
11751 B: NativeSidecarBridge + Send + 'static,
11752 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11753{
11754 let resolution = match kernel.resolve_dns_records(hostname, record_type, policy) {
11755 Ok(resolution) => resolution,
11756 Err(error) => {
11757 let sidecar_error = kernel_error(error.clone());
11758 if error.code() != "EACCES" {
11759 emit_dns_resolution_failure_event(bridge, vm_id, hostname, dns, &sidecar_error);
11760 }
11761 return Err(sidecar_error);
11762 }
11763 };
11764 emit_dns_record_resolution_event(bridge, vm_id, hostname, &resolution, dns);
11765 Ok(resolution)
11766}
11767
11768fn filter_dns_ip_addrs(
11769 addresses: Vec<IpAddr>,
11770 family: Option<u8>,
11771) -> Result<Vec<IpAddr>, SidecarError> {
11772 let filtered: Vec<_> = match family.unwrap_or(0) {
11773 0 => addresses,
11774 4 => addresses
11775 .into_iter()
11776 .filter(|ip| matches!(ip, IpAddr::V4(_)))
11777 .collect(),
11778 6 => addresses
11779 .into_iter()
11780 .filter(|ip| matches!(ip, IpAddr::V6(_)))
11781 .collect(),
11782 other => {
11783 return Err(SidecarError::InvalidState(format!(
11784 "unsupported dns family {other}"
11785 )));
11786 }
11787 };
11788
11789 if filtered.is_empty() {
11790 return Err(SidecarError::Execution(String::from(
11791 "failed to resolve DNS address for requested family",
11792 )));
11793 }
11794
11795 Ok(filtered)
11796}
11797
11798fn resolve_udp_bind_addr(
11799 host: &str,
11800 port: u16,
11801 family: JavascriptUdpFamily,
11802) -> Result<SocketAddr, SidecarError> {
11803 (host, port)
11804 .to_socket_addrs()
11805 .map_err(sidecar_net_error)?
11806 .find(|addr| family.matches_addr(addr))
11807 .ok_or_else(|| {
11808 SidecarError::Execution(format!(
11809 "failed to resolve {} UDP bind address {host}:{port}",
11810 family.socket_type()
11811 ))
11812 })
11813}
11814
11815fn resolve_udp_addr<B>(request: UdpRemoteAddrRequest<'_, B>) -> Result<SocketAddr, SidecarError>
11816where
11817 B: NativeSidecarBridge + Send + 'static,
11818 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
11819{
11820 let UdpRemoteAddrRequest {
11821 bridge,
11822 kernel,
11823 vm_id,
11824 dns,
11825 host,
11826 port,
11827 family,
11828 context,
11829 } = request;
11830 resolve_dns_ip_addrs(
11831 bridge,
11832 kernel,
11833 vm_id,
11834 dns,
11835 host,
11836 DnsLookupPolicy::SkipPermissions,
11837 )?
11838 .into_iter()
11839 .map(|ip| {
11840 let family_key = JavascriptSocketFamily::from_ip(ip);
11841 let actual_port = if is_loopback_ip(ip) {
11842 context
11843 .translate_udp_loopback_port(family_key, port)
11844 .unwrap_or(port)
11845 } else {
11846 port
11847 };
11848 SocketAddr::new(ip, actual_port)
11849 })
11850 .find(|addr| family.matches_addr(addr))
11851 .ok_or_else(|| {
11852 SidecarError::Execution(format!(
11853 "failed to resolve {} UDP address {host}:{port}",
11854 family.socket_type()
11855 ))
11856 })
11857}
11858
11859fn socket_addr_family(addr: &SocketAddr) -> &'static str {
11860 match addr {
11861 SocketAddr::V4(_) => "IPv4",
11862 SocketAddr::V6(_) => "IPv6",
11863 }
11864}
11865
11866fn javascript_net_timeout_value() -> Value {
11867 Value::String(String::from(JAVASCRIPT_NET_TIMEOUT_SENTINEL))
11868}
11869
11870fn javascript_net_json_string(value: Value, label: &str) -> Result<Value, SidecarError> {
11871 serde_json::to_string(&value)
11872 .map(Value::String)
11873 .map_err(|error| {
11874 SidecarError::InvalidState(format!("failed to serialize {label} payload: {error}"))
11875 })
11876}
11877
11878fn javascript_net_read_value(
11879 event: Option<JavascriptTcpSocketEvent>,
11880) -> Result<Value, SidecarError> {
11881 match event {
11882 Some(JavascriptTcpSocketEvent::Data(chunk)) => Ok(Value::String(
11883 base64::engine::general_purpose::STANDARD.encode(chunk),
11884 )),
11885 Some(JavascriptTcpSocketEvent::End | JavascriptTcpSocketEvent::Close { .. }) => {
11886 Ok(Value::Null)
11887 }
11888 Some(JavascriptTcpSocketEvent::Error { code, message }) => {
11889 let detail = code.unwrap_or_else(|| String::from("socket read"));
11890 Err(SidecarError::Execution(format!("{detail}: {message}")))
11891 }
11892 None => Ok(javascript_net_timeout_value()),
11893 }
11894}
11895
11896fn io_error_code(error: &std::io::Error) -> Option<String> {
11897 match error.raw_os_error() {
11898 Some(libc::EADDRINUSE) => Some(String::from("EADDRINUSE")),
11899 Some(libc::EADDRNOTAVAIL) => Some(String::from("EADDRNOTAVAIL")),
11900 Some(libc::ECONNREFUSED) => Some(String::from("ECONNREFUSED")),
11901 Some(libc::ECONNRESET) => Some(String::from("ECONNRESET")),
11902 Some(libc::EINVAL) => Some(String::from("EINVAL")),
11903 Some(libc::EPIPE) => Some(String::from("EPIPE")),
11904 Some(libc::ETIMEDOUT) => Some(String::from("ETIMEDOUT")),
11905 Some(libc::EHOSTUNREACH) => Some(String::from("EHOSTUNREACH")),
11906 Some(libc::ENETUNREACH) => Some(String::from("ENETUNREACH")),
11907 _ => None,
11908 }
11909}
11910
11911fn sidecar_net_error(error: std::io::Error) -> SidecarError {
11912 let message = match io_error_code(&error) {
11913 Some(code) => format!("{code}: {error}"),
11914 None => error.to_string(),
11915 };
11916 SidecarError::Execution(message)
11917}
11918
11919fn tls_provider() -> Arc<rustls::crypto::CryptoProvider> {
11920 Arc::new(aws_lc_rs::default_provider())
11921}
11922
11923fn tls_local_certificates(
11924 options: &JavascriptTlsBridgeOptions,
11925) -> Result<Vec<Vec<u8>>, SidecarError> {
11926 let Some(certificates) = options.cert.as_ref() else {
11927 return Ok(Vec::new());
11928 };
11929 tls_material_entries(certificates)
11930}
11931
11932fn tls_material_entries(material: &JavascriptTlsMaterial) -> Result<Vec<Vec<u8>>, SidecarError> {
11933 match material {
11934 JavascriptTlsMaterial::Single(entry) => tls_data_value(entry).map(|value| vec![value]),
11935 JavascriptTlsMaterial::Many(entries) => entries.iter().map(tls_data_value).collect(),
11936 }
11937}
11938
11939fn tls_data_value(value: &JavascriptTlsDataValue) -> Result<Vec<u8>, SidecarError> {
11940 match value {
11941 JavascriptTlsDataValue::Buffer { data } => base64::engine::general_purpose::STANDARD
11942 .decode(data)
11943 .map_err(|error| {
11944 SidecarError::InvalidState(format!("TLS material contains invalid base64: {error}"))
11945 }),
11946 JavascriptTlsDataValue::String { data } => Ok(data.as_bytes().to_vec()),
11947 }
11948}
11949
11950fn tls_certificates_from_material(
11951 material: &JavascriptTlsMaterial,
11952) -> Result<Vec<CertificateDer<'static>>, SidecarError> {
11953 let mut certificates = Vec::new();
11954 for entry in tls_material_entries(material)? {
11955 let mut reader = std::io::BufReader::new(Cursor::new(entry.clone()));
11956 let parsed = rustls_pemfile::certs(&mut reader)
11957 .collect::<Result<Vec<_>, _>>()
11958 .map_err(sidecar_net_error)?;
11959 if parsed.is_empty() {
11960 certificates.push(CertificateDer::from(entry));
11961 } else {
11962 certificates.extend(parsed);
11963 }
11964 }
11965 if certificates.is_empty() {
11966 return Err(SidecarError::InvalidState(String::from(
11967 "TLS certificate material did not contain any certificates",
11968 )));
11969 }
11970 Ok(certificates)
11971}
11972
11973fn tls_private_key_from_material(
11974 material: &JavascriptTlsMaterial,
11975) -> Result<PrivateKeyDer<'static>, SidecarError> {
11976 for entry in tls_material_entries(material)? {
11977 let mut reader = std::io::BufReader::new(Cursor::new(entry));
11978 if let Some(key) = rustls_pemfile::private_key(&mut reader).map_err(sidecar_net_error)? {
11979 return Ok(key);
11980 }
11981 }
11982 Err(SidecarError::InvalidState(String::from(
11983 "TLS private key material did not contain a supported key",
11984 )))
11985}
11986
11987fn tls_root_store(options: &JavascriptTlsBridgeOptions) -> Result<RootCertStore, SidecarError> {
11988 let mut roots = RootCertStore::empty();
11989 if let Some(ca) = options.ca.as_ref() {
11990 for certificate in tls_certificates_from_material(ca)? {
11991 roots.add(certificate).map_err(|error| {
11992 SidecarError::InvalidState(format!("failed to add TLS CA certificate: {error}"))
11993 })?;
11994 }
11995 return Ok(roots);
11996 }
11997
11998 for certificate in rustls_native_certs::load_native_certs().certs {
11999 roots.add(certificate).map_err(|error| {
12000 SidecarError::InvalidState(format!(
12001 "failed to add native TLS certificate to root store: {error}"
12002 ))
12003 })?;
12004 }
12005 Ok(roots)
12006}
12007
12008fn build_client_tls_stream(
12009 stream: TcpStream,
12010 options: &JavascriptTlsBridgeOptions,
12011) -> Result<rustls::StreamOwned<ClientConnection, TcpStream>, SidecarError> {
12012 let config = build_client_tls_config(options)?;
12013 let server_name = options
12014 .servername
12015 .clone()
12016 .unwrap_or_else(|| String::from("localhost"));
12017 let server_name = ServerName::try_from(server_name)
12018 .map_err(|_| SidecarError::InvalidState(String::from("invalid TLS servername")))?;
12019 stream
12020 .set_read_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
12021 .map_err(sidecar_net_error)?;
12022 stream
12023 .set_write_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
12024 .map_err(sidecar_net_error)?;
12025 let mut tls_stream = rustls::StreamOwned::new(
12026 ClientConnection::new(Arc::new(config), server_name).map_err(|error| {
12027 SidecarError::Execution(format!("failed to start TLS client: {error}"))
12028 })?,
12029 stream,
12030 );
12031 while tls_stream.conn.is_handshaking() {
12032 tls_stream
12033 .conn
12034 .complete_io(&mut tls_stream.sock)
12035 .map_err(sidecar_net_error)?;
12036 }
12037 tls_stream
12038 .sock
12039 .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
12040 .map_err(sidecar_net_error)?;
12041 tls_stream
12042 .sock
12043 .set_write_timeout(None)
12044 .map_err(sidecar_net_error)?;
12045 Ok(tls_stream)
12046}
12047
12048fn build_client_loopback_tls_stream(
12049 transport: crate::state::LoopbackTlsEndpoint,
12050 options: &JavascriptTlsBridgeOptions,
12051) -> Result<rustls::StreamOwned<ClientConnection, crate::state::LoopbackTlsEndpoint>, SidecarError>
12052{
12053 let config = build_client_tls_config(options)?;
12054 let server_name = options
12055 .servername
12056 .clone()
12057 .unwrap_or_else(|| String::from("localhost"));
12058 let server_name = ServerName::try_from(server_name)
12059 .map_err(|_| SidecarError::InvalidState(String::from("invalid TLS servername")))?;
12060 let mut tls_stream = rustls::StreamOwned::new(
12061 ClientConnection::new(Arc::new(config), server_name).map_err(|error| {
12062 SidecarError::Execution(format!("failed to start TLS client: {error}"))
12063 })?,
12064 transport,
12065 );
12066 match tls_stream.conn.complete_io(&mut tls_stream.sock) {
12067 Ok(_) => {}
12068 Err(error)
12069 if matches!(
12070 error.kind(),
12071 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
12072 ) => {}
12073 Err(error) => return Err(sidecar_net_error(error)),
12074 }
12075 Ok(tls_stream)
12076}
12077
12078fn build_client_tls_config(
12079 options: &JavascriptTlsBridgeOptions,
12080) -> Result<ClientConfig, SidecarError> {
12081 let provider = tls_provider();
12082 let builder = ClientConfig::builder_with_provider(provider.clone())
12083 .with_safe_default_protocol_versions()
12084 .map_err(|error| {
12085 SidecarError::InvalidState(format!("invalid TLS protocol config: {error}"))
12086 })?;
12087
12088 let mut config = if options.reject_unauthorized == Some(false) {
12089 let verifier = Arc::new(InsecureTlsVerifier {
12090 supported_schemes: provider
12091 .signature_verification_algorithms
12092 .supported_schemes(),
12093 });
12094 builder
12095 .dangerous()
12096 .with_custom_certificate_verifier(verifier)
12097 .with_no_client_auth()
12098 } else {
12099 builder
12100 .with_root_certificates(tls_root_store(options)?)
12101 .with_no_client_auth()
12102 };
12103
12104 if let Some(protocols) = options.alpn_protocols.as_ref() {
12105 config.alpn_protocols = protocols
12106 .iter()
12107 .map(|protocol| protocol.as_bytes().to_vec())
12108 .collect();
12109 }
12110 Ok(config)
12111}
12112
12113fn build_server_tls_stream(
12114 stream: TcpStream,
12115 options: &JavascriptTlsBridgeOptions,
12116) -> Result<rustls::StreamOwned<ServerConnection, TcpStream>, SidecarError> {
12117 let config = build_server_tls_config(options)?;
12118 stream
12119 .set_read_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
12120 .map_err(sidecar_net_error)?;
12121 stream
12122 .set_write_timeout(Some(TLS_HANDSHAKE_TIMEOUT))
12123 .map_err(sidecar_net_error)?;
12124 let mut tls_stream = rustls::StreamOwned::new(
12125 ServerConnection::new(Arc::new(config)).map_err(|error| {
12126 SidecarError::Execution(format!("failed to start TLS server: {error}"))
12127 })?,
12128 stream,
12129 );
12130 while tls_stream.conn.is_handshaking() {
12131 tls_stream
12132 .conn
12133 .complete_io(&mut tls_stream.sock)
12134 .map_err(sidecar_net_error)?;
12135 }
12136 tls_stream
12137 .sock
12138 .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT))
12139 .map_err(sidecar_net_error)?;
12140 tls_stream
12141 .sock
12142 .set_write_timeout(None)
12143 .map_err(sidecar_net_error)?;
12144 Ok(tls_stream)
12145}
12146
12147fn build_server_loopback_tls_stream(
12148 transport: crate::state::LoopbackTlsEndpoint,
12149 options: &JavascriptTlsBridgeOptions,
12150) -> Result<rustls::StreamOwned<ServerConnection, crate::state::LoopbackTlsEndpoint>, SidecarError>
12151{
12152 let config = build_server_tls_config(options)?;
12153 Ok(rustls::StreamOwned::new(
12154 ServerConnection::new(Arc::new(config)).map_err(|error| {
12155 SidecarError::Execution(format!("failed to start TLS server: {error}"))
12156 })?,
12157 transport,
12158 ))
12159}
12160
12161fn build_server_tls_config(
12162 options: &JavascriptTlsBridgeOptions,
12163) -> Result<ServerConfig, SidecarError> {
12164 let certificates = tls_certificates_from_material(options.cert.as_ref().ok_or_else(|| {
12165 SidecarError::InvalidState(String::from("TLS server upgrade requires a certificate"))
12166 })?)?;
12167 let key = tls_private_key_from_material(options.key.as_ref().ok_or_else(|| {
12168 SidecarError::InvalidState(String::from("TLS server upgrade requires a private key"))
12169 })?)?;
12170
12171 let mut config = ServerConfig::builder_with_provider(tls_provider())
12172 .with_safe_default_protocol_versions()
12173 .map_err(|error| {
12174 SidecarError::InvalidState(format!("invalid TLS protocol config: {error}"))
12175 })?
12176 .with_no_client_auth()
12177 .with_single_cert(certificates, key)
12178 .map_err(|error| {
12179 SidecarError::InvalidState(format!("invalid TLS server config: {error}"))
12180 })?;
12181
12182 if let Some(protocols) = options.alpn_protocols.as_ref() {
12183 config.alpn_protocols = protocols
12184 .iter()
12185 .map(|protocol| protocol.as_bytes().to_vec())
12186 .collect();
12187 }
12188 Ok(config)
12189}
12190
12191fn tls_protocol_name(version: rustls::ProtocolVersion) -> String {
12192 match version {
12193 rustls::ProtocolVersion::TLSv1_2 => String::from("TLSv1.2"),
12194 rustls::ProtocolVersion::TLSv1_3 => String::from("TLSv1.3"),
12195 other => other
12196 .as_str()
12197 .map(str::to_owned)
12198 .unwrap_or_else(|| format!("{other:?}")),
12199 }
12200}
12201
12202fn tls_cipher_bridge_value(suite: rustls::SupportedCipherSuite) -> Value {
12203 tls_bridge_object(vec![
12204 (
12205 "name",
12206 suite
12207 .suite()
12208 .as_str()
12209 .map(|value| Value::String(value.to_owned()))
12210 .unwrap_or(Value::Null),
12211 ),
12212 (
12213 "standardName",
12214 suite
12215 .suite()
12216 .as_str()
12217 .map(|value| Value::String(value.to_owned()))
12218 .unwrap_or(Value::Null),
12219 ),
12220 (
12221 "version",
12222 Value::String(if suite.tls13().is_some() {
12223 String::from("TLSv1.3")
12224 } else {
12225 String::from("TLSv1.2")
12226 }),
12227 ),
12228 ])
12229}
12230
12231fn tls_certificate_bridge_value(certificate: &[u8], detailed: bool) -> Value {
12232 let mut fields = vec![("raw", tls_bridge_buffer_value(certificate))];
12233 if detailed {
12234 fields.push(("issuerCertificate", tls_bridge_undefined_value()));
12235 }
12236 tls_bridge_object(fields)
12237}
12238
12239fn tls_bridge_buffer_value(bytes: &[u8]) -> Value {
12240 json!({
12241 "type": "buffer",
12242 "data": base64::engine::general_purpose::STANDARD.encode(bytes),
12243 })
12244}
12245
12246fn tls_bridge_object(entries: Vec<(&str, Value)>) -> Value {
12247 let value = entries
12248 .into_iter()
12249 .map(|(key, value)| (key.to_owned(), value))
12250 .collect::<serde_json::Map<String, Value>>();
12251 json!({
12252 "type": "object",
12253 "id": 1,
12254 "value": value,
12255 })
12256}
12257
12258fn tls_bridge_undefined_value() -> Value {
12259 json!({
12260 "type": "undefined",
12261 })
12262}
12263
12264fn spawn_tcp_socket_reader(
12265 stream: TcpStream,
12266 sender: Sender<JavascriptTcpSocketEvent>,
12267 tls_mode: Arc<AtomicBool>,
12268 saw_local_shutdown: Arc<AtomicBool>,
12269 saw_remote_end: Arc<AtomicBool>,
12270 close_notified: Arc<AtomicBool>,
12271) {
12272 thread::spawn(move || {
12273 let mut stream = stream;
12274 let mut buffer = vec![0_u8; 64 * 1024];
12275 loop {
12276 if tls_mode.load(Ordering::SeqCst) {
12277 break;
12278 }
12279 match stream.read(&mut buffer) {
12280 Ok(0) => {
12281 saw_remote_end.store(true, Ordering::SeqCst);
12282 let _ = sender.send(JavascriptTcpSocketEvent::End);
12283 if saw_local_shutdown.load(Ordering::SeqCst)
12284 && !close_notified.swap(true, Ordering::SeqCst)
12285 {
12286 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12287 }
12288 break;
12289 }
12290 Ok(bytes_read) => {
12291 if sender
12292 .send(JavascriptTcpSocketEvent::Data(
12293 buffer[..bytes_read].to_vec(),
12294 ))
12295 .is_err()
12296 {
12297 break;
12298 }
12299 }
12300 Err(error)
12301 if matches!(
12302 error.kind(),
12303 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
12304 ) =>
12305 {
12306 continue;
12307 }
12308 Err(error) => {
12309 let code = io_error_code(&error);
12310 let _ = sender.send(JavascriptTcpSocketEvent::Error {
12311 code,
12312 message: error.to_string(),
12313 });
12314 if !close_notified.swap(true, Ordering::SeqCst) {
12315 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12316 }
12317 break;
12318 }
12319 }
12320 }
12321 });
12322}
12323
12324fn spawn_tls_socket_reader(
12325 tls_stream: Arc<Mutex<Option<ActiveTlsStream>>>,
12326 sender: Sender<JavascriptTcpSocketEvent>,
12327 saw_local_shutdown: Arc<AtomicBool>,
12328 saw_remote_end: Arc<AtomicBool>,
12329 close_notified: Arc<AtomicBool>,
12330) {
12331 thread::spawn(move || {
12332 let mut buffer = vec![0_u8; 64 * 1024];
12333 loop {
12334 let read_result = {
12335 let mut guard = match tls_stream.lock() {
12336 Ok(guard) => guard,
12337 Err(_) => return,
12338 };
12339 let Some(stream) = guard.as_mut() else {
12340 return;
12341 };
12342 stream.read(&mut buffer)
12343 };
12344
12345 match read_result {
12346 Ok(0) => {
12347 saw_remote_end.store(true, Ordering::SeqCst);
12348 let _ = sender.send(JavascriptTcpSocketEvent::End);
12349 if saw_local_shutdown.load(Ordering::SeqCst)
12350 && !close_notified.swap(true, Ordering::SeqCst)
12351 {
12352 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12353 }
12354 break;
12355 }
12356 Ok(bytes_read) => {
12357 if sender
12358 .send(JavascriptTcpSocketEvent::Data(
12359 buffer[..bytes_read].to_vec(),
12360 ))
12361 .is_err()
12362 {
12363 break;
12364 }
12365 }
12366 Err(error)
12367 if matches!(
12368 error.kind(),
12369 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
12370 ) =>
12371 {
12372 std::thread::sleep(Duration::from_millis(1));
12375 continue;
12376 }
12377 Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => {
12378 saw_remote_end.store(true, Ordering::SeqCst);
12379 let _ = sender.send(JavascriptTcpSocketEvent::End);
12380 if saw_local_shutdown.load(Ordering::SeqCst)
12381 && !close_notified.swap(true, Ordering::SeqCst)
12382 {
12383 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12384 }
12385 break;
12386 }
12387 Err(error) => {
12388 let code = io_error_code(&error);
12389 let _ = sender.send(JavascriptTcpSocketEvent::Error {
12390 code,
12391 message: error.to_string(),
12392 });
12393 if !close_notified.swap(true, Ordering::SeqCst) {
12394 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12395 }
12396 break;
12397 }
12398 }
12399 }
12400 });
12401}
12402
12403fn spawn_unix_socket_reader(
12404 stream: UnixStream,
12405 sender: Sender<JavascriptTcpSocketEvent>,
12406 saw_local_shutdown: Arc<AtomicBool>,
12407 saw_remote_end: Arc<AtomicBool>,
12408 close_notified: Arc<AtomicBool>,
12409) {
12410 thread::spawn(move || {
12411 let mut stream = stream;
12412 let mut buffer = vec![0_u8; 64 * 1024];
12413 loop {
12414 match stream.read(&mut buffer) {
12415 Ok(0) => {
12416 saw_remote_end.store(true, Ordering::SeqCst);
12417 let _ = sender.send(JavascriptTcpSocketEvent::End);
12418 if saw_local_shutdown.load(Ordering::SeqCst)
12419 && !close_notified.swap(true, Ordering::SeqCst)
12420 {
12421 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false });
12422 }
12423 break;
12424 }
12425 Ok(bytes_read) => {
12426 if sender
12427 .send(JavascriptTcpSocketEvent::Data(
12428 buffer[..bytes_read].to_vec(),
12429 ))
12430 .is_err()
12431 {
12432 break;
12433 }
12434 }
12435 Err(error) => {
12436 let code = io_error_code(&error);
12437 let _ = sender.send(JavascriptTcpSocketEvent::Error {
12438 code,
12439 message: error.to_string(),
12440 });
12441 if !close_notified.swap(true, Ordering::SeqCst) {
12442 let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true });
12443 }
12444 break;
12445 }
12446 }
12447 }
12448 });
12449}
12450
12451fn terminate_child_process_tree(kernel: &mut SidecarKernel, process: &mut ActiveProcess) {
12452 let sqlite_database_ids = process.sqlite_databases.keys().copied().collect::<Vec<_>>();
12453 for database_id in sqlite_database_ids {
12454 let _ = close_sqlite_database(kernel, process, database_id);
12455 }
12456 process.sqlite_statements.clear();
12457 process.http_servers.clear();
12458 process.pending_http_requests.clear();
12459 if let Ok(mut http2) = process.http2.shared.lock() {
12460 let sessions = http2.sessions.values().cloned().collect::<Vec<_>>();
12461 http2.server_events.clear();
12462 http2.session_events.clear();
12463 http2.streams.clear();
12464 http2.servers.clear();
12465 http2.sessions.clear();
12466 drop(http2);
12467 for session in sessions {
12468 let (respond_to, _rx) = mpsc::channel();
12469 let _ = session.command_tx.send(Http2SessionCommand::Close {
12470 abrupt: true,
12471 respond_to,
12472 });
12473 }
12474 }
12475
12476 let listener_ids = process.tcp_listeners.keys().cloned().collect::<Vec<_>>();
12477 for listener_id in listener_ids {
12478 if let Some(listener) = process.tcp_listeners.remove(&listener_id) {
12479 let _ = listener.close(kernel, process.kernel_pid);
12480 }
12481 }
12482
12483 let sockets = process.tcp_sockets.keys().cloned().collect::<Vec<_>>();
12484 for socket_id in sockets {
12485 if let Some(socket) = process.tcp_sockets.remove(&socket_id) {
12486 let _ = socket.close(kernel, process.kernel_pid);
12487 }
12488 }
12489
12490 let unix_listener_ids = process.unix_listeners.keys().cloned().collect::<Vec<_>>();
12491 for listener_id in unix_listener_ids {
12492 if let Some(listener) = process.unix_listeners.remove(&listener_id) {
12493 let _ = listener.close();
12494 }
12495 }
12496
12497 let unix_sockets = process.unix_sockets.keys().cloned().collect::<Vec<_>>();
12498 for socket_id in unix_sockets {
12499 if let Some(socket) = process.unix_sockets.remove(&socket_id) {
12500 let _ = socket.close();
12501 }
12502 }
12503
12504 let udp_socket_ids = process.udp_sockets.keys().cloned().collect::<Vec<_>>();
12505 for socket_id in udp_socket_ids {
12506 if let Some(mut socket) = process.udp_sockets.remove(&socket_id) {
12507 socket.close(kernel, process.kernel_pid);
12508 }
12509 }
12510
12511 let child_ids = process.child_processes.keys().cloned().collect::<Vec<_>>();
12512 for child_id in child_ids {
12513 let Some(mut child) = process.child_processes.remove(&child_id) else {
12514 continue;
12515 };
12516 terminate_child_process_tree(kernel, &mut child);
12517 let _ = kernel.kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, SIGTERM);
12518 let _ = signal_runtime_process(child.execution.child_pid(), SIGTERM);
12519 child.kernel_handle.finish(0);
12520 let _ = kernel.wait_and_reap(child.kernel_pid);
12521 }
12522}
12523
12524fn service_javascript_sqlite_sync_rpc(
12525 kernel: &mut SidecarKernel,
12526 process: &mut ActiveProcess,
12527 request: &JavascriptSyncRpcRequest,
12528) -> Result<Value, SidecarError> {
12529 match request.method.as_str() {
12530 "sqlite.constants" => Ok(json!({})),
12531 "sqlite.open" => sqlite_open_database(kernel, process, request),
12532 "sqlite.close" => {
12533 let database_id =
12534 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.close database id")?;
12535 close_sqlite_database(kernel, process, database_id)?;
12536 Ok(Value::Null)
12537 }
12538 "sqlite.exec" => sqlite_exec_database(kernel, process, request),
12539 "sqlite.query" => sqlite_query_database(process, request),
12540 "sqlite.prepare" => sqlite_prepare_statement(process, request),
12541 "sqlite.location" => {
12542 let database_id =
12543 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.location database id")?;
12544 let database = sqlite_database(process, database_id)?;
12545 Ok(database
12546 .vm_path
12547 .as_ref()
12548 .map(|path| Value::String(path.clone()))
12549 .unwrap_or(Value::Null))
12550 }
12551 "sqlite.checkpoint" => {
12552 let database_id =
12553 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.checkpoint database id")?;
12554 let kernel_pid = process.kernel_pid;
12555 let database = sqlite_database_mut(process, database_id)?;
12556 sqlite_sync_database(kernel, kernel_pid, database)?;
12557 Ok(Value::Null)
12558 }
12559 "sqlite.statement.run" => sqlite_run_statement(kernel, process, request),
12560 "sqlite.statement.get" => sqlite_get_statement(process, request),
12561 "sqlite.statement.all" | "sqlite.statement.iterate" => {
12562 sqlite_all_statement(process, request)
12563 }
12564 "sqlite.statement.columns" => sqlite_statement_columns(process, request),
12565 "sqlite.statement.setReturnArrays" => {
12566 let statement_id = javascript_sync_rpc_arg_u64(
12567 &request.args,
12568 0,
12569 "sqlite.statement.setReturnArrays statement id",
12570 )?;
12571 let enabled = javascript_sync_rpc_arg_bool(
12572 &request.args,
12573 1,
12574 "sqlite.statement.setReturnArrays enabled",
12575 )?;
12576 sqlite_statement_mut(process, statement_id)?.return_arrays = enabled;
12577 Ok(Value::Null)
12578 }
12579 "sqlite.statement.setReadBigInts" => {
12580 let statement_id = javascript_sync_rpc_arg_u64(
12581 &request.args,
12582 0,
12583 "sqlite.statement.setReadBigInts statement id",
12584 )?;
12585 let enabled = javascript_sync_rpc_arg_bool(
12586 &request.args,
12587 1,
12588 "sqlite.statement.setReadBigInts enabled",
12589 )?;
12590 sqlite_statement_mut(process, statement_id)?.read_bigints = enabled;
12591 Ok(Value::Null)
12592 }
12593 "sqlite.statement.setAllowBareNamedParameters" => {
12594 let statement_id = javascript_sync_rpc_arg_u64(
12595 &request.args,
12596 0,
12597 "sqlite.statement.setAllowBareNamedParameters statement id",
12598 )?;
12599 let enabled = javascript_sync_rpc_arg_bool(
12600 &request.args,
12601 1,
12602 "sqlite.statement.setAllowBareNamedParameters enabled",
12603 )?;
12604 sqlite_statement_mut(process, statement_id)?.allow_bare_named_parameters = enabled;
12605 Ok(Value::Null)
12606 }
12607 "sqlite.statement.setAllowUnknownNamedParameters" => {
12608 let statement_id = javascript_sync_rpc_arg_u64(
12609 &request.args,
12610 0,
12611 "sqlite.statement.setAllowUnknownNamedParameters statement id",
12612 )?;
12613 let enabled = javascript_sync_rpc_arg_bool(
12614 &request.args,
12615 1,
12616 "sqlite.statement.setAllowUnknownNamedParameters enabled",
12617 )?;
12618 sqlite_statement_mut(process, statement_id)?.allow_unknown_named_parameters = enabled;
12619 Ok(Value::Null)
12620 }
12621 "sqlite.statement.finalize" => {
12622 let statement_id = javascript_sync_rpc_arg_u64(
12623 &request.args,
12624 0,
12625 "sqlite.statement.finalize statement id",
12626 )?;
12627 process
12628 .sqlite_statements
12629 .remove(&statement_id)
12630 .ok_or_else(|| {
12631 SidecarError::InvalidState(format!(
12632 "sqlite statement handle not found: {statement_id}"
12633 ))
12634 })?;
12635 Ok(Value::Null)
12636 }
12637 other => Err(SidecarError::InvalidState(format!(
12638 "unsupported JavaScript sqlite sync RPC method {other}"
12639 ))),
12640 }
12641}
12642
12643fn sqlite_open_database(
12644 kernel: &mut SidecarKernel,
12645 process: &mut ActiveProcess,
12646 request: &JavascriptSyncRpcRequest,
12647) -> Result<Value, SidecarError> {
12648 ensure_per_process_state_handle_capacity(process.sqlite_databases.len(), "sqlite database")?;
12649 let path = request.args.first().and_then(Value::as_str);
12650 let vm_path = path.filter(|value| !value.is_empty() && *value != ":memory:");
12651 let options = request.args.get(1);
12652 let read_only = sqlite_option_bool(options, "readOnly").unwrap_or(false);
12653 let create = sqlite_option_bool(options, "create").unwrap_or(!read_only);
12654 let timeout_ms = sqlite_option_u64(options, "timeout");
12655
12656 process.next_sqlite_database_id += 1;
12657 let database_id = process.next_sqlite_database_id;
12658
12659 let host_path = if vm_path.is_some() {
12660 Some(
12661 std::env::temp_dir()
12662 .join(format!(
12663 "secure-exec-sidecar-sqlite-{}-{database_id}",
12664 process.kernel_pid
12665 ))
12666 .join("database.sqlite"),
12667 )
12668 } else {
12669 None
12670 };
12671
12672 if let Some(host_path) = host_path.as_ref() {
12673 if let Some(parent) = host_path.parent() {
12674 fs::create_dir_all(parent).map_err(|error| {
12675 SidecarError::Io(format!(
12676 "failed to prepare sqlite temp directory {}: {error}",
12677 parent.display()
12678 ))
12679 })?;
12680 }
12681 }
12682
12683 if let (Some(vm_path), Some(host_path)) = (vm_path, host_path.as_ref()) {
12684 if kernel
12685 .exists_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, vm_path)
12686 .map_err(kernel_error)?
12687 {
12688 let contents = kernel
12689 .read_file_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, vm_path)
12690 .map_err(kernel_error)?;
12691 fs::write(host_path, contents).map_err(|error| {
12692 SidecarError::Io(format!(
12693 "failed to materialize sqlite database {}: {error}",
12694 host_path.display()
12695 ))
12696 })?;
12697 } else if read_only && !create {
12698 return Err(SidecarError::InvalidState(format!(
12699 "sqlite database does not exist: {vm_path}"
12700 )));
12701 }
12702 }
12703
12704 let target = host_path
12705 .as_ref()
12706 .map(|path| path.to_string_lossy().into_owned())
12707 .unwrap_or_else(|| String::from(":memory:"));
12708 let mut flags = if read_only {
12709 SqliteOpenFlags::SQLITE_OPEN_READ_ONLY
12710 } else {
12711 SqliteOpenFlags::SQLITE_OPEN_READ_WRITE
12712 };
12713 if create && !read_only {
12714 flags |= SqliteOpenFlags::SQLITE_OPEN_CREATE;
12715 }
12716
12717 let connection = SqliteConnection::open_with_flags(&target, flags).map_err(|error| {
12718 SidecarError::InvalidState(format!(
12719 "sqlite database open failed for {}: {error}",
12720 vm_path.unwrap_or(":memory:")
12721 ))
12722 })?;
12723 if let Some(timeout_ms) = timeout_ms {
12724 connection
12725 .busy_timeout(Duration::from_millis(timeout_ms))
12726 .map_err(sqlite_error)?;
12727 }
12728 if host_path.is_some() && !read_only {
12729 let _ = connection.pragma_update(None, "journal_mode", "WAL");
12730 }
12731
12732 process.sqlite_databases.insert(
12733 database_id,
12734 ActiveSqliteDatabase {
12735 connection,
12736 host_path,
12737 vm_path: vm_path.map(String::from),
12738 dirty: false,
12739 transaction_depth: 0,
12740 read_only,
12741 },
12742 );
12743
12744 Ok(json!(database_id))
12745}
12746
12747fn sqlite_exec_database(
12748 kernel: &mut SidecarKernel,
12749 process: &mut ActiveProcess,
12750 request: &JavascriptSyncRpcRequest,
12751) -> Result<Value, SidecarError> {
12752 let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.exec database id")?;
12753 let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.exec sql")?;
12754 let kernel_pid = process.kernel_pid;
12755 let database = sqlite_database_mut(process, database_id)?;
12756 let before = database.connection.total_changes();
12757 database
12758 .connection
12759 .execute_batch(sql)
12760 .map_err(sqlite_error)?;
12761 mark_sqlite_mutation(database, sql);
12762 sqlite_sync_database(kernel, kernel_pid, database)?;
12763 Ok(json!(database
12764 .connection
12765 .total_changes()
12766 .saturating_sub(before)))
12767}
12768
12769fn sqlite_query_database(
12770 process: &mut ActiveProcess,
12771 request: &JavascriptSyncRpcRequest,
12772) -> Result<Value, SidecarError> {
12773 let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.query database id")?;
12774 let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.query sql")?;
12775 let params = request.args.get(2);
12776 let options = request.args.get(3);
12777 let return_arrays = sqlite_option_bool(options, "returnArrays").unwrap_or(false);
12778 let read_bigints = sqlite_option_bool(options, "readBigInts").unwrap_or(false);
12779 let database = sqlite_database_mut(process, database_id)?;
12780 sqlite_query_rows(
12781 &mut database.connection,
12782 sql,
12783 params,
12784 return_arrays,
12785 read_bigints,
12786 true,
12787 false,
12788 )
12789}
12790
12791fn sqlite_prepare_statement(
12792 process: &mut ActiveProcess,
12793 request: &JavascriptSyncRpcRequest,
12794) -> Result<Value, SidecarError> {
12795 ensure_per_process_state_handle_capacity(process.sqlite_statements.len(), "sqlite statement")?;
12796 let database_id = javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.prepare database id")?;
12797 let sql = javascript_sync_rpc_arg_str(&request.args, 1, "sqlite.prepare sql")?;
12798 let _ = sqlite_database(process, database_id)?;
12799 process.next_sqlite_statement_id += 1;
12800 let statement_id = process.next_sqlite_statement_id;
12801 process.sqlite_statements.insert(
12802 statement_id,
12803 ActiveSqliteStatement {
12804 database_id,
12805 sql: sql.to_owned(),
12806 return_arrays: false,
12807 read_bigints: false,
12808 allow_bare_named_parameters: false,
12809 allow_unknown_named_parameters: false,
12810 },
12811 );
12812 Ok(json!(statement_id))
12813}
12814
12815fn sqlite_run_statement(
12816 kernel: &mut SidecarKernel,
12817 process: &mut ActiveProcess,
12818 request: &JavascriptSyncRpcRequest,
12819) -> Result<Value, SidecarError> {
12820 let statement_id =
12821 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.run statement id")?;
12822 let params = request.args.get(1);
12823 let statement_state = sqlite_statement(process, statement_id)?.clone();
12824 let kernel_pid = process.kernel_pid;
12825 let database = sqlite_database_mut(process, statement_state.database_id)?;
12826 let before = database.connection.total_changes();
12827 {
12828 let mut statement = database
12829 .connection
12830 .prepare(&statement_state.sql)
12831 .map_err(sqlite_error)?;
12832 bind_sqlite_parameters(
12833 &mut statement,
12834 params,
12835 statement_state.allow_bare_named_parameters,
12836 statement_state.allow_unknown_named_parameters,
12837 )?;
12838 statement.raw_execute().map_err(sqlite_error)?;
12839 }
12840 let changes = database.connection.total_changes().saturating_sub(before);
12841 let last_insert_rowid = database.connection.last_insert_rowid();
12842 mark_sqlite_mutation(database, &statement_state.sql);
12843 sqlite_sync_database(kernel, kernel_pid, database)?;
12844 let result = json!({
12845 "changes": changes,
12846 "lastInsertRowid": encode_sqlite_integer(last_insert_rowid, true),
12847 });
12848 Ok(result)
12849}
12850
12851fn sqlite_get_statement(
12852 process: &mut ActiveProcess,
12853 request: &JavascriptSyncRpcRequest,
12854) -> Result<Value, SidecarError> {
12855 let statement_id =
12856 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.get statement id")?;
12857 let params = request.args.get(1);
12858 let statement_state = sqlite_statement(process, statement_id)?.clone();
12859 let database = sqlite_database_mut(process, statement_state.database_id)?;
12860 let rows = sqlite_query_rows(
12861 &mut database.connection,
12862 &statement_state.sql,
12863 params,
12864 statement_state.return_arrays,
12865 statement_state.read_bigints,
12866 statement_state.allow_bare_named_parameters,
12867 statement_state.allow_unknown_named_parameters,
12868 )?;
12869 Ok(rows
12870 .as_array()
12871 .and_then(|rows| rows.first().cloned())
12872 .unwrap_or(Value::Null))
12873}
12874
12875fn sqlite_all_statement(
12876 process: &mut ActiveProcess,
12877 request: &JavascriptSyncRpcRequest,
12878) -> Result<Value, SidecarError> {
12879 let statement_id =
12880 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.all statement id")?;
12881 let params = request.args.get(1);
12882 let statement_state = sqlite_statement(process, statement_id)?.clone();
12883 let database = sqlite_database_mut(process, statement_state.database_id)?;
12884 sqlite_query_rows(
12885 &mut database.connection,
12886 &statement_state.sql,
12887 params,
12888 statement_state.return_arrays,
12889 statement_state.read_bigints,
12890 statement_state.allow_bare_named_parameters,
12891 statement_state.allow_unknown_named_parameters,
12892 )
12893}
12894
12895fn sqlite_statement_columns(
12896 process: &mut ActiveProcess,
12897 request: &JavascriptSyncRpcRequest,
12898) -> Result<Value, SidecarError> {
12899 let statement_id =
12900 javascript_sync_rpc_arg_u64(&request.args, 0, "sqlite.statement.columns statement id")?;
12901 let statement_state = sqlite_statement(process, statement_id)?.clone();
12902 let database = sqlite_database_mut(process, statement_state.database_id)?;
12903 let statement = database
12904 .connection
12905 .prepare(&statement_state.sql)
12906 .map_err(sqlite_error)?;
12907 Ok(Value::Array(
12908 statement
12909 .column_names()
12910 .iter()
12911 .map(|name| json!({ "name": name }))
12912 .collect(),
12913 ))
12914}
12915
12916fn sqlite_query_rows(
12917 connection: &mut SqliteConnection,
12918 sql: &str,
12919 params: Option<&Value>,
12920 return_arrays: bool,
12921 read_bigints: bool,
12922 allow_bare_named_parameters: bool,
12923 allow_unknown_named_parameters: bool,
12924) -> Result<Value, SidecarError> {
12925 let mut statement = connection.prepare(sql).map_err(sqlite_error)?;
12926 let column_names = statement
12927 .column_names()
12928 .iter()
12929 .map(|name| (*name).to_owned())
12930 .collect::<Vec<_>>();
12931 let column_count = statement.column_count();
12932 bind_sqlite_parameters(
12933 &mut statement,
12934 params,
12935 allow_bare_named_parameters,
12936 allow_unknown_named_parameters,
12937 )?;
12938 let mut rows = statement.raw_query();
12939 let mut encoded_rows = Vec::new();
12940 while let Some(row) = rows.next().map_err(sqlite_error)? {
12941 encoded_rows.push(encode_sqlite_row(
12942 row,
12943 &column_names,
12944 column_count,
12945 return_arrays,
12946 read_bigints,
12947 )?);
12948 }
12949 Ok(Value::Array(encoded_rows))
12950}
12951
12952fn encode_sqlite_row(
12953 row: &rusqlite::Row<'_>,
12954 column_names: &[String],
12955 column_count: usize,
12956 return_arrays: bool,
12957 read_bigints: bool,
12958) -> Result<Value, SidecarError> {
12959 if return_arrays {
12960 let mut values = Vec::with_capacity(column_count);
12961 for index in 0..column_count {
12962 values.push(encode_sqlite_value_ref(
12963 row.get_ref(index).map_err(sqlite_error)?,
12964 read_bigints,
12965 )?);
12966 }
12967 return Ok(Value::Array(values));
12968 }
12969
12970 let mut object = Map::with_capacity(column_count);
12971 for (index, name) in column_names.iter().enumerate() {
12972 object.insert(
12973 name.clone(),
12974 encode_sqlite_value_ref(row.get_ref(index).map_err(sqlite_error)?, read_bigints)?,
12975 );
12976 }
12977 Ok(Value::Object(object))
12978}
12979
12980fn encode_sqlite_value_ref(
12981 value: SqliteValueRef<'_>,
12982 read_bigints: bool,
12983) -> Result<Value, SidecarError> {
12984 Ok(match value {
12985 SqliteValueRef::Null => Value::Null,
12986 SqliteValueRef::Integer(number) => encode_sqlite_integer(number, read_bigints),
12987 SqliteValueRef::Real(number) => json!(number),
12988 SqliteValueRef::Text(text) => Value::String(String::from_utf8_lossy(text).into_owned()),
12989 SqliteValueRef::Blob(bytes) => json!({
12990 "__agentosSqliteType": "uint8array",
12991 "value": base64::engine::general_purpose::STANDARD.encode(bytes),
12992 }),
12993 })
12994}
12995
12996fn encode_sqlite_integer(number: i64, read_bigints: bool) -> Value {
12997 if read_bigints || number.abs() > SQLITE_JS_SAFE_INTEGER_MAX {
12998 json!({
12999 "__agentosSqliteType": "bigint",
13000 "value": number.to_string(),
13001 })
13002 } else {
13003 json!(number)
13004 }
13005}
13006
13007fn bind_sqlite_parameters(
13008 statement: &mut SqliteStatement<'_>,
13009 params: Option<&Value>,
13010 allow_bare_named_parameters: bool,
13011 allow_unknown_named_parameters: bool,
13012) -> Result<(), SidecarError> {
13013 let Some(params) = params else {
13014 return Ok(());
13015 };
13016 match params {
13017 Value::Null => Ok(()),
13018 Value::Array(values) => {
13019 for (index, value) in values.iter().enumerate() {
13020 statement
13021 .raw_bind_parameter(index + 1, decode_sqlite_parameter(value)?)
13022 .map_err(sqlite_error)?;
13023 }
13024 Ok(())
13025 }
13026 Value::Object(map)
13027 if map
13028 .get("__agentosSqliteType")
13029 .and_then(Value::as_str)
13030 .is_none() =>
13031 {
13032 for (key, value) in map {
13033 let index =
13034 resolve_sqlite_parameter_index(statement, key, allow_bare_named_parameters)?;
13035 let Some(index) = index else {
13036 if allow_unknown_named_parameters {
13037 continue;
13038 }
13039 return Err(SidecarError::InvalidState(format!(
13040 "sqlite named parameter not found: {key}"
13041 )));
13042 };
13043 statement
13044 .raw_bind_parameter(index, decode_sqlite_parameter(value)?)
13045 .map_err(sqlite_error)?;
13046 }
13047 Ok(())
13048 }
13049 other => statement
13050 .raw_bind_parameter(1, decode_sqlite_parameter(other)?)
13051 .map_err(sqlite_error),
13052 }
13053}
13054
13055fn resolve_sqlite_parameter_index(
13056 statement: &mut SqliteStatement<'_>,
13057 key: &str,
13058 allow_bare_named_parameters: bool,
13059) -> Result<Option<usize>, SidecarError> {
13060 let mut candidates = vec![key.to_owned()];
13061 if allow_bare_named_parameters
13062 && !key.starts_with(':')
13063 && !key.starts_with('@')
13064 && !key.starts_with('$')
13065 {
13066 candidates.push(format!(":{key}"));
13067 candidates.push(format!("@{key}"));
13068 candidates.push(format!("${key}"));
13069 }
13070 for candidate in candidates {
13071 if let Some(index) = statement
13072 .parameter_index(&candidate)
13073 .map_err(sqlite_error)?
13074 {
13075 return Ok(Some(index));
13076 }
13077 }
13078 Ok(None)
13079}
13080
13081fn decode_sqlite_parameter(value: &Value) -> Result<rusqlite::types::Value, SidecarError> {
13082 Ok(match value {
13083 Value::Null => rusqlite::types::Value::Null,
13084 Value::Bool(value) => rusqlite::types::Value::Integer(i64::from(*value)),
13085 Value::Number(value) => match (value.as_i64(), value.as_f64()) {
13086 (Some(integer), _) => rusqlite::types::Value::Integer(integer),
13087 (_, Some(real)) => rusqlite::types::Value::Real(real),
13088 _ => {
13089 return Err(SidecarError::InvalidState(String::from(
13090 "sqlite parameter number is not representable",
13091 )));
13092 }
13093 },
13094 Value::String(value) => rusqlite::types::Value::Text(value.clone()),
13095 Value::Array(_) => {
13096 return Err(SidecarError::InvalidState(String::from(
13097 "sqlite parameters do not support nested arrays",
13098 )));
13099 }
13100 Value::Object(map) => match map.get("__agentosSqliteType").and_then(Value::as_str) {
13101 Some("bigint") => rusqlite::types::Value::Integer(
13102 map.get("value")
13103 .and_then(Value::as_str)
13104 .ok_or_else(|| {
13105 SidecarError::InvalidState(String::from(
13106 "sqlite bigint parameter missing string value",
13107 ))
13108 })?
13109 .parse::<i64>()
13110 .map_err(|error| {
13111 SidecarError::InvalidState(format!(
13112 "sqlite bigint parameter is not a signed 64-bit integer: {error}"
13113 ))
13114 })?,
13115 ),
13116 Some("uint8array") => rusqlite::types::Value::Blob(
13117 base64::engine::general_purpose::STANDARD
13118 .decode(map.get("value").and_then(Value::as_str).ok_or_else(|| {
13119 SidecarError::InvalidState(String::from(
13120 "sqlite blob parameter missing base64 value",
13121 ))
13122 })?)
13123 .map_err(|error| {
13124 SidecarError::InvalidState(format!(
13125 "sqlite blob parameter contains invalid base64: {error}"
13126 ))
13127 })?,
13128 ),
13129 Some(other) => {
13130 return Err(SidecarError::InvalidState(format!(
13131 "unsupported sqlite tagged parameter type {other}"
13132 )));
13133 }
13134 None => {
13135 return Err(SidecarError::InvalidState(String::from(
13136 "sqlite named parameter objects must be passed as the top-level params object",
13137 )));
13138 }
13139 },
13140 })
13141}
13142
13143fn close_sqlite_database(
13144 kernel: &mut SidecarKernel,
13145 process: &mut ActiveProcess,
13146 database_id: u64,
13147) -> Result<(), SidecarError> {
13148 let mut database = process
13149 .sqlite_databases
13150 .remove(&database_id)
13151 .ok_or_else(|| {
13152 SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
13153 })?;
13154 process
13155 .sqlite_statements
13156 .retain(|_, statement| statement.database_id != database_id);
13157 sqlite_sync_database(kernel, process.kernel_pid, &mut database)?;
13158 let host_path = database.host_path.clone();
13159 drop(database);
13160 cleanup_sqlite_host_artifacts(host_path.as_deref())?;
13161 Ok(())
13162}
13163
13164fn ensure_per_process_state_handle_capacity(len: usize, label: &str) -> Result<(), SidecarError> {
13165 if len >= MAX_PER_PROCESS_STATE_HANDLES {
13166 return Err(SidecarError::InvalidState(format!(
13167 "{label} handle limit exceeded: limit is {MAX_PER_PROCESS_STATE_HANDLES}"
13168 )));
13169 }
13170 Ok(())
13171}
13172
13173fn sqlite_sync_database(
13174 kernel: &mut SidecarKernel,
13175 kernel_pid: u32,
13176 database: &mut ActiveSqliteDatabase,
13177) -> Result<(), SidecarError> {
13178 if !database.dirty
13179 || database.transaction_depth > 0
13180 || database.read_only
13181 || database.host_path.is_none()
13182 || database.vm_path.is_none()
13183 {
13184 return Ok(());
13185 }
13186
13187 let _ = database
13188 .connection
13189 .execute_batch("PRAGMA wal_checkpoint(TRUNCATE)");
13190 let host_path = database.host_path.as_ref().expect("sqlite host path");
13191 if !host_path.exists() {
13192 return Ok(());
13193 }
13194 ensure_vm_parent_dir(
13195 kernel,
13196 kernel_pid,
13197 database.vm_path.as_deref().expect("sqlite vm path"),
13198 )?;
13199 let contents = fs::read(host_path).map_err(|error| {
13200 SidecarError::Io(format!(
13201 "failed to read sqlite temp database {}: {error}",
13202 host_path.display()
13203 ))
13204 })?;
13205 kernel
13206 .write_file_for_process(
13207 EXECUTION_DRIVER_NAME,
13208 kernel_pid,
13209 database.vm_path.as_deref().expect("sqlite vm path"),
13210 contents,
13211 None,
13212 )
13213 .map_err(kernel_error)?;
13214 database.dirty = false;
13215 Ok(())
13216}
13217
13218fn cleanup_sqlite_host_artifacts(host_path: Option<&Path>) -> Result<(), SidecarError> {
13219 let Some(host_path) = host_path else {
13220 return Ok(());
13221 };
13222 let parent = host_path.parent().map(PathBuf::from);
13223 for suffix in ["", "-wal", "-shm"] {
13224 let path = PathBuf::from(format!("{}{}", host_path.display(), suffix));
13225 if path.exists() {
13226 fs::remove_file(&path).map_err(|error| {
13227 SidecarError::Io(format!(
13228 "failed to remove sqlite temp artifact {}: {error}",
13229 path.display()
13230 ))
13231 })?;
13232 }
13233 }
13234 if let Some(parent) = parent {
13235 let _ = fs::remove_dir_all(parent);
13236 }
13237 Ok(())
13238}
13239
13240fn ensure_vm_parent_dir(
13241 kernel: &mut SidecarKernel,
13242 kernel_pid: u32,
13243 path: &str,
13244) -> Result<(), SidecarError> {
13245 let parent = dirname(path);
13246 if parent == "/" || parent == "." {
13247 return Ok(());
13248 }
13249 let mut current = String::new();
13250 for segment in parent.split('/').filter(|segment| !segment.is_empty()) {
13251 current.push('/');
13252 current.push_str(segment);
13253 if !kernel
13254 .exists_for_process(EXECUTION_DRIVER_NAME, kernel_pid, ¤t)
13255 .map_err(kernel_error)?
13256 {
13257 kernel
13258 .mkdir_for_process(EXECUTION_DRIVER_NAME, kernel_pid, ¤t, false, None)
13259 .map_err(kernel_error)?;
13260 }
13261 }
13262 Ok(())
13263}
13264
13265fn sqlite_database(
13266 process: &ActiveProcess,
13267 database_id: u64,
13268) -> Result<&ActiveSqliteDatabase, SidecarError> {
13269 process.sqlite_databases.get(&database_id).ok_or_else(|| {
13270 SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
13271 })
13272}
13273
13274fn sqlite_database_mut(
13275 process: &mut ActiveProcess,
13276 database_id: u64,
13277) -> Result<&mut ActiveSqliteDatabase, SidecarError> {
13278 process
13279 .sqlite_databases
13280 .get_mut(&database_id)
13281 .ok_or_else(|| {
13282 SidecarError::InvalidState(format!("sqlite database handle not found: {database_id}"))
13283 })
13284}
13285
13286fn sqlite_statement(
13287 process: &ActiveProcess,
13288 statement_id: u64,
13289) -> Result<&ActiveSqliteStatement, SidecarError> {
13290 process.sqlite_statements.get(&statement_id).ok_or_else(|| {
13291 SidecarError::InvalidState(format!("sqlite statement handle not found: {statement_id}"))
13292 })
13293}
13294
13295fn sqlite_statement_mut(
13296 process: &mut ActiveProcess,
13297 statement_id: u64,
13298) -> Result<&mut ActiveSqliteStatement, SidecarError> {
13299 process
13300 .sqlite_statements
13301 .get_mut(&statement_id)
13302 .ok_or_else(|| {
13303 SidecarError::InvalidState(format!("sqlite statement handle not found: {statement_id}"))
13304 })
13305}
13306
13307fn mark_sqlite_mutation(database: &mut ActiveSqliteDatabase, sql: &str) {
13308 let normalized = sql.trim_start().to_ascii_lowercase();
13309 if normalized.starts_with("begin") || normalized.starts_with("savepoint") {
13310 database.dirty = true;
13311 database.transaction_depth += 1;
13312 return;
13313 }
13314 if normalized.starts_with("commit") || normalized.starts_with("release savepoint") {
13315 database.dirty = true;
13316 database.transaction_depth = database.transaction_depth.saturating_sub(1);
13317 return;
13318 }
13319 if normalized.starts_with("rollback") && !normalized.starts_with("rollback to") {
13320 database.dirty = true;
13321 database.transaction_depth = database.transaction_depth.saturating_sub(1);
13322 return;
13323 }
13324 if normalized.starts_with("insert")
13325 || normalized.starts_with("update")
13326 || normalized.starts_with("delete")
13327 || normalized.starts_with("replace")
13328 || normalized.starts_with("create")
13329 || normalized.starts_with("alter")
13330 || normalized.starts_with("drop")
13331 || normalized.starts_with("vacuum")
13332 || normalized.starts_with("reindex")
13333 || normalized.starts_with("analyze")
13334 || normalized.starts_with("attach")
13335 || normalized.starts_with("detach")
13336 || normalized.starts_with("pragma")
13337 {
13338 database.dirty = true;
13339 }
13340}
13341
13342fn sqlite_option_bool(options: Option<&Value>, key: &str) -> Option<bool> {
13343 options
13344 .and_then(|value| value.get(key))
13345 .and_then(Value::as_bool)
13346}
13347
13348fn sqlite_option_u64(options: Option<&Value>, key: &str) -> Option<u64> {
13349 options
13350 .and_then(|value| value.get(key))
13351 .and_then(Value::as_u64)
13352}
13353
13354fn sqlite_error(error: rusqlite::Error) -> SidecarError {
13355 SidecarError::InvalidState(format!("sqlite error: {error}"))
13356}
13357
13358pub(crate) fn javascript_sync_rpc_arg_str<'a>(
13359 args: &'a [Value],
13360 index: usize,
13361 label: &str,
13362) -> Result<&'a str, SidecarError> {
13363 args.get(index)
13364 .and_then(Value::as_str)
13365 .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a string argument")))
13366}
13367
13368pub(crate) fn javascript_sync_rpc_arg_bool(
13369 args: &[Value],
13370 index: usize,
13371 label: &str,
13372) -> Result<bool, SidecarError> {
13373 args.get(index)
13374 .and_then(Value::as_bool)
13375 .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a boolean argument")))
13376}
13377
13378pub(crate) fn javascript_sync_rpc_encoding(args: &[Value]) -> Option<String> {
13379 args.get(1).and_then(|value| {
13380 value.as_str().map(str::to_owned).or_else(|| {
13381 value
13382 .get("encoding")
13383 .and_then(Value::as_str)
13384 .map(str::to_owned)
13385 })
13386 })
13387}
13388
13389pub(crate) fn javascript_sync_rpc_option_bool(
13390 args: &[Value],
13391 index: usize,
13392 key: &str,
13393) -> Option<bool> {
13394 let value = args.get(index)?;
13395 if key == "recursive" {
13396 if let Some(boolean) = value.as_bool() {
13397 return Some(boolean);
13398 }
13399 }
13400 value.get(key).and_then(Value::as_bool)
13401}
13402
13403pub(crate) fn javascript_sync_rpc_option_u32(
13404 args: &[Value],
13405 index: usize,
13406 key: &str,
13407) -> Result<Option<u32>, SidecarError> {
13408 let Some(value) = args.get(index).and_then(|value| {
13409 if value.is_object() {
13410 value.get(key)
13411 } else if key == "mode" && value.is_number() {
13412 Some(value)
13413 } else {
13414 None
13415 }
13416 }) else {
13417 return Ok(None);
13418 };
13419 if value.is_null() {
13420 return Ok(None);
13421 }
13422
13423 let numeric = value
13424 .as_u64()
13425 .or_else(|| {
13426 value
13427 .as_f64()
13428 .filter(|number| number.is_finite() && *number >= 0.0)
13429 .map(|number| number as u64)
13430 })
13431 .ok_or_else(|| SidecarError::InvalidState(format!("{key} must be numeric")))?;
13432
13433 u32::try_from(numeric)
13434 .map(Some)
13435 .map_err(|_| SidecarError::InvalidState(format!("{key} must fit within u32")))
13436}
13437
13438pub(crate) fn javascript_sync_rpc_arg_u32(
13439 args: &[Value],
13440 index: usize,
13441 label: &str,
13442) -> Result<u32, SidecarError> {
13443 let value = javascript_sync_rpc_arg_u64(args, index, label)?;
13444 u32::try_from(value)
13445 .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")))
13446}
13447
13448pub(crate) fn javascript_sync_rpc_arg_i32(
13449 args: &[Value],
13450 index: usize,
13451 label: &str,
13452) -> Result<i32, SidecarError> {
13453 let Some(value) = args.get(index) else {
13454 return Err(SidecarError::InvalidState(format!("{label} is required")));
13455 };
13456
13457 let numeric = value
13458 .as_i64()
13459 .or_else(|| {
13460 value
13461 .as_f64()
13462 .filter(|number| number.is_finite())
13463 .map(|number| number as i64)
13464 })
13465 .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a numeric argument")))?;
13466
13467 i32::try_from(numeric)
13468 .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within i32")))
13469}
13470
13471pub(crate) fn javascript_sync_rpc_arg_u32_optional(
13472 args: &[Value],
13473 index: usize,
13474 label: &str,
13475) -> Result<Option<u32>, SidecarError> {
13476 javascript_sync_rpc_arg_u64_optional(args, index, label)?
13477 .map(|value| {
13478 u32::try_from(value)
13479 .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")))
13480 })
13481 .transpose()
13482}
13483
13484pub(crate) fn javascript_sync_rpc_arg_u64(
13485 args: &[Value],
13486 index: usize,
13487 label: &str,
13488) -> Result<u64, SidecarError> {
13489 let Some(value) = args.get(index) else {
13490 return Err(SidecarError::InvalidState(format!("{label} is required")));
13491 };
13492
13493 value
13494 .as_u64()
13495 .or_else(|| {
13496 value
13497 .as_f64()
13498 .filter(|number| number.is_finite() && *number >= 0.0)
13499 .map(|number| number as u64)
13500 })
13501 .ok_or_else(|| SidecarError::InvalidState(format!("{label} must be a numeric argument")))
13502}
13503
13504pub(crate) fn javascript_sync_rpc_arg_u64_optional(
13505 args: &[Value],
13506 index: usize,
13507 label: &str,
13508) -> Result<Option<u64>, SidecarError> {
13509 let Some(value) = args.get(index) else {
13510 return Ok(None);
13511 };
13512 if value.is_null() {
13513 return Ok(None);
13514 }
13515 javascript_sync_rpc_arg_u64(args, index, label).map(Some)
13516}
13517
13518pub(crate) fn javascript_sync_rpc_bytes_arg(
13519 args: &[Value],
13520 index: usize,
13521 label: &str,
13522) -> Result<Vec<u8>, SidecarError> {
13523 let Some(value) = args.get(index) else {
13524 return Err(SidecarError::InvalidState(format!("{label} is required")));
13525 };
13526
13527 if let Some(text) = value.as_str() {
13528 return Ok(text.as_bytes().to_vec());
13529 }
13530
13531 let Some(base64_value) = value
13532 .get("__agentOSType")
13533 .and_then(Value::as_str)
13534 .filter(|kind| *kind == "bytes")
13535 .and_then(|_| value.get("base64"))
13536 .and_then(Value::as_str)
13537 else {
13538 return Err(SidecarError::InvalidState(format!(
13539 "{label} must be a string or encoded bytes payload"
13540 )));
13541 };
13542
13543 base64::engine::general_purpose::STANDARD
13544 .decode(base64_value)
13545 .map_err(|error| {
13546 SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
13547 })
13548}
13549
13550pub(crate) fn javascript_sync_rpc_bytes_value(bytes: &[u8]) -> Value {
13551 json!({
13552 "__agentOSType": "bytes",
13553 "base64": base64::engine::general_purpose::STANDARD.encode(bytes),
13554 })
13555}
13556
13557#[derive(Debug, Deserialize)]
13558struct KernelPollFdRequest {
13559 fd: u32,
13560 events: u16,
13561}
13562
13563#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
13564struct KernelPollFdResponse {
13565 fd: u32,
13566 events: u16,
13567 revents: u16,
13568}
13569
13570fn javascript_sync_rpc_base64_arg(
13571 args: &[Value],
13572 index: usize,
13573 label: &str,
13574) -> Result<Vec<u8>, SidecarError> {
13575 let value = javascript_sync_rpc_arg_str(args, index, label)?;
13576 base64::engine::general_purpose::STANDARD
13577 .decode(value)
13578 .map_err(|error| {
13579 SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
13580 })
13581}
13582
13583static SYNC_RPC_STATS: std::sync::OnceLock<
13590 std::sync::Mutex<std::collections::BTreeMap<String, u64>>,
13591> = std::sync::OnceLock::new();
13592
13593fn sync_rpc_trace_enabled() -> bool {
13594 std::env::var("AGENTOS_SYNC_RPC_TRACE").as_deref() == Ok("1")
13595}
13596
13597fn record_sync_rpc(method: &str) {
13598 let stats =
13599 SYNC_RPC_STATS.get_or_init(|| std::sync::Mutex::new(std::collections::BTreeMap::new()));
13600 let Ok(mut map) = stats.lock() else {
13601 return;
13602 };
13603 *map.entry(method.to_string()).or_insert(0) += 1;
13604 let total: u64 = map.values().sum();
13605 if total == 1 || total.is_multiple_of(50) {
13606 let mut top: Vec<(&String, &u64)> = map.iter().collect();
13607 top.sort_by(|a, b| b.1.cmp(a.1));
13608 let breakdown = top
13609 .iter()
13610 .take(8)
13611 .map(|(m, c)| format!("{m}={c}"))
13612 .collect::<Vec<_>>()
13613 .join(" ");
13614 tracing::info!(target: "secure_exec_sidecar::perf", total, %breakdown, "sync_rpc count");
13615 }
13616}
13617
13618pub(crate) fn service_javascript_sync_rpc<B>(
13619 request: JavascriptSyncRpcServiceRequest<'_, B>,
13620) -> Result<Value, SidecarError>
13621where
13622 B: NativeSidecarBridge + Send + 'static,
13623 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
13624{
13625 if sync_rpc_trace_enabled() {
13626 record_sync_rpc(request.sync_request.method.as_str());
13627 }
13628 let JavascriptSyncRpcServiceRequest {
13629 bridge,
13630 vm_id,
13631 dns,
13632 socket_paths,
13633 kernel,
13634 process,
13635 sync_request: request,
13636 resource_limits,
13637 network_counts,
13638 } = request;
13639 match request.method.as_str() {
13640 "_resolveModule"
13643 | "_resolveModuleSync"
13644 | "__resolve_module"
13645 | "_batchResolveModules"
13646 | "__batch_resolve_modules"
13647 | "_loadFile"
13648 | "_loadFileSync"
13649 | "__load_file"
13650 | "_moduleFormat"
13651 | "__module_format" => service_javascript_module_sync_rpc(kernel, process, request),
13652 "_loadPolyfill" | "__load_polyfill" => {
13654 service_javascript_internal_bridge_sync_rpc(process, request)
13655 }
13656 "__kernel_stdin_read" => match &process.execution {
13657 ActiveExecution::Javascript(execution) => execution
13658 .read_kernel_stdin_sync_rpc(request)
13659 .map_err(|error| SidecarError::Execution(error.to_string())),
13660 ActiveExecution::Python(_) | ActiveExecution::Wasm(_) | ActiveExecution::Tool(_) => {
13661 service_javascript_kernel_stdin_sync_rpc(kernel, process, request)
13662 }
13663 },
13664 "__kernel_stdio_write" => {
13665 service_javascript_kernel_stdio_write_sync_rpc(kernel, process, request)
13666 }
13667 "__kernel_poll" => service_javascript_kernel_poll_sync_rpc(kernel, process, request),
13668 "__pty_set_raw_mode" => {
13669 service_javascript_pty_set_raw_mode_sync_rpc(kernel, process, request)
13670 }
13671 "crypto.hashDigest"
13672 | "crypto.hmacDigest"
13673 | "crypto.pbkdf2"
13674 | "crypto.scrypt"
13675 | "crypto.cipheriv"
13676 | "crypto.decipheriv"
13677 | "crypto.cipherivCreate"
13678 | "crypto.cipherivUpdate"
13679 | "crypto.cipherivFinal"
13680 | "crypto.sign"
13681 | "crypto.verify"
13682 | "crypto.asymmetricOp"
13683 | "crypto.createKeyObject"
13684 | "crypto.generateKeyPairSync"
13685 | "crypto.generateKeySync"
13686 | "crypto.generatePrimeSync"
13687 | "crypto.diffieHellman"
13688 | "crypto.diffieHellmanGroup"
13689 | "crypto.diffieHellmanSessionCreate"
13690 | "crypto.diffieHellmanSessionCall"
13691 | "crypto.diffieHellmanSessionDestroy"
13692 | "crypto.subtle" => service_javascript_crypto_sync_rpc(process, request),
13693 "dns.lookup" | "dns.resolve" | "dns.resolve4" | "dns.resolve6" => {
13694 service_javascript_dns_sync_rpc(bridge, kernel, vm_id, dns, request)
13695 }
13696 "net.http_listen" | "net.http_close" | "net.http_wait" | "net.http_respond" => {
13697 service_javascript_net_sync_rpc(JavascriptNetSyncRpcServiceRequest {
13698 bridge,
13699 vm_id,
13700 dns,
13701 socket_paths,
13702 kernel,
13703 process,
13704 sync_request: request,
13705 resource_limits,
13706 network_counts,
13707 })
13708 }
13709 "net.http2_server_listen"
13710 | "net.http2_server_poll"
13711 | "net.http2_server_close"
13712 | "net.http2_server_respond"
13713 | "net.http2_server_wait"
13714 | "net.http2_session_connect"
13715 | "net.http2_session_request"
13716 | "net.http2_session_settings"
13717 | "net.http2_session_set_local_window_size"
13718 | "net.http2_session_goaway"
13719 | "net.http2_session_close"
13720 | "net.http2_session_destroy"
13721 | "net.http2_session_poll"
13722 | "net.http2_session_wait"
13723 | "net.http2_stream_respond"
13724 | "net.http2_stream_push_stream"
13725 | "net.http2_stream_write"
13726 | "net.http2_stream_end"
13727 | "net.http2_stream_close"
13728 | "net.http2_stream_pause"
13729 | "net.http2_stream_resume"
13730 | "net.http2_stream_respond_with_file" => {
13731 service_javascript_http2_sync_rpc(JavascriptHttp2SyncRpcServiceRequest {
13732 bridge,
13733 kernel,
13734 vm_id,
13735 dns,
13736 socket_paths,
13737 process,
13738 sync_request: request,
13739 resource_limits,
13740 network_counts,
13741 })
13742 }
13743 "net.connect"
13744 | "net.reserve_tcp_port"
13745 | "net.release_tcp_port"
13746 | "net.listen"
13747 | "net.poll"
13748 | "net.socket_wait_connect"
13749 | "net.socket_read"
13750 | "net.socket_set_no_delay"
13751 | "net.socket_set_keep_alive"
13752 | "net.socket_upgrade_tls"
13753 | "net.socket_get_tls_client_hello"
13754 | "net.socket_tls_query"
13755 | "net.server_poll"
13756 | "net.server_accept"
13757 | "net.server_connections"
13758 | "net.upgrade_socket_write"
13759 | "net.upgrade_socket_end"
13760 | "net.upgrade_socket_destroy"
13761 | "net.write"
13762 | "net.shutdown"
13763 | "net.destroy"
13764 | "net.server_close"
13765 | "tls.get_ciphers" => {
13766 service_javascript_net_sync_rpc(JavascriptNetSyncRpcServiceRequest {
13767 bridge,
13768 vm_id,
13769 dns,
13770 socket_paths,
13771 kernel,
13772 process,
13773 sync_request: request,
13774 resource_limits,
13775 network_counts,
13776 })
13777 }
13778 "dgram.createSocket"
13779 | "dgram.bind"
13780 | "dgram.send"
13781 | "dgram.poll"
13782 | "dgram.close"
13783 | "dgram.address"
13784 | "dgram.setBufferSize"
13785 | "dgram.getBufferSize" => {
13786 service_javascript_dgram_sync_rpc(JavascriptDgramSyncRpcServiceRequest {
13787 bridge,
13788 kernel,
13789 vm_id,
13790 dns,
13791 socket_paths,
13792 process,
13793 sync_request: request,
13794 resource_limits,
13795 network_counts,
13796 })
13797 }
13798 "sqlite.constants"
13799 | "sqlite.open"
13800 | "sqlite.close"
13801 | "sqlite.exec"
13802 | "sqlite.query"
13803 | "sqlite.prepare"
13804 | "sqlite.location"
13805 | "sqlite.checkpoint"
13806 | "sqlite.statement.run"
13807 | "sqlite.statement.get"
13808 | "sqlite.statement.all"
13809 | "sqlite.statement.iterate"
13810 | "sqlite.statement.columns"
13811 | "sqlite.statement.setReturnArrays"
13812 | "sqlite.statement.setReadBigInts"
13813 | "sqlite.statement.setAllowBareNamedParameters"
13814 | "sqlite.statement.setAllowUnknownNamedParameters"
13815 | "sqlite.statement.finalize" => {
13816 service_javascript_sqlite_sync_rpc(kernel, process, request)
13817 }
13818 "process.kill" => {
13819 let target_pid =
13820 javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?;
13821 let signal = javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?;
13822 let parsed_signal = parse_signal(signal)?;
13823 if parsed_signal == 0 {
13824 kernel
13825 .signal_process(EXECUTION_DRIVER_NAME, target_pid, parsed_signal)
13826 .map_err(kernel_error)?;
13827 return Ok(Value::Null);
13828 }
13829 let process_pid = i32::try_from(process.kernel_pid)
13830 .map_err(|_| SidecarError::InvalidState("process pid exceeds i32".into()))?;
13831 if target_pid != process_pid {
13832 return Err(SidecarError::InvalidState(format!(
13833 "unknown process pid {target_pid}"
13834 )));
13835 }
13836 process.pending_self_signal_exit = None;
13837 if parsed_signal != 0
13838 && !matches!(
13839 canonical_signal_name(parsed_signal),
13840 Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG")
13841 )
13842 {
13843 process.pending_self_signal_exit = Some(parsed_signal);
13844 }
13845 Ok(json!({
13846 "self": true,
13847 "action": "default",
13848 }))
13849 }
13850 "process.umask" => {
13851 let new_mask = javascript_sync_rpc_arg_u32_optional(&request.args, 0, "process umask")?;
13852 kernel
13853 .umask(EXECUTION_DRIVER_NAME, process.kernel_pid, new_mask)
13854 .map(|mask| json!(mask))
13855 .map_err(kernel_error)
13856 }
13857 "fs.chmodSync" | "fs.promises.chmod" => {
13858 let response =
13859 service_javascript_fs_sync_rpc(kernel, process, process.kernel_pid, request)?;
13860 mirror_process_chmod_to_host(process, request)?;
13861 Ok(response)
13862 }
13863 _ => service_javascript_fs_sync_rpc(kernel, process, process.kernel_pid, request),
13864 }
13865}
13866
13867fn service_javascript_internal_bridge_sync_rpc(
13868 process: &ActiveProcess,
13869 request: &JavascriptSyncRpcRequest,
13870) -> Result<Value, SidecarError> {
13871 let method = match request.method.as_str() {
13875 "_loadPolyfill" | "__load_polyfill" => "_loadPolyfill",
13876 other => {
13877 return Err(SidecarError::InvalidState(format!(
13878 "unsupported JavaScript internal bridge method {other}"
13879 )));
13880 }
13881 };
13882
13883 handle_internal_bridge_call_from_host_context(
13884 &process.host_cwd,
13885 &process.guest_cwd,
13886 &process.env,
13887 method,
13888 &request.args,
13889 )
13890 .ok_or_else(|| {
13891 SidecarError::InvalidState(format!(
13892 "JavaScript internal bridge method {method} returned no value"
13893 ))
13894 })
13895}
13896
13897fn mirror_process_chmod_to_host(
13898 process: &ActiveProcess,
13899 request: &JavascriptSyncRpcRequest,
13900) -> Result<(), SidecarError> {
13901 let guest_path = javascript_sync_rpc_arg_str(&request.args, 0, "filesystem chmod path")?;
13902 let mode = javascript_sync_rpc_arg_u32(&request.args, 1, "filesystem chmod mode")? & 0o7777;
13903 let Some(host_path) = resolve_process_guest_path_to_host(process, guest_path) else {
13904 return Ok(());
13905 };
13906 if !host_path.exists() {
13907 return Ok(());
13908 }
13909 fs::set_permissions(&host_path, fs::Permissions::from_mode(mode)).map_err(|error| {
13910 SidecarError::Io(format!(
13911 "failed to mirror chmod to host path {}: {error}",
13912 host_path.display()
13913 ))
13914 })
13915}
13916
13917fn resolve_process_guest_path_to_host(
13918 process: &ActiveProcess,
13919 guest_path: &str,
13920) -> Option<PathBuf> {
13921 let normalized_guest_path = if guest_path.starts_with('/') {
13922 normalize_path(guest_path)
13923 } else {
13924 normalize_path(&format!(
13925 "{}/{}",
13926 process.guest_cwd.trim_end_matches('/'),
13927 guest_path
13928 ))
13929 };
13930 if let Some(host_path) =
13931 host_path_from_runtime_guest_mappings(&process.env, &normalized_guest_path)
13932 {
13933 return Some(host_path);
13934 }
13935 let normalized_guest_cwd = normalize_path(&process.guest_cwd);
13936 let mut host_root = normalize_host_path(&process.host_cwd);
13937 for _ in normalized_guest_cwd
13938 .trim_start_matches('/')
13939 .split('/')
13940 .filter(|segment| !segment.is_empty())
13941 {
13942 host_root = host_root.parent()?.to_path_buf();
13943 }
13944 if normalized_guest_path == "/" {
13945 Some(host_root)
13946 } else {
13947 Some(host_root.join(normalized_guest_path.trim_start_matches('/')))
13948 }
13949}
13950
13951pub(crate) fn service_javascript_crypto_sync_rpc(
13952 process: &mut ActiveProcess,
13953 request: &JavascriptSyncRpcRequest,
13954) -> Result<Value, SidecarError> {
13955 match request.method.as_str() {
13956 "crypto.hashDigest" => {
13957 let algorithm = javascript_crypto_digest_algorithm(
13958 &request.args,
13959 0,
13960 "crypto.hashDigest algorithm",
13961 )?;
13962 let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.hashDigest data")?;
13963 Ok(Value::String(
13964 base64::engine::general_purpose::STANDARD.encode(algorithm.digest(&data)),
13965 ))
13966 }
13967 "crypto.hmacDigest" => {
13968 let algorithm = javascript_crypto_digest_algorithm(
13969 &request.args,
13970 0,
13971 "crypto.hmacDigest algorithm",
13972 )?;
13973 let key = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.hmacDigest key")?;
13974 let data = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.hmacDigest data")?;
13975 Ok(Value::String(
13976 base64::engine::general_purpose::STANDARD.encode(algorithm.hmac(&key, &data)?),
13977 ))
13978 }
13979 "crypto.pbkdf2" => {
13980 let password =
13981 javascript_sync_rpc_base64_arg(&request.args, 0, "crypto.pbkdf2 password")?;
13982 let salt = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.pbkdf2 salt")?;
13983 let iterations =
13984 javascript_sync_rpc_arg_u32(&request.args, 2, "crypto.pbkdf2 iterations")?;
13985 if iterations == 0 {
13986 return Err(SidecarError::InvalidState(String::from(
13987 "crypto.pbkdf2 iterations must be greater than zero",
13988 )));
13989 }
13990 let key_len = usize::try_from(javascript_sync_rpc_arg_u64(
13991 &request.args,
13992 3,
13993 "crypto.pbkdf2 key length",
13994 )?)
13995 .map_err(|_| {
13996 SidecarError::InvalidState(String::from(
13997 "crypto.pbkdf2 key length must fit within usize",
13998 ))
13999 })?;
14000 let algorithm =
14001 javascript_crypto_digest_algorithm(&request.args, 4, "crypto.pbkdf2 digest")?;
14002 let mut output = vec![0u8; key_len];
14003 algorithm.pbkdf2(&password, &salt, iterations, &mut output);
14004 Ok(Value::String(
14005 base64::engine::general_purpose::STANDARD.encode(output),
14006 ))
14007 }
14008 "crypto.scrypt" => {
14009 let password =
14010 javascript_sync_rpc_base64_arg(&request.args, 0, "crypto.scrypt password")?;
14011 let salt = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.scrypt salt")?;
14012 let key_len = usize::try_from(javascript_sync_rpc_arg_u64(
14013 &request.args,
14014 2,
14015 "crypto.scrypt key length",
14016 )?)
14017 .map_err(|_| {
14018 SidecarError::InvalidState(String::from(
14019 "crypto.scrypt key length must fit within usize",
14020 ))
14021 })?;
14022 let options_json =
14023 javascript_sync_rpc_arg_str(&request.args, 3, "crypto.scrypt options")?;
14024 let options: JavascriptScryptOptions =
14025 serde_json::from_str(options_json).map_err(|error| {
14026 SidecarError::InvalidState(format!(
14027 "crypto.scrypt options must be valid JSON: {error}"
14028 ))
14029 })?;
14030 let cost = options.cost.unwrap_or(DEFAULT_SCRYPT_COST);
14031 if cost == 0 || !cost.is_power_of_two() {
14032 return Err(SidecarError::InvalidState(String::from(
14033 "crypto.scrypt cost must be a positive power of two",
14034 )));
14035 }
14036 let log_n = u8::try_from(cost.ilog2()).map_err(|_| {
14037 SidecarError::InvalidState(String::from(
14038 "crypto.scrypt cost exceeds supported parameter range",
14039 ))
14040 })?;
14041 let params = ScryptParams::new(
14042 log_n,
14043 options.block_size.unwrap_or(DEFAULT_SCRYPT_BLOCK_SIZE),
14044 options
14045 .parallelization
14046 .unwrap_or(DEFAULT_SCRYPT_PARALLELIZATION),
14047 key_len,
14048 )
14049 .map_err(|error| {
14050 SidecarError::InvalidState(format!("crypto.scrypt options are invalid: {error}"))
14051 })?;
14052 let mut output = vec![0u8; key_len];
14053 scrypt(&password, &salt, ¶ms, &mut output).map_err(|error| {
14054 SidecarError::Execution(format!("crypto.scrypt failed: {error}"))
14055 })?;
14056 Ok(Value::String(
14057 base64::engine::general_purpose::STANDARD.encode(output),
14058 ))
14059 }
14060 "crypto.cipheriv" => service_javascript_crypto_cipheriv_sync_rpc(request),
14061 "crypto.decipheriv" => service_javascript_crypto_decipheriv_sync_rpc(request),
14062 "crypto.cipherivCreate" => {
14063 service_javascript_crypto_cipheriv_create_sync_rpc(process, request)
14064 }
14065 "crypto.cipherivUpdate" => {
14066 service_javascript_crypto_cipheriv_update_sync_rpc(process, request)
14067 }
14068 "crypto.cipherivFinal" => {
14069 service_javascript_crypto_cipheriv_final_sync_rpc(process, request)
14070 }
14071 "crypto.sign" => service_javascript_crypto_sign_sync_rpc(request),
14072 "crypto.verify" => service_javascript_crypto_verify_sync_rpc(request),
14073 "crypto.asymmetricOp" => service_javascript_crypto_asymmetric_op_sync_rpc(request),
14074 "crypto.createKeyObject" => service_javascript_crypto_create_key_object_sync_rpc(request),
14075 "crypto.generateKeyPairSync" => {
14076 service_javascript_crypto_generate_key_pair_sync_rpc(request)
14077 }
14078 "crypto.generateKeySync" => service_javascript_crypto_generate_key_sync_rpc(request),
14079 "crypto.generatePrimeSync" => service_javascript_crypto_generate_prime_sync_rpc(request),
14080 "crypto.diffieHellman" => service_javascript_crypto_diffie_hellman_sync_rpc(request),
14081 "crypto.diffieHellmanGroup" => {
14082 service_javascript_crypto_diffie_hellman_group_sync_rpc(request)
14083 }
14084 "crypto.diffieHellmanSessionCreate" => {
14085 service_javascript_crypto_diffie_hellman_session_create_sync_rpc(process, request)
14086 }
14087 "crypto.diffieHellmanSessionCall" => {
14088 service_javascript_crypto_diffie_hellman_session_call_sync_rpc(process, request)
14089 }
14090 "crypto.diffieHellmanSessionDestroy" => {
14091 service_javascript_crypto_diffie_hellman_session_destroy_sync_rpc(process, request)
14092 }
14093 "crypto.subtle" => service_javascript_crypto_subtle_sync_rpc(request),
14094 _ => Err(SidecarError::InvalidState(format!(
14095 "unsupported JavaScript crypto sync RPC method {}",
14096 request.method
14097 ))),
14098 }
14099}
14100
14101fn javascript_crypto_digest_algorithm(
14102 args: &[Value],
14103 index: usize,
14104 label: &str,
14105) -> Result<JavascriptCryptoDigestAlgorithm, SidecarError> {
14106 JavascriptCryptoDigestAlgorithm::parse(javascript_sync_rpc_arg_str(args, index, label)?)
14107}
14108
14109impl JavascriptCryptoDigestAlgorithm {
14110 fn parse(value: &str) -> Result<Self, SidecarError> {
14111 match value.trim().to_ascii_lowercase().replace('-', "").as_str() {
14112 "md5" => Ok(Self::Md5),
14113 "sha1" => Ok(Self::Sha1),
14114 "sha256" => Ok(Self::Sha256),
14115 "sha512" => Ok(Self::Sha512),
14116 _ => Err(SidecarError::InvalidState(format!(
14117 "unsupported crypto digest algorithm {value}"
14118 ))),
14119 }
14120 }
14121
14122 fn digest(self, data: &[u8]) -> Vec<u8> {
14123 match self {
14124 Self::Md5 => Md5::digest(data).to_vec(),
14125 Self::Sha1 => Sha1::digest(data).to_vec(),
14126 Self::Sha256 => Sha256::digest(data).to_vec(),
14127 Self::Sha512 => Sha512::digest(data).to_vec(),
14128 }
14129 }
14130
14131 fn hmac(self, key: &[u8], data: &[u8]) -> Result<Vec<u8>, SidecarError> {
14132 match self {
14133 Self::Md5 => {
14134 let mut mac = Hmac::<Md5>::new_from_slice(key).map_err(|error| {
14135 SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14136 })?;
14137 mac.update(data);
14138 Ok(mac.finalize().into_bytes().to_vec())
14139 }
14140 Self::Sha1 => {
14141 let mut mac = Hmac::<Sha1>::new_from_slice(key).map_err(|error| {
14142 SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14143 })?;
14144 mac.update(data);
14145 Ok(mac.finalize().into_bytes().to_vec())
14146 }
14147 Self::Sha256 => {
14148 let mut mac = Hmac::<Sha256>::new_from_slice(key).map_err(|error| {
14149 SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14150 })?;
14151 mac.update(data);
14152 Ok(mac.finalize().into_bytes().to_vec())
14153 }
14154 Self::Sha512 => {
14155 let mut mac = Hmac::<Sha512>::new_from_slice(key).map_err(|error| {
14156 SidecarError::InvalidState(format!("invalid HMAC key: {error}"))
14157 })?;
14158 mac.update(data);
14159 Ok(mac.finalize().into_bytes().to_vec())
14160 }
14161 }
14162 }
14163
14164 fn pbkdf2(self, password: &[u8], salt: &[u8], iterations: u32, output: &mut [u8]) {
14165 match self {
14166 Self::Md5 => pbkdf2_hmac::<Md5>(password, salt, iterations, output),
14167 Self::Sha1 => pbkdf2_hmac::<Sha1>(password, salt, iterations, output),
14168 Self::Sha256 => pbkdf2_hmac::<Sha256>(password, salt, iterations, output),
14169 Self::Sha512 => pbkdf2_hmac::<Sha512>(password, salt, iterations, output),
14170 }
14171 }
14172}
14173
14174#[derive(Debug, Clone)]
14175enum JavascriptCryptoKeyMaterial {
14176 Private(PKey<Private>),
14177 Public(PKey<Public>),
14178 Secret(Vec<u8>),
14179}
14180
14181#[derive(Debug, Clone, Deserialize, Serialize)]
14182struct JavascriptSerializedSandboxKeyObject {
14183 #[serde(rename = "type")]
14184 kind: String,
14185 #[serde(skip_serializing_if = "Option::is_none")]
14186 pem: Option<String>,
14187 #[serde(skip_serializing_if = "Option::is_none")]
14188 raw: Option<String>,
14189 #[serde(skip_serializing_if = "Option::is_none", rename = "asymmetricKeyType")]
14190 asymmetric_key_type: Option<String>,
14191 #[serde(
14192 skip_serializing_if = "Option::is_none",
14193 rename = "asymmetricKeyDetails"
14194 )]
14195 asymmetric_key_details: Option<Map<String, Value>>,
14196 #[serde(skip_serializing_if = "Option::is_none")]
14197 jwk: Option<Value>,
14198}
14199
14200#[derive(Debug, Clone)]
14201struct JavascriptDirectKeyInput {
14202 key: JavascriptCryptoKeyMaterial,
14203 padding: Option<Padding>,
14204}
14205
14206fn service_javascript_crypto_cipheriv_sync_rpc(
14207 request: &JavascriptSyncRpcRequest,
14208) -> Result<Value, SidecarError> {
14209 service_javascript_crypto_cipheriv_inner(request, false)
14210}
14211
14212fn service_javascript_crypto_decipheriv_sync_rpc(
14213 request: &JavascriptSyncRpcRequest,
14214) -> Result<Value, SidecarError> {
14215 service_javascript_crypto_cipheriv_inner(request, true)
14216}
14217
14218fn service_javascript_crypto_cipheriv_create_sync_rpc(
14219 process: &mut ActiveProcess,
14220 request: &JavascriptSyncRpcRequest,
14221) -> Result<Value, SidecarError> {
14222 ensure_per_process_state_handle_capacity(process.cipher_sessions.len(), "cipher session")?;
14223 let mode = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.cipherivCreate mode")?;
14224 let decrypt = mode == "decipher";
14225 let algorithm =
14226 javascript_sync_rpc_arg_str(&request.args, 1, "crypto.cipherivCreate algorithm")?;
14227 let key = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.cipherivCreate key")?;
14228 let iv = javascript_sync_rpc_base64_arg_optional(&request.args, 3, "crypto.cipherivCreate iv")?;
14229 let options =
14230 javascript_sync_rpc_json_arg_optional(&request.args, 4, "crypto.cipherivCreate options")?;
14231 let auth_tag_len = javascript_crypto_requested_aead_tag_len(algorithm, options.as_ref())?;
14232 let context = javascript_crypto_build_cipher_context(
14233 algorithm,
14234 &key,
14235 iv.as_deref(),
14236 decrypt,
14237 options.as_ref(),
14238 )?;
14239 process.next_cipher_session_id += 1;
14240 let session_id = process.next_cipher_session_id;
14241 process.cipher_sessions.insert(
14242 session_id,
14243 ActiveCipherSession {
14244 algorithm: algorithm.to_string(),
14245 auth_tag_len,
14246 context,
14247 },
14248 );
14249 Ok(json!(session_id))
14250}
14251
14252fn service_javascript_crypto_cipheriv_update_sync_rpc(
14253 process: &mut ActiveProcess,
14254 request: &JavascriptSyncRpcRequest,
14255) -> Result<Value, SidecarError> {
14256 let session_id =
14257 javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.cipherivUpdate session id")?;
14258 let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.cipherivUpdate data")?;
14259 let session = process
14260 .cipher_sessions
14261 .get_mut(&session_id)
14262 .ok_or_else(|| {
14263 SidecarError::InvalidState(format!("Cipher session {session_id} not found"))
14264 })?;
14265 let result = javascript_crypto_cipher_update(&mut session.context, &data)?;
14266 Ok(Value::String(
14267 base64::engine::general_purpose::STANDARD.encode(result),
14268 ))
14269}
14270
14271fn service_javascript_crypto_cipheriv_final_sync_rpc(
14272 process: &mut ActiveProcess,
14273 request: &JavascriptSyncRpcRequest,
14274) -> Result<Value, SidecarError> {
14275 let session_id =
14276 javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.cipherivFinal session id")?;
14277 let mut session = process.cipher_sessions.remove(&session_id).ok_or_else(|| {
14278 SidecarError::InvalidState(format!("Cipher session {session_id} not found"))
14279 })?;
14280 let data = javascript_crypto_cipher_finalize(&mut session.context)?;
14281 let mut response = Map::new();
14282 response.insert(
14283 String::from("data"),
14284 Value::String(base64::engine::general_purpose::STANDARD.encode(data)),
14285 );
14286 if javascript_crypto_is_aead(&session.algorithm) {
14287 let mut auth_tag = vec![0_u8; session.auth_tag_len];
14288 session
14289 .context
14290 .get_tag(&mut auth_tag)
14291 .map_err(javascript_crypto_openssl_error)?;
14292 response.insert(
14293 String::from("authTag"),
14294 Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
14295 );
14296 }
14297 Ok(Value::String(serde_json::to_string(&response).map_err(
14298 |error| SidecarError::InvalidState(format!("serialize cipher final response: {error}")),
14299 )?))
14300}
14301
14302fn service_javascript_crypto_sign_sync_rpc(
14303 request: &JavascriptSyncRpcRequest,
14304) -> Result<Value, SidecarError> {
14305 let algorithm = request.args.first().and_then(Value::as_str);
14306 let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.sign data")?;
14307 let key_json = javascript_sync_rpc_arg_str(&request.args, 2, "crypto.sign key")?;
14308 let key_input =
14309 javascript_crypto_parse_direct_key_input(key_json, Some("private"), "crypto.sign key")?;
14310 let private_key = javascript_crypto_expect_private_key(key_input.key, "crypto.sign key")?;
14311 let mut signer = javascript_crypto_new_signer(algorithm, &private_key)?;
14312 if let Some(padding) = key_input.padding {
14313 signer
14314 .set_rsa_padding(padding)
14315 .map_err(javascript_crypto_openssl_error)?;
14316 }
14317 signer
14318 .update(&data)
14319 .map_err(javascript_crypto_openssl_error)?;
14320 Ok(Value::String(
14321 base64::engine::general_purpose::STANDARD.encode(
14322 signer
14323 .sign_to_vec()
14324 .map_err(javascript_crypto_openssl_error)?,
14325 ),
14326 ))
14327}
14328
14329fn service_javascript_crypto_verify_sync_rpc(
14330 request: &JavascriptSyncRpcRequest,
14331) -> Result<Value, SidecarError> {
14332 let algorithm = request.args.first().and_then(Value::as_str);
14333 let data = javascript_sync_rpc_base64_arg(&request.args, 1, "crypto.verify data")?;
14334 let key_json = javascript_sync_rpc_arg_str(&request.args, 2, "crypto.verify key")?;
14335 let signature = javascript_sync_rpc_base64_arg(&request.args, 3, "crypto.verify signature")?;
14336 let key_input =
14337 javascript_crypto_parse_direct_key_input(key_json, Some("public"), "crypto.verify key")?;
14338 let public_key = javascript_crypto_expect_public_key(key_input.key, "crypto.verify key")?;
14339 let mut verifier = javascript_crypto_new_verifier(algorithm, &public_key)?;
14340 if let Some(padding) = key_input.padding {
14341 verifier
14342 .set_rsa_padding(padding)
14343 .map_err(javascript_crypto_openssl_error)?;
14344 }
14345 verifier
14346 .update(&data)
14347 .map_err(javascript_crypto_openssl_error)?;
14348 Ok(json!(verifier
14349 .verify(&signature)
14350 .map_err(javascript_crypto_openssl_error)?))
14351}
14352
14353fn service_javascript_crypto_asymmetric_op_sync_rpc(
14354 request: &JavascriptSyncRpcRequest,
14355) -> Result<Value, SidecarError> {
14356 let operation = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.asymmetricOp operation")?;
14357 let key_json = javascript_sync_rpc_arg_str(&request.args, 1, "crypto.asymmetricOp key")?;
14358 let data = javascript_sync_rpc_base64_arg(&request.args, 2, "crypto.asymmetricOp data")?;
14359 let expect_kind = match operation {
14360 "publicEncrypt" | "publicDecrypt" => Some("public"),
14361 "privateEncrypt" | "privateDecrypt" => Some("private"),
14362 other => {
14363 return Err(SidecarError::InvalidState(format!(
14364 "Unsupported asymmetric crypto operation: {other}"
14365 )));
14366 }
14367 };
14368 let key_input =
14369 javascript_crypto_parse_direct_key_input(key_json, expect_kind, "crypto.asymmetricOp key")?;
14370 let padding = key_input.padding.unwrap_or(Padding::PKCS1);
14371 let mut output = vec![0_u8; javascript_crypto_rsa_output_size(&key_input.key)?];
14372 let written = match (operation, key_input.key) {
14373 ("publicEncrypt", JavascriptCryptoKeyMaterial::Public(key))
14374 | ("publicDecrypt", JavascriptCryptoKeyMaterial::Public(key)) => {
14375 let rsa = key.rsa().map_err(javascript_crypto_openssl_error)?;
14376 if operation == "publicEncrypt" {
14377 rsa.public_encrypt(&data, &mut output, padding)
14378 .map_err(javascript_crypto_openssl_error)?
14379 } else {
14380 rsa.public_decrypt(&data, &mut output, padding)
14381 .map_err(javascript_crypto_openssl_error)?
14382 }
14383 }
14384 ("privateEncrypt", JavascriptCryptoKeyMaterial::Private(key))
14385 | ("privateDecrypt", JavascriptCryptoKeyMaterial::Private(key)) => {
14386 let rsa = key.rsa().map_err(javascript_crypto_openssl_error)?;
14387 if operation == "privateEncrypt" {
14388 rsa.private_encrypt(&data, &mut output, padding)
14389 .map_err(javascript_crypto_openssl_error)?
14390 } else {
14391 rsa.private_decrypt(&data, &mut output, padding)
14392 .map_err(javascript_crypto_openssl_error)?
14393 }
14394 }
14395 _ => {
14396 return Err(SidecarError::InvalidState(format!(
14397 "{operation} requires an RSA {} key",
14398 expect_kind.unwrap_or("asymmetric")
14399 )));
14400 }
14401 };
14402 output.truncate(written);
14403 Ok(Value::String(
14404 base64::engine::general_purpose::STANDARD.encode(output),
14405 ))
14406}
14407
14408fn service_javascript_crypto_create_key_object_sync_rpc(
14409 request: &JavascriptSyncRpcRequest,
14410) -> Result<Value, SidecarError> {
14411 let operation =
14412 javascript_sync_rpc_arg_str(&request.args, 0, "crypto.createKeyObject operation")?;
14413 let key_json = javascript_sync_rpc_arg_str(&request.args, 1, "crypto.createKeyObject key")?;
14414 let expected = match operation {
14415 "createPrivateKey" => Some("private"),
14416 "createPublicKey" => Some("public"),
14417 other => {
14418 return Err(SidecarError::InvalidState(format!(
14419 "Unsupported key creation operation: {other}"
14420 )));
14421 }
14422 };
14423 let key_input =
14424 javascript_crypto_parse_direct_key_input(key_json, expected, "crypto.createKeyObject key")?;
14425 Ok(Value::String(
14426 serde_json::to_string(&javascript_crypto_serialize_sandbox_key_object(
14427 &key_input.key,
14428 )?)
14429 .map_err(|error| {
14430 SidecarError::InvalidState(format!("serialize crypto key object: {error}"))
14431 })?,
14432 ))
14433}
14434
14435fn service_javascript_crypto_generate_key_pair_sync_rpc(
14436 request: &JavascriptSyncRpcRequest,
14437) -> Result<Value, SidecarError> {
14438 let key_type =
14439 javascript_sync_rpc_arg_str(&request.args, 0, "crypto.generateKeyPairSync type")?;
14440 let options = javascript_crypto_parse_serialized_options_arg(
14441 &request.args,
14442 1,
14443 "crypto.generateKeyPairSync options",
14444 )?
14445 .unwrap_or(Value::Object(Map::new()));
14446 let public_encoding = options.get("publicKeyEncoding").cloned();
14447 let private_encoding = options.get("privateKeyEncoding").cloned();
14448
14449 let private_key = match key_type {
14450 "rsa" => {
14451 let bits = options
14452 .get("modulusLength")
14453 .and_then(Value::as_u64)
14454 .unwrap_or(2048) as u32;
14455 let exponent = options
14456 .get("publicExponent")
14457 .map(|value| javascript_crypto_u32_from_bridge_value(value, "rsa publicExponent"))
14458 .transpose()?
14459 .unwrap_or(65_537);
14460 let exponent = BigNum::from_u32(exponent).map_err(javascript_crypto_openssl_error)?;
14461 let rsa =
14462 Rsa::generate_with_e(bits, &exponent).map_err(javascript_crypto_openssl_error)?;
14463 PKey::from_rsa(rsa).map_err(javascript_crypto_openssl_error)?
14464 }
14465 "ec" => {
14466 let named_curve = options
14467 .get("namedCurve")
14468 .and_then(Value::as_str)
14469 .ok_or_else(|| {
14470 SidecarError::InvalidState(String::from(
14471 "crypto.generateKeyPairSync ec requires namedCurve",
14472 ))
14473 })?;
14474 let group = EcGroup::from_curve_name(javascript_crypto_curve_nid(named_curve)?)
14475 .map_err(javascript_crypto_openssl_error)?;
14476 let key = EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?;
14477 PKey::from_ec_key(key).map_err(javascript_crypto_openssl_error)?
14478 }
14479 "ed25519" => PKey::generate_ed25519().map_err(javascript_crypto_openssl_error)?,
14480 "x25519" => PKey::generate_x25519().map_err(javascript_crypto_openssl_error)?,
14481 other => {
14482 return Err(SidecarError::InvalidState(format!(
14483 "unsupported crypto key pair type {other}"
14484 )));
14485 }
14486 };
14487 let public_key = PKey::public_key_from_pem(
14488 &private_key
14489 .public_key_to_pem()
14490 .map_err(javascript_crypto_openssl_error)?,
14491 )
14492 .map_err(javascript_crypto_openssl_error)?;
14493 let response = if public_encoding.is_some() || private_encoding.is_some() {
14494 json!({
14495 "publicKey": javascript_crypto_serialize_encoded_key_value_public(&public_key, public_encoding.as_ref())?,
14496 "privateKey": javascript_crypto_serialize_encoded_key_value_private(&private_key, private_encoding.as_ref())?,
14497 })
14498 } else {
14499 json!({
14500 "publicKey": javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Public(public_key))?,
14501 "privateKey": javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Private(private_key))?,
14502 })
14503 };
14504 Ok(Value::String(serde_json::to_string(&response).map_err(
14505 |error| SidecarError::InvalidState(format!("serialize generated key pair: {error}")),
14506 )?))
14507}
14508
14509fn service_javascript_crypto_generate_key_sync_rpc(
14510 request: &JavascriptSyncRpcRequest,
14511) -> Result<Value, SidecarError> {
14512 let key_type = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.generateKeySync type")?;
14513 let options = javascript_crypto_parse_serialized_options_arg(
14514 &request.args,
14515 1,
14516 "crypto.generateKeySync options",
14517 )?
14518 .unwrap_or(Value::Object(Map::new()));
14519 let bit_length = options
14520 .get("length")
14521 .and_then(Value::as_u64)
14522 .ok_or_else(|| {
14523 SidecarError::InvalidState(String::from(
14524 "crypto.generateKeySync options.length is required",
14525 ))
14526 })? as usize;
14527 let mut raw = vec![0_u8; bit_length.div_ceil(8)];
14528 rand_bytes(&mut raw).map_err(javascript_crypto_openssl_error)?;
14529 let serialized = match key_type {
14530 "hmac" => javascript_crypto_serialize_sandbox_key_object(
14531 &JavascriptCryptoKeyMaterial::Secret(raw),
14532 )?,
14533 "aes" => javascript_crypto_serialize_sandbox_key_object(
14534 &JavascriptCryptoKeyMaterial::Secret(raw),
14535 )?,
14536 other => {
14537 return Err(SidecarError::InvalidState(format!(
14538 "unsupported crypto.generateKeySync type {other}"
14539 )));
14540 }
14541 };
14542 Ok(Value::String(serde_json::to_string(&serialized).map_err(
14543 |error| SidecarError::InvalidState(format!("serialize generated key: {error}")),
14544 )?))
14545}
14546
14547fn service_javascript_crypto_generate_prime_sync_rpc(
14548 request: &JavascriptSyncRpcRequest,
14549) -> Result<Value, SidecarError> {
14550 let bits =
14551 javascript_sync_rpc_arg_u64(&request.args, 0, "crypto.generatePrimeSync size")? as i32;
14552 let options = javascript_crypto_parse_serialized_options_arg(
14553 &request.args,
14554 1,
14555 "crypto.generatePrimeSync options",
14556 )?
14557 .unwrap_or(Value::Object(Map::new()));
14558 let safe = options
14559 .get("safe")
14560 .and_then(Value::as_bool)
14561 .unwrap_or(false);
14562 let add = options
14563 .get("add")
14564 .map(|value| javascript_crypto_bignum_from_bridge_value(value, "prime add"))
14565 .transpose()?;
14566 let rem = options
14567 .get("rem")
14568 .map(|value| javascript_crypto_bignum_from_bridge_value(value, "prime rem"))
14569 .transpose()?;
14570 let mut prime = BigNum::new().map_err(javascript_crypto_openssl_error)?;
14571 prime
14572 .generate_prime(bits, safe, add.as_deref(), rem.as_deref())
14573 .map_err(javascript_crypto_openssl_error)?;
14574 let payload = if options
14575 .get("bigint")
14576 .and_then(Value::as_bool)
14577 .unwrap_or(false)
14578 {
14579 json!({
14580 "__type": "bigint",
14581 "value": prime.to_dec_str().map_err(javascript_crypto_openssl_error)?.to_string(),
14582 })
14583 } else {
14584 json!({
14585 "__type": "buffer",
14586 "value": base64::engine::general_purpose::STANDARD.encode(prime.to_vec()),
14587 })
14588 };
14589 Ok(Value::String(serde_json::to_string(&payload).map_err(
14590 |error| SidecarError::InvalidState(format!("serialize generated prime: {error}")),
14591 )?))
14592}
14593
14594fn service_javascript_crypto_diffie_hellman_sync_rpc(
14595 request: &JavascriptSyncRpcRequest,
14596) -> Result<Value, SidecarError> {
14597 let options = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.diffieHellman options")?;
14598 let parsed: Value = serde_json::from_str(options).map_err(|error| {
14599 SidecarError::InvalidState(format!(
14600 "crypto.diffieHellman options must be valid JSON: {error}"
14601 ))
14602 })?;
14603 let private_key = javascript_crypto_parse_key_material_value(
14604 parsed.get("privateKey").ok_or_else(|| {
14605 SidecarError::InvalidState(String::from("crypto.diffieHellman missing privateKey"))
14606 })?,
14607 Some("private"),
14608 "crypto.diffieHellman privateKey",
14609 )?;
14610 let public_key = javascript_crypto_parse_key_material_value(
14611 parsed.get("publicKey").ok_or_else(|| {
14612 SidecarError::InvalidState(String::from("crypto.diffieHellman missing publicKey"))
14613 })?,
14614 Some("public"),
14615 "crypto.diffieHellman publicKey",
14616 )?;
14617 let private_key =
14618 javascript_crypto_expect_private_key(private_key, "crypto.diffieHellman privateKey")?;
14619 let public_key =
14620 javascript_crypto_expect_public_key(public_key, "crypto.diffieHellman publicKey")?;
14621 let mut deriver = Deriver::new(&private_key).map_err(javascript_crypto_openssl_error)?;
14622 deriver
14623 .set_peer(&public_key)
14624 .map_err(javascript_crypto_openssl_error)?;
14625 let secret = deriver
14626 .derive_to_vec()
14627 .map_err(javascript_crypto_openssl_error)?;
14628 Ok(Value::String(
14629 serde_json::to_string(&json!({
14630 "__type": "buffer",
14631 "value": base64::engine::general_purpose::STANDARD.encode(secret),
14632 }))
14633 .map_err(|error| {
14634 SidecarError::InvalidState(format!("serialize derived secret: {error}"))
14635 })?,
14636 ))
14637}
14638
14639fn service_javascript_crypto_diffie_hellman_group_sync_rpc(
14640 request: &JavascriptSyncRpcRequest,
14641) -> Result<Value, SidecarError> {
14642 let name = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.diffieHellmanGroup name")?;
14643 let params = javascript_crypto_named_dh_group(name)?;
14644 let response = json!({
14645 "prime": {
14646 "__type": "buffer",
14647 "value": base64::engine::general_purpose::STANDARD.encode(params.prime_p().to_vec()),
14648 },
14649 "generator": {
14650 "__type": "buffer",
14651 "value": base64::engine::general_purpose::STANDARD.encode(params.generator().to_vec()),
14652 },
14653 });
14654 Ok(Value::String(serde_json::to_string(&response).map_err(
14655 |error| {
14656 SidecarError::InvalidState(format!("serialize diffieHellmanGroup response: {error}"))
14657 },
14658 )?))
14659}
14660
14661fn service_javascript_crypto_diffie_hellman_session_create_sync_rpc(
14662 process: &mut ActiveProcess,
14663 request: &JavascriptSyncRpcRequest,
14664) -> Result<Value, SidecarError> {
14665 ensure_per_process_state_handle_capacity(
14666 process.diffie_hellman_sessions.len(),
14667 "diffie-hellman session",
14668 )?;
14669 let raw = javascript_sync_rpc_arg_str(
14670 &request.args,
14671 0,
14672 "crypto.diffieHellmanSessionCreate request",
14673 )?;
14674 let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14675 SidecarError::InvalidState(format!(
14676 "crypto.diffieHellmanSessionCreate request must be valid JSON: {error}"
14677 ))
14678 })?;
14679 let session = match parsed.get("type").and_then(Value::as_str) {
14680 Some("group") => {
14681 let name = parsed.get("name").and_then(Value::as_str).ok_or_else(|| {
14682 SidecarError::InvalidState(String::from(
14683 "crypto.diffieHellmanSessionCreate group requires name",
14684 ))
14685 })?;
14686 ActiveDiffieHellmanSession::Dh(ActiveDhSession {
14687 params: javascript_crypto_named_dh_group(name)?,
14688 key_pair: None,
14689 })
14690 }
14691 Some("dh") => {
14692 let args = parsed
14693 .get("args")
14694 .and_then(Value::as_array)
14695 .ok_or_else(|| {
14696 SidecarError::InvalidState(String::from(
14697 "crypto.diffieHellmanSessionCreate dh requires args",
14698 ))
14699 })?;
14700 let params = javascript_crypto_build_dh_params(args)?;
14701 ActiveDiffieHellmanSession::Dh(ActiveDhSession {
14702 params,
14703 key_pair: None,
14704 })
14705 }
14706 Some("ecdh") => {
14707 let curve = parsed.get("name").and_then(Value::as_str).ok_or_else(|| {
14708 SidecarError::InvalidState(String::from(
14709 "crypto.diffieHellmanSessionCreate ecdh requires name",
14710 ))
14711 })?;
14712 ActiveDiffieHellmanSession::Ecdh(ActiveEcdhSession {
14713 curve: curve.to_string(),
14714 key_pair: None,
14715 })
14716 }
14717 other => {
14718 return Err(SidecarError::InvalidState(format!(
14719 "Unsupported Diffie-Hellman session type: {}",
14720 other.unwrap_or("<missing>")
14721 )));
14722 }
14723 };
14724 process.next_diffie_hellman_session_id += 1;
14725 let session_id = process.next_diffie_hellman_session_id;
14726 process.diffie_hellman_sessions.insert(session_id, session);
14727 Ok(json!(session_id))
14728}
14729
14730fn service_javascript_crypto_diffie_hellman_session_call_sync_rpc(
14731 process: &mut ActiveProcess,
14732 request: &JavascriptSyncRpcRequest,
14733) -> Result<Value, SidecarError> {
14734 let session_id = javascript_sync_rpc_arg_u64(
14735 &request.args,
14736 0,
14737 "crypto.diffieHellmanSessionCall session id",
14738 )?;
14739 let raw =
14740 javascript_sync_rpc_arg_str(&request.args, 1, "crypto.diffieHellmanSessionCall request")?;
14741 let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14742 SidecarError::InvalidState(format!(
14743 "crypto.diffieHellmanSessionCall request must be valid JSON: {error}"
14744 ))
14745 })?;
14746 let method = parsed
14747 .get("method")
14748 .and_then(Value::as_str)
14749 .ok_or_else(|| {
14750 SidecarError::InvalidState(String::from(
14751 "crypto.diffieHellmanSessionCall request missing method",
14752 ))
14753 })?;
14754 let args = parsed
14755 .get("args")
14756 .and_then(Value::as_array)
14757 .cloned()
14758 .unwrap_or_default();
14759 let session = process
14760 .diffie_hellman_sessions
14761 .get_mut(&session_id)
14762 .ok_or_else(|| {
14763 SidecarError::InvalidState(format!("Diffie-Hellman session {session_id} not found"))
14764 })?;
14765 let (result, has_result) = match session {
14766 ActiveDiffieHellmanSession::Dh(session) => {
14767 javascript_crypto_call_dh_session(session, method, &args)?
14768 }
14769 ActiveDiffieHellmanSession::Ecdh(session) => {
14770 javascript_crypto_call_ecdh_session(session, method, &args)?
14771 }
14772 };
14773 Ok(Value::String(
14774 serde_json::to_string(&json!({
14775 "result": result,
14776 "hasResult": has_result,
14777 }))
14778 .map_err(|error| {
14779 SidecarError::InvalidState(format!("serialize diffie session result: {error}"))
14780 })?,
14781 ))
14782}
14783
14784fn service_javascript_crypto_diffie_hellman_session_destroy_sync_rpc(
14785 process: &mut ActiveProcess,
14786 request: &JavascriptSyncRpcRequest,
14787) -> Result<Value, SidecarError> {
14788 let session_id = javascript_sync_rpc_arg_u64(
14789 &request.args,
14790 0,
14791 "crypto.diffieHellmanSessionDestroy session id",
14792 )?;
14793 process
14794 .diffie_hellman_sessions
14795 .remove(&session_id)
14796 .ok_or_else(|| {
14797 SidecarError::InvalidState(format!("Diffie-Hellman session {session_id} not found"))
14798 })?;
14799 Ok(Value::Null)
14800}
14801
14802fn service_javascript_crypto_subtle_sync_rpc(
14803 request: &JavascriptSyncRpcRequest,
14804) -> Result<Value, SidecarError> {
14805 let raw = javascript_sync_rpc_arg_str(&request.args, 0, "crypto.subtle request")?;
14806 let parsed: Value = serde_json::from_str(raw).map_err(|error| {
14807 SidecarError::InvalidState(format!("crypto.subtle request must be valid JSON: {error}"))
14808 })?;
14809 let op = parsed.get("op").and_then(Value::as_str).ok_or_else(|| {
14810 SidecarError::InvalidState(String::from("crypto.subtle request missing op"))
14811 })?;
14812 match op {
14813 "digest" => {
14814 let algorithm = parsed
14815 .get("algorithm")
14816 .and_then(Value::as_str)
14817 .ok_or_else(|| {
14818 SidecarError::InvalidState(String::from(
14819 "crypto.subtle.digest missing algorithm",
14820 ))
14821 })?;
14822 let data = parsed.get("data").and_then(Value::as_str).ok_or_else(|| {
14823 SidecarError::InvalidState(String::from("crypto.subtle.digest missing data"))
14824 })?;
14825 let bytes = base64::engine::general_purpose::STANDARD
14826 .decode(data)
14827 .map_err(|error| {
14828 SidecarError::InvalidState(format!("crypto.subtle.digest data base64: {error}"))
14829 })?;
14830 let digest = JavascriptCryptoDigestAlgorithm::parse(algorithm)?.digest(&bytes);
14831 Ok(Value::String(
14832 serde_json::to_string(&json!({
14833 "data": base64::engine::general_purpose::STANDARD.encode(digest),
14834 }))
14835 .map_err(|error| {
14836 SidecarError::InvalidState(format!("serialize crypto.subtle digest: {error}"))
14837 })?,
14838 ))
14839 }
14840 "generateKey" => {
14841 let algorithm = parsed.get("algorithm").ok_or_else(|| {
14842 SidecarError::InvalidState(String::from(
14843 "crypto.subtle.generateKey missing algorithm",
14844 ))
14845 })?;
14846 let name =
14847 javascript_crypto_subtle_algorithm_name(algorithm, "crypto.subtle.generateKey")?;
14848 if !matches!(name, "AES-GCM" | "AES-CBC" | "AES-CTR" | "AES-KW") {
14849 return Err(SidecarError::InvalidState(format!(
14850 "Unsupported key algorithm: {name}"
14851 )));
14852 }
14853 let length_bits = algorithm
14854 .get("length")
14855 .and_then(Value::as_u64)
14856 .ok_or_else(|| {
14857 SidecarError::InvalidState(String::from(
14858 "crypto.subtle.generateKey AES algorithm requires length",
14859 ))
14860 })?;
14861 if length_bits % 8 != 0 {
14862 return Err(SidecarError::InvalidState(String::from(
14863 "crypto.subtle.generateKey length must be byte-aligned",
14864 )));
14865 }
14866 let length_bytes = usize::try_from(length_bits / 8).map_err(|_| {
14867 SidecarError::InvalidState(String::from(
14868 "crypto.subtle.generateKey length is too large",
14869 ))
14870 })?;
14871 let mut raw = vec![0_u8; length_bytes];
14872 rand_bytes(&mut raw).map_err(javascript_crypto_openssl_error)?;
14873 let key = javascript_crypto_serialize_subtle_secret_key(
14874 &raw,
14875 javascript_crypto_normalize_subtle_secret_algorithm(algorithm.clone(), &raw)?,
14876 parsed
14877 .get("extractable")
14878 .and_then(Value::as_bool)
14879 .unwrap_or(false),
14880 parsed.get("usages").cloned().unwrap_or_else(|| json!([])),
14881 )?;
14882 Ok(Value::String(
14883 serde_json::to_string(&json!({ "key": key })).map_err(|error| {
14884 SidecarError::InvalidState(format!(
14885 "serialize crypto.subtle generated key: {error}"
14886 ))
14887 })?,
14888 ))
14889 }
14890 "importKey" => {
14891 let format = parsed
14892 .get("format")
14893 .and_then(Value::as_str)
14894 .ok_or_else(|| {
14895 SidecarError::InvalidState(String::from(
14896 "crypto.subtle.importKey missing format",
14897 ))
14898 })?;
14899 if format != "raw" {
14900 return Err(SidecarError::InvalidState(format!(
14901 "Unsupported import format: {format}"
14902 )));
14903 }
14904 let key_data = parsed
14905 .get("keyData")
14906 .and_then(Value::as_str)
14907 .ok_or_else(|| {
14908 SidecarError::InvalidState(String::from(
14909 "crypto.subtle.importKey missing keyData",
14910 ))
14911 })?;
14912 let raw = base64::engine::general_purpose::STANDARD
14913 .decode(key_data)
14914 .map_err(|error| {
14915 SidecarError::InvalidState(format!(
14916 "crypto.subtle.importKey keyData base64: {error}"
14917 ))
14918 })?;
14919 let algorithm = parsed.get("algorithm").ok_or_else(|| {
14920 SidecarError::InvalidState(String::from(
14921 "crypto.subtle.importKey missing algorithm",
14922 ))
14923 })?;
14924 let key = javascript_crypto_serialize_subtle_secret_key(
14925 &raw,
14926 javascript_crypto_normalize_subtle_secret_algorithm(algorithm.clone(), &raw)?,
14927 parsed
14928 .get("extractable")
14929 .and_then(Value::as_bool)
14930 .unwrap_or(false),
14931 parsed.get("usages").cloned().unwrap_or_else(|| json!([])),
14932 )?;
14933 Ok(Value::String(
14934 serde_json::to_string(&json!({ "key": key })).map_err(|error| {
14935 SidecarError::InvalidState(format!(
14936 "serialize crypto.subtle imported key: {error}"
14937 ))
14938 })?,
14939 ))
14940 }
14941 "exportKey" => {
14942 let format = parsed
14943 .get("format")
14944 .and_then(Value::as_str)
14945 .ok_or_else(|| {
14946 SidecarError::InvalidState(String::from(
14947 "crypto.subtle.exportKey missing format",
14948 ))
14949 })?;
14950 if format != "raw" {
14951 return Err(SidecarError::InvalidState(format!(
14952 "Unsupported export format: {format}"
14953 )));
14954 }
14955 let raw = javascript_crypto_subtle_key_raw(
14956 parsed.get("key").ok_or_else(|| {
14957 SidecarError::InvalidState(String::from("crypto.subtle.exportKey missing key"))
14958 })?,
14959 "crypto.subtle.exportKey key",
14960 )?;
14961 Ok(Value::String(
14962 serde_json::to_string(&json!({
14963 "data": base64::engine::general_purpose::STANDARD.encode(raw),
14964 }))
14965 .map_err(|error| {
14966 SidecarError::InvalidState(format!("serialize crypto.subtle export: {error}"))
14967 })?,
14968 ))
14969 }
14970 "encrypt" | "decrypt" => service_javascript_crypto_subtle_aes_crypt_sync_rpc(op, &parsed),
14971 _ => Err(SidecarError::InvalidState(format!(
14972 "Unsupported subtle operation: {op}"
14973 ))),
14974 }
14975}
14976
14977fn javascript_crypto_subtle_algorithm_name<'a>(
14978 algorithm: &'a Value,
14979 label: &str,
14980) -> Result<&'a str, SidecarError> {
14981 if let Some(name) = algorithm.as_str() {
14982 return Ok(name);
14983 }
14984 algorithm
14985 .get("name")
14986 .and_then(Value::as_str)
14987 .ok_or_else(|| SidecarError::InvalidState(format!("{label} algorithm missing name")))
14988}
14989
14990fn javascript_crypto_normalize_subtle_secret_algorithm(
14991 algorithm: Value,
14992 raw: &[u8],
14993) -> Result<Value, SidecarError> {
14994 let mut object = match algorithm {
14995 Value::String(name) => {
14996 let mut object = Map::new();
14997 object.insert(String::from("name"), Value::String(name));
14998 object
14999 }
15000 Value::Object(object) => object,
15001 _ => {
15002 return Err(SidecarError::InvalidState(String::from(
15003 "crypto.subtle secret algorithm must be a string or object",
15004 )));
15005 }
15006 };
15007 let name = object
15008 .get("name")
15009 .and_then(Value::as_str)
15010 .ok_or_else(|| {
15011 SidecarError::InvalidState(String::from("crypto.subtle secret algorithm missing name"))
15012 })?
15013 .to_string();
15014 if matches!(name.as_str(), "AES-GCM" | "AES-CBC" | "AES-CTR" | "AES-KW")
15015 && !object.contains_key("length")
15016 {
15017 object.insert(String::from("length"), json!(raw.len() * 8));
15018 }
15019 Ok(Value::Object(object))
15020}
15021
15022fn javascript_crypto_serialize_subtle_secret_key(
15023 raw: &[u8],
15024 algorithm: Value,
15025 extractable: bool,
15026 usages: Value,
15027) -> Result<Value, SidecarError> {
15028 let raw_base64 = base64::engine::general_purpose::STANDARD.encode(raw);
15029 let source_key_object_data = javascript_crypto_serialize_sandbox_key_object(
15030 &JavascriptCryptoKeyMaterial::Secret(raw.to_vec()),
15031 )?;
15032 Ok(json!({
15033 "type": "secret",
15034 "algorithm": algorithm,
15035 "extractable": extractable,
15036 "usages": usages,
15037 "_raw": raw_base64,
15038 "_sourceKeyObjectData": source_key_object_data,
15039 }))
15040}
15041
15042fn javascript_crypto_subtle_key_raw(key: &Value, label: &str) -> Result<Vec<u8>, SidecarError> {
15043 let raw = key.get("_raw").and_then(Value::as_str).ok_or_else(|| {
15044 SidecarError::InvalidState(format!("{label} must be a raw secret CryptoKey"))
15045 })?;
15046 base64::engine::general_purpose::STANDARD
15047 .decode(raw)
15048 .map_err(|error| SidecarError::InvalidState(format!("{label} raw base64: {error}")))
15049}
15050
15051fn service_javascript_crypto_subtle_aes_crypt_sync_rpc(
15052 op: &str,
15053 parsed: &Value,
15054) -> Result<Value, SidecarError> {
15055 let algorithm = parsed.get("algorithm").ok_or_else(|| {
15056 SidecarError::InvalidState(format!("crypto.subtle.{op} missing algorithm"))
15057 })?;
15058 let name = javascript_crypto_subtle_algorithm_name(algorithm, &format!("crypto.subtle.{op}"))?;
15059 if name != "AES-GCM" {
15060 return Err(SidecarError::InvalidState(format!(
15061 "Unsupported subtle AES operation algorithm: {name}"
15062 )));
15063 }
15064 let key = javascript_crypto_subtle_key_raw(
15065 parsed
15066 .get("key")
15067 .ok_or_else(|| SidecarError::InvalidState(format!("crypto.subtle.{op} missing key")))?,
15068 &format!("crypto.subtle.{op} key"),
15069 )?;
15070 let iv = algorithm.get("iv").and_then(Value::as_str).ok_or_else(|| {
15071 SidecarError::InvalidState(format!("crypto.subtle.{op} AES-GCM missing iv"))
15072 })?;
15073 let iv = base64::engine::general_purpose::STANDARD
15074 .decode(iv)
15075 .map_err(|error| {
15076 SidecarError::InvalidState(format!("crypto.subtle.{op} iv base64: {error}"))
15077 })?;
15078 let data = parsed
15079 .get("data")
15080 .and_then(Value::as_str)
15081 .ok_or_else(|| SidecarError::InvalidState(format!("crypto.subtle.{op} missing data")))?;
15082 let mut data = base64::engine::general_purpose::STANDARD
15083 .decode(data)
15084 .map_err(|error| {
15085 SidecarError::InvalidState(format!("crypto.subtle.{op} data base64: {error}"))
15086 })?;
15087 let tag_len = javascript_crypto_subtle_aes_gcm_tag_len(algorithm)?;
15088 let mut options = Map::new();
15089 options.insert(String::from("authTagLength"), json!(tag_len));
15090 if let Some(additional_data) = algorithm.get("additionalData").and_then(Value::as_str) {
15091 options.insert(
15092 String::from("aad"),
15093 Value::String(additional_data.to_string()),
15094 );
15095 }
15096 let decrypt = op == "decrypt";
15097 if decrypt {
15098 if data.len() < tag_len {
15099 return Err(SidecarError::InvalidState(String::from(
15100 "crypto.subtle.decrypt AES-GCM data shorter than auth tag",
15101 )));
15102 }
15103 let auth_tag = data.split_off(data.len() - tag_len);
15104 options.insert(
15105 String::from("authTag"),
15106 Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
15107 );
15108 }
15109 let cipher_name = format!("aes-{}-gcm", key.len() * 8);
15110 let mut context = javascript_crypto_build_cipher_context(
15111 &cipher_name,
15112 &key,
15113 Some(&iv),
15114 decrypt,
15115 Some(&Value::Object(options)),
15116 )?;
15117 let mut output = javascript_crypto_cipher_update(&mut context, &data)?;
15118 output.extend(javascript_crypto_cipher_finalize(&mut context)?);
15119 if !decrypt {
15120 let mut auth_tag = vec![0_u8; tag_len];
15121 context
15122 .get_tag(&mut auth_tag)
15123 .map_err(javascript_crypto_openssl_error)?;
15124 output.extend(auth_tag);
15125 }
15126 Ok(Value::String(
15127 serde_json::to_string(&json!({
15128 "data": base64::engine::general_purpose::STANDARD.encode(output),
15129 }))
15130 .map_err(|error| {
15131 SidecarError::InvalidState(format!("serialize crypto.subtle {op}: {error}"))
15132 })?,
15133 ))
15134}
15135
15136fn javascript_crypto_subtle_aes_gcm_tag_len(algorithm: &Value) -> Result<usize, SidecarError> {
15137 let tag_bits = algorithm
15138 .get("tagLength")
15139 .and_then(Value::as_u64)
15140 .unwrap_or(128);
15141 if !tag_bits.is_multiple_of(8) {
15142 return Err(SidecarError::InvalidState(String::from(
15143 "crypto.subtle AES-GCM tagLength must be byte-aligned",
15144 )));
15145 }
15146 usize::try_from(tag_bits / 8).map_err(|_| {
15147 SidecarError::InvalidState(String::from("crypto.subtle AES-GCM tagLength too large"))
15148 })
15149}
15150
15151fn service_javascript_crypto_cipheriv_inner(
15152 request: &JavascriptSyncRpcRequest,
15153 decrypt: bool,
15154) -> Result<Value, SidecarError> {
15155 let label = if decrypt {
15156 "crypto.decipheriv"
15157 } else {
15158 "crypto.cipheriv"
15159 };
15160 let algorithm = javascript_sync_rpc_arg_str(&request.args, 0, &format!("{label} algorithm"))?;
15161 let key = javascript_sync_rpc_base64_arg(&request.args, 1, &format!("{label} key"))?;
15162 let iv = javascript_sync_rpc_base64_arg_optional(&request.args, 2, &format!("{label} iv"))?;
15163 let data = javascript_sync_rpc_base64_arg(&request.args, 3, &format!("{label} data"))?;
15164 let options =
15165 javascript_sync_rpc_json_arg_optional(&request.args, 4, &format!("{label} options"))?;
15166 let auth_tag_len = javascript_crypto_requested_aead_tag_len(algorithm, options.as_ref())?;
15167 let mut context = javascript_crypto_build_cipher_context(
15168 algorithm,
15169 &key,
15170 iv.as_deref(),
15171 decrypt,
15172 options.as_ref(),
15173 )?;
15174 let payload = javascript_crypto_cipher_update(&mut context, &data)?;
15175 let final_bytes = javascript_crypto_cipher_finalize(&mut context)?;
15176 if decrypt {
15177 let mut output = payload;
15178 output.extend(final_bytes);
15179 return Ok(Value::String(
15180 base64::engine::general_purpose::STANDARD.encode(output),
15181 ));
15182 }
15183
15184 let mut response = Map::new();
15185 let mut encrypted = payload;
15186 encrypted.extend(final_bytes);
15187 response.insert(
15188 String::from("data"),
15189 Value::String(base64::engine::general_purpose::STANDARD.encode(encrypted)),
15190 );
15191 if javascript_crypto_is_aead(algorithm) {
15192 let mut auth_tag = vec![0_u8; auth_tag_len];
15193 context
15194 .get_tag(&mut auth_tag)
15195 .map_err(javascript_crypto_openssl_error)?;
15196 response.insert(
15197 String::from("authTag"),
15198 Value::String(base64::engine::general_purpose::STANDARD.encode(auth_tag)),
15199 );
15200 }
15201 Ok(Value::String(serde_json::to_string(&response).map_err(
15202 |error| SidecarError::InvalidState(format!("serialize {label} response: {error}")),
15203 )?))
15204}
15205
15206fn javascript_sync_rpc_base64_arg_optional(
15207 args: &[Value],
15208 index: usize,
15209 label: &str,
15210) -> Result<Option<Vec<u8>>, SidecarError> {
15211 if args.get(index).is_none() || args[index].is_null() {
15212 return Ok(None);
15213 }
15214 javascript_sync_rpc_base64_arg(args, index, label).map(Some)
15215}
15216
15217fn javascript_sync_rpc_json_arg_optional(
15218 args: &[Value],
15219 index: usize,
15220 label: &str,
15221) -> Result<Option<Value>, SidecarError> {
15222 if args.get(index).is_none() || args[index].is_null() {
15223 return Ok(None);
15224 }
15225 let raw = javascript_sync_rpc_arg_str(args, index, label)?;
15226 serde_json::from_str(raw)
15227 .map(Some)
15228 .map_err(|error| SidecarError::InvalidState(format!("{label} must be valid JSON: {error}")))
15229}
15230
15231fn javascript_crypto_parse_direct_key_input(
15232 raw: &str,
15233 expected: Option<&str>,
15234 label: &str,
15235) -> Result<JavascriptDirectKeyInput, SidecarError> {
15236 let parsed: Value = serde_json::from_str(raw).map_err(|error| {
15237 SidecarError::InvalidState(format!("{label} must be valid JSON: {error}"))
15238 })?;
15239 let padding = match parsed.as_object().and_then(|value| value.get("padding")) {
15240 Some(value) => javascript_crypto_padding_from_value(value)?,
15241 None => None,
15242 };
15243 Ok(JavascriptDirectKeyInput {
15244 key: javascript_crypto_parse_key_material_value(&parsed, expected, label)?,
15245 padding,
15246 })
15247}
15248
15249fn javascript_crypto_parse_key_material_value(
15250 value: &Value,
15251 expected: Option<&str>,
15252 label: &str,
15253) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15254 if let Some(object) = value.as_object() {
15255 if object.get("__type").and_then(Value::as_str) == Some("keyObject") {
15256 let serialized = object.get("value").ok_or_else(|| {
15257 SidecarError::InvalidState(format!("{label} keyObject is missing a value"))
15258 })?;
15259 return javascript_crypto_parse_serialized_key_object(serialized, expected, label);
15260 }
15261 if object.contains_key("type") && (object.contains_key("pem") || object.contains_key("raw"))
15262 {
15263 return javascript_crypto_parse_serialized_key_object(value, expected, label);
15264 }
15265 if let Some(source) = object.get("key") {
15266 return javascript_crypto_parse_key_source(
15267 source,
15268 object.get("format").and_then(Value::as_str),
15269 object.get("type").and_then(Value::as_str),
15270 expected,
15271 label,
15272 );
15273 }
15274 }
15275 javascript_crypto_parse_key_source(value, None, None, expected, label)
15276}
15277
15278fn javascript_crypto_parse_key_source(
15279 source: &Value,
15280 format: Option<&str>,
15281 kind: Option<&str>,
15282 expected: Option<&str>,
15283 label: &str,
15284) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15285 match source {
15286 Value::String(pem) => javascript_crypto_parse_key_from_pem(pem.as_bytes(), expected, label),
15287 Value::Object(object) if object.get("__type").and_then(Value::as_str) == Some("buffer") => {
15288 let data = javascript_crypto_decode_bridge_buffer(source, label)?;
15289 javascript_crypto_parse_key_from_bytes(&data, format, kind, expected, label)
15290 }
15291 Value::Object(_) => {
15292 if format == Some("jwk") {
15293 return Err(SidecarError::InvalidState(format!(
15294 "{label} jwk inputs are not supported yet"
15295 )));
15296 }
15297 Err(SidecarError::InvalidState(format!(
15298 "{label} has an unsupported key shape"
15299 )))
15300 }
15301 _ => Err(SidecarError::InvalidState(format!(
15302 "{label} has an unsupported key value"
15303 ))),
15304 }
15305}
15306
15307fn javascript_crypto_parse_key_from_pem(
15308 pem: &[u8],
15309 expected: Option<&str>,
15310 label: &str,
15311) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15312 match expected {
15313 Some("private") => PKey::private_key_from_pem(pem)
15314 .map(JavascriptCryptoKeyMaterial::Private)
15315 .map_err(|error| {
15316 SidecarError::InvalidState(format!("{label} private key is invalid: {error}"))
15317 }),
15318 Some("public") => PKey::public_key_from_pem(pem)
15319 .map(JavascriptCryptoKeyMaterial::Public)
15320 .map_err(|error| {
15321 SidecarError::InvalidState(format!("{label} public key is invalid: {error}"))
15322 }),
15323 _ => PKey::private_key_from_pem(pem)
15324 .map(JavascriptCryptoKeyMaterial::Private)
15325 .or_else(|_| PKey::public_key_from_pem(pem).map(JavascriptCryptoKeyMaterial::Public))
15326 .map_err(|error| {
15327 SidecarError::InvalidState(format!("{label} PEM key is invalid: {error}"))
15328 }),
15329 }
15330}
15331
15332fn javascript_crypto_parse_key_from_bytes(
15333 der: &[u8],
15334 format: Option<&str>,
15335 kind: Option<&str>,
15336 expected: Option<&str>,
15337 label: &str,
15338) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15339 match (format.unwrap_or("der"), kind.or(expected)) {
15340 ("der", Some("pkcs8")) | ("der", Some("private")) => PKey::private_key_from_der(der)
15341 .map(JavascriptCryptoKeyMaterial::Private)
15342 .map_err(|error| {
15343 SidecarError::InvalidState(format!("{label} private key DER is invalid: {error}"))
15344 }),
15345 ("der", Some("spki")) | ("der", Some("public")) => PKey::public_key_from_der(der)
15346 .map(JavascriptCryptoKeyMaterial::Public)
15347 .map_err(|error| {
15348 SidecarError::InvalidState(format!("{label} public key DER is invalid: {error}"))
15349 }),
15350 _ => Err(SidecarError::InvalidState(format!(
15351 "{label} unsupported key bytes format"
15352 ))),
15353 }
15354}
15355
15356fn javascript_crypto_parse_serialized_key_object(
15357 value: &Value,
15358 expected: Option<&str>,
15359 label: &str,
15360) -> Result<JavascriptCryptoKeyMaterial, SidecarError> {
15361 let serialized: JavascriptSerializedSandboxKeyObject = serde_json::from_value(value.clone())
15362 .map_err(|error| {
15363 SidecarError::InvalidState(format!("{label} keyObject is invalid: {error}"))
15364 })?;
15365 match serialized.kind.as_str() {
15366 "secret" => {
15367 if expected == Some("public") || expected == Some("private") {
15368 return Err(SidecarError::InvalidState(format!(
15369 "{label} expected an asymmetric key"
15370 )));
15371 }
15372 Ok(JavascriptCryptoKeyMaterial::Secret(
15373 base64::engine::general_purpose::STANDARD
15374 .decode(serialized.raw.unwrap_or_default())
15375 .map_err(|error| {
15376 SidecarError::InvalidState(format!(
15377 "{label} secret key contains invalid base64: {error}"
15378 ))
15379 })?,
15380 ))
15381 }
15382 "private" => {
15383 let pem = serialized.pem.ok_or_else(|| {
15384 SidecarError::InvalidState(format!("{label} private keyObject is missing pem"))
15385 })?;
15386 javascript_crypto_parse_key_from_pem(pem.as_bytes(), Some("private"), label)
15387 }
15388 "public" => {
15389 let pem = serialized.pem.ok_or_else(|| {
15390 SidecarError::InvalidState(format!("{label} public keyObject is missing pem"))
15391 })?;
15392 javascript_crypto_parse_key_from_pem(pem.as_bytes(), Some("public"), label)
15393 }
15394 other => Err(SidecarError::InvalidState(format!(
15395 "{label} has unsupported keyObject type {other}"
15396 ))),
15397 }
15398}
15399
15400fn javascript_crypto_expect_private_key(
15401 key: JavascriptCryptoKeyMaterial,
15402 label: &str,
15403) -> Result<PKey<Private>, SidecarError> {
15404 match key {
15405 JavascriptCryptoKeyMaterial::Private(key) => Ok(key),
15406 _ => Err(SidecarError::InvalidState(format!(
15407 "{label} requires a private key"
15408 ))),
15409 }
15410}
15411
15412fn javascript_crypto_expect_public_key(
15413 key: JavascriptCryptoKeyMaterial,
15414 label: &str,
15415) -> Result<PKey<Public>, SidecarError> {
15416 match key {
15417 JavascriptCryptoKeyMaterial::Public(key) => Ok(key),
15418 JavascriptCryptoKeyMaterial::Private(key) => {
15419 let pem = key
15420 .public_key_to_pem()
15421 .map_err(javascript_crypto_openssl_error)?;
15422 PKey::public_key_from_pem(&pem).map_err(javascript_crypto_openssl_error)
15423 }
15424 _ => Err(SidecarError::InvalidState(format!(
15425 "{label} requires a public key"
15426 ))),
15427 }
15428}
15429
15430fn javascript_crypto_new_signer<'a>(
15431 algorithm: Option<&'a str>,
15432 key: &'a PKey<Private>,
15433) -> Result<Signer<'a>, SidecarError> {
15434 if matches!(key.id(), PKeyId::ED25519 | PKeyId::ED448) || algorithm.is_none() {
15435 return Signer::new_without_digest(key).map_err(javascript_crypto_openssl_error);
15436 }
15437 Signer::new(
15438 javascript_crypto_message_digest_from_name(algorithm.ok_or_else(|| {
15439 SidecarError::InvalidState(String::from("crypto.sign requires a digest algorithm"))
15440 })?)?,
15441 key,
15442 )
15443 .map_err(javascript_crypto_openssl_error)
15444}
15445
15446fn javascript_crypto_new_verifier<'a>(
15447 algorithm: Option<&'a str>,
15448 key: &'a PKey<Public>,
15449) -> Result<Verifier<'a>, SidecarError> {
15450 if matches!(key.id(), PKeyId::ED25519 | PKeyId::ED448) || algorithm.is_none() {
15451 return Verifier::new_without_digest(key).map_err(javascript_crypto_openssl_error);
15452 }
15453 Verifier::new(
15454 javascript_crypto_message_digest_from_name(algorithm.ok_or_else(|| {
15455 SidecarError::InvalidState(String::from("crypto.verify requires a digest algorithm"))
15456 })?)?,
15457 key,
15458 )
15459 .map_err(javascript_crypto_openssl_error)
15460}
15461
15462fn javascript_crypto_message_digest_from_name(name: &str) -> Result<MessageDigest, SidecarError> {
15463 match name.trim().to_ascii_lowercase().replace('-', "").as_str() {
15464 "md5" => Ok(MessageDigest::md5()),
15465 "sha1" => Ok(MessageDigest::sha1()),
15466 "sha256" => Ok(MessageDigest::sha256()),
15467 "sha384" => Ok(MessageDigest::sha384()),
15468 "sha512" => Ok(MessageDigest::sha512()),
15469 other => Err(SidecarError::InvalidState(format!(
15470 "unsupported crypto digest algorithm {other}"
15471 ))),
15472 }
15473}
15474
15475fn javascript_crypto_padding_from_value(value: &Value) -> Result<Option<Padding>, SidecarError> {
15476 let Some(number) = value.as_i64() else {
15477 return Ok(None);
15478 };
15479 let padding = match number {
15480 1 => Padding::PKCS1,
15481 3 => Padding::NONE,
15482 4 => Padding::PKCS1_OAEP,
15483 6 => Padding::PKCS1_PSS,
15484 other => {
15485 return Err(SidecarError::InvalidState(format!(
15486 "unsupported RSA padding constant {other}"
15487 )));
15488 }
15489 };
15490 Ok(Some(padding))
15491}
15492
15493fn javascript_crypto_decode_bridge_buffer(
15494 value: &Value,
15495 label: &str,
15496) -> Result<Vec<u8>, SidecarError> {
15497 let base64_value = value
15498 .as_object()
15499 .filter(|object| object.get("__type").and_then(Value::as_str) == Some("buffer"))
15500 .and_then(|object| object.get("value"))
15501 .and_then(Value::as_str)
15502 .ok_or_else(|| {
15503 SidecarError::InvalidState(format!("{label} must be a serialized bridge buffer"))
15504 })?;
15505 base64::engine::general_purpose::STANDARD
15506 .decode(base64_value)
15507 .map_err(|error| {
15508 SidecarError::InvalidState(format!("{label} contains invalid base64: {error}"))
15509 })
15510}
15511
15512fn javascript_crypto_serialize_sandbox_key_object(
15513 key: &JavascriptCryptoKeyMaterial,
15514) -> Result<Value, SidecarError> {
15515 let serialized = match key {
15516 JavascriptCryptoKeyMaterial::Private(key) => JavascriptSerializedSandboxKeyObject {
15517 kind: String::from("private"),
15518 pem: Some(
15519 String::from_utf8(
15520 key.private_key_to_pem_pkcs8()
15521 .map_err(javascript_crypto_openssl_error)?,
15522 )
15523 .map_err(|error| {
15524 SidecarError::InvalidState(format!("private key PEM is not utf8: {error}"))
15525 })?,
15526 ),
15527 raw: None,
15528 asymmetric_key_type: javascript_crypto_pkey_type_name(key.id()),
15529 asymmetric_key_details: None,
15530 jwk: None,
15531 },
15532 JavascriptCryptoKeyMaterial::Public(key) => JavascriptSerializedSandboxKeyObject {
15533 kind: String::from("public"),
15534 pem: Some(
15535 String::from_utf8(
15536 key.public_key_to_pem()
15537 .map_err(javascript_crypto_openssl_error)?,
15538 )
15539 .map_err(|error| {
15540 SidecarError::InvalidState(format!("public key PEM is not utf8: {error}"))
15541 })?,
15542 ),
15543 raw: None,
15544 asymmetric_key_type: javascript_crypto_pkey_type_name(key.id()),
15545 asymmetric_key_details: None,
15546 jwk: None,
15547 },
15548 JavascriptCryptoKeyMaterial::Secret(raw) => JavascriptSerializedSandboxKeyObject {
15549 kind: String::from("secret"),
15550 pem: None,
15551 raw: Some(base64::engine::general_purpose::STANDARD.encode(raw)),
15552 asymmetric_key_type: None,
15553 asymmetric_key_details: None,
15554 jwk: None,
15555 },
15556 };
15557 serde_json::to_value(serialized)
15558 .map_err(|error| SidecarError::InvalidState(format!("serialize key object: {error}")))
15559}
15560
15561fn javascript_crypto_pkey_type_name(id: PKeyId) -> Option<String> {
15562 match id {
15563 PKeyId::RSA => Some(String::from("rsa")),
15564 PKeyId::EC => Some(String::from("ec")),
15565 PKeyId::ED25519 => Some(String::from("ed25519")),
15566 PKeyId::ED448 => Some(String::from("ed448")),
15567 PKeyId::X25519 => Some(String::from("x25519")),
15568 PKeyId::X448 => Some(String::from("x448")),
15569 PKeyId::DH => Some(String::from("dh")),
15570 _ => None,
15571 }
15572}
15573
15574fn javascript_crypto_rsa_output_size(
15575 key: &JavascriptCryptoKeyMaterial,
15576) -> Result<usize, SidecarError> {
15577 match key {
15578 JavascriptCryptoKeyMaterial::Private(key) => key
15579 .rsa()
15580 .map(|rsa| rsa.size() as usize)
15581 .map_err(javascript_crypto_openssl_error),
15582 JavascriptCryptoKeyMaterial::Public(key) => key
15583 .rsa()
15584 .map(|rsa| rsa.size() as usize)
15585 .map_err(javascript_crypto_openssl_error),
15586 JavascriptCryptoKeyMaterial::Secret(_) => Err(SidecarError::InvalidState(String::from(
15587 "RSA operations require an asymmetric key",
15588 ))),
15589 }
15590}
15591
15592fn javascript_crypto_parse_serialized_options_arg(
15593 args: &[Value],
15594 index: usize,
15595 label: &str,
15596) -> Result<Option<Value>, SidecarError> {
15597 let Some(raw) = args.get(index).and_then(Value::as_str) else {
15598 return Ok(None);
15599 };
15600 let parsed: Value = serde_json::from_str(raw).map_err(|error| {
15601 SidecarError::InvalidState(format!("{label} must be valid JSON: {error}"))
15602 })?;
15603 if parsed.get("hasOptions").and_then(Value::as_bool) == Some(true) {
15604 Ok(parsed.get("options").cloned())
15605 } else {
15606 Ok(None)
15607 }
15608}
15609
15610fn javascript_crypto_u32_from_bridge_value(
15611 value: &Value,
15612 label: &str,
15613) -> Result<u32, SidecarError> {
15614 if let Some(number) = value.as_u64() {
15615 return u32::try_from(number)
15616 .map_err(|_| SidecarError::InvalidState(format!("{label} must fit within u32")));
15617 }
15618 let bytes = javascript_crypto_decode_bridge_buffer(value, label)?;
15619 if bytes.len() > 4 {
15620 return Err(SidecarError::InvalidState(format!(
15621 "{label} buffer is too large for u32"
15622 )));
15623 }
15624 Ok(bytes
15625 .into_iter()
15626 .fold(0_u32, |acc, byte| (acc << 8) | u32::from(byte)))
15627}
15628
15629fn javascript_crypto_bignum_from_bridge_value(
15630 value: &Value,
15631 label: &str,
15632) -> Result<BigNum, SidecarError> {
15633 if let Some(object) = value.as_object() {
15634 if object.get("__type").and_then(Value::as_str) == Some("bigint") {
15635 let decimal = object.get("value").and_then(Value::as_str).ok_or_else(|| {
15636 SidecarError::InvalidState(format!("{label} bigint is missing a value"))
15637 })?;
15638 return BigNum::from_dec_str(decimal).map_err(javascript_crypto_openssl_error);
15639 }
15640 }
15641 let bytes = javascript_crypto_decode_bridge_buffer(value, label)?;
15642 BigNum::from_slice(&bytes).map_err(javascript_crypto_openssl_error)
15643}
15644
15645fn javascript_crypto_curve_nid(name: &str) -> Result<Nid, SidecarError> {
15646 match name {
15647 "prime256v1" | "P-256" => Ok(Nid::X9_62_PRIME256V1),
15648 "secp384r1" | "P-384" => Ok(Nid::SECP384R1),
15649 "secp521r1" | "P-521" => Ok(Nid::SECP521R1),
15650 "secp256k1" => Ok(Nid::SECP256K1),
15651 other => Err(SidecarError::InvalidState(format!(
15652 "unsupported EC curve {other}"
15653 ))),
15654 }
15655}
15656
15657fn javascript_crypto_named_dh_group(name: &str) -> Result<Dh<Params>, SidecarError> {
15658 match name {
15659 "modp2" => Dh::get_1024_160().map_err(javascript_crypto_openssl_error),
15660 "modp14" | "modp15" | "modp16" | "modp17" | "modp18" => {
15661 Dh::get_2048_256().map_err(javascript_crypto_openssl_error)
15662 }
15663 other => Err(SidecarError::InvalidState(format!(
15664 "unsupported Diffie-Hellman group {other}"
15665 ))),
15666 }
15667}
15668
15669fn javascript_crypto_clone_dh_params(params: &Dh<Params>) -> Result<Dh<Params>, SidecarError> {
15670 Dh::from_pqg(
15671 params
15672 .prime_p()
15673 .to_owned()
15674 .map_err(javascript_crypto_openssl_error)?,
15675 params
15676 .prime_q()
15677 .map(|value| value.to_owned().map_err(javascript_crypto_openssl_error))
15678 .transpose()?,
15679 params
15680 .generator()
15681 .to_owned()
15682 .map_err(javascript_crypto_openssl_error)?,
15683 )
15684 .map_err(javascript_crypto_openssl_error)
15685}
15686
15687fn javascript_crypto_build_dh_params(args: &[Value]) -> Result<Dh<Params>, SidecarError> {
15688 let Some(first) = args.first() else {
15689 return Err(SidecarError::InvalidState(String::from(
15690 "Diffie-Hellman session args are required",
15691 )));
15692 };
15693 if let Some(bits) = first.as_u64() {
15694 let generator = args
15695 .get(1)
15696 .map(|value| javascript_crypto_u32_from_bridge_value(value, "Diffie-Hellman generator"))
15697 .transpose()?
15698 .unwrap_or(2);
15699 return Dh::generate_params(bits as u32, generator)
15700 .map_err(javascript_crypto_openssl_error);
15701 }
15702 let prime = javascript_crypto_bignum_from_bridge_value(first, "Diffie-Hellman prime")?;
15703 let generator = args
15704 .get(1)
15705 .map(|value| javascript_crypto_bignum_from_bridge_value(value, "Diffie-Hellman generator"))
15706 .transpose()?
15707 .unwrap_or(BigNum::from_u32(2).map_err(javascript_crypto_openssl_error)?);
15708 Dh::from_pqg(prime, None, generator).map_err(javascript_crypto_openssl_error)
15709}
15710
15711fn javascript_crypto_call_dh_session(
15712 session: &mut ActiveDhSession,
15713 method: &str,
15714 args: &[Value],
15715) -> Result<(Value, bool), SidecarError> {
15716 match method {
15717 "verifyError" => Ok((Value::Null, false)),
15718 "generateKeys" => {
15719 if session.key_pair.is_none() {
15720 session.key_pair = Some(
15721 javascript_crypto_clone_dh_params(&session.params)?
15722 .generate_key()
15723 .map_err(javascript_crypto_openssl_error)?,
15724 );
15725 }
15726 let public = session
15727 .key_pair
15728 .as_ref()
15729 .expect("dh key pair")
15730 .public_key()
15731 .to_vec();
15732 Ok((javascript_crypto_bridge_buffer_value(&public), true))
15733 }
15734 "computeSecret" => {
15735 if session.key_pair.is_none() {
15736 session.key_pair = Some(
15737 javascript_crypto_clone_dh_params(&session.params)?
15738 .generate_key()
15739 .map_err(javascript_crypto_openssl_error)?,
15740 );
15741 }
15742 let peer = javascript_crypto_bignum_from_bridge_value(
15743 args.first().ok_or_else(|| {
15744 SidecarError::InvalidState(String::from(
15745 "computeSecret requires peer public key",
15746 ))
15747 })?,
15748 "Diffie-Hellman peer public key",
15749 )?;
15750 let secret = session
15751 .key_pair
15752 .as_ref()
15753 .expect("dh key pair")
15754 .compute_key(&peer)
15755 .map_err(javascript_crypto_openssl_error)?;
15756 Ok((javascript_crypto_bridge_buffer_value(&secret), true))
15757 }
15758 "getPrime" => Ok((
15759 javascript_crypto_bridge_buffer_value(&session.params.prime_p().to_vec()),
15760 true,
15761 )),
15762 "getGenerator" => Ok((
15763 javascript_crypto_bridge_buffer_value(&session.params.generator().to_vec()),
15764 true,
15765 )),
15766 "getPublicKey" => {
15767 if session.key_pair.is_none() {
15768 session.key_pair = Some(
15769 javascript_crypto_clone_dh_params(&session.params)?
15770 .generate_key()
15771 .map_err(javascript_crypto_openssl_error)?,
15772 );
15773 }
15774 Ok((
15775 javascript_crypto_bridge_buffer_value(
15776 &session
15777 .key_pair
15778 .as_ref()
15779 .expect("dh key pair")
15780 .public_key()
15781 .to_vec(),
15782 ),
15783 true,
15784 ))
15785 }
15786 "getPrivateKey" => {
15787 if session.key_pair.is_none() {
15788 session.key_pair = Some(
15789 javascript_crypto_clone_dh_params(&session.params)?
15790 .generate_key()
15791 .map_err(javascript_crypto_openssl_error)?,
15792 );
15793 }
15794 Ok((
15795 javascript_crypto_bridge_buffer_value(
15796 &session
15797 .key_pair
15798 .as_ref()
15799 .expect("dh key pair")
15800 .private_key()
15801 .to_vec(),
15802 ),
15803 true,
15804 ))
15805 }
15806 other => Err(SidecarError::InvalidState(format!(
15807 "Unsupported Diffie-Hellman method: {other}"
15808 ))),
15809 }
15810}
15811
15812fn javascript_crypto_call_ecdh_session(
15813 session: &mut ActiveEcdhSession,
15814 method: &str,
15815 args: &[Value],
15816) -> Result<(Value, bool), SidecarError> {
15817 let nid = javascript_crypto_curve_nid(&session.curve)?;
15818 let group = EcGroup::from_curve_name(nid).map_err(javascript_crypto_openssl_error)?;
15819 match method {
15820 "verifyError" => Ok((Value::Null, false)),
15821 "generateKeys" => {
15822 if session.key_pair.is_none() {
15823 session.key_pair =
15824 Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15825 }
15826 let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
15827 let bytes = session
15828 .key_pair
15829 .as_ref()
15830 .expect("ecdh key pair")
15831 .public_key()
15832 .to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx)
15833 .map_err(javascript_crypto_openssl_error)?;
15834 Ok((javascript_crypto_bridge_buffer_value(&bytes), true))
15835 }
15836 "computeSecret" => {
15837 if session.key_pair.is_none() {
15838 session.key_pair =
15839 Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15840 }
15841 let peer_bytes = javascript_crypto_decode_bridge_buffer(
15842 args.first().ok_or_else(|| {
15843 SidecarError::InvalidState(String::from(
15844 "computeSecret requires peer public key",
15845 ))
15846 })?,
15847 "ECDH peer public key",
15848 )?;
15849 let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
15850 let peer_point = EcPoint::from_bytes(&group, &peer_bytes, &mut ctx)
15851 .map_err(javascript_crypto_openssl_error)?;
15852 let peer_key = EcKey::from_public_key(&group, &peer_point)
15853 .map_err(javascript_crypto_openssl_error)?;
15854 let private =
15855 PKey::from_ec_key(session.key_pair.as_ref().expect("ecdh key pair").to_owned())
15856 .map_err(javascript_crypto_openssl_error)?;
15857 let peer = PKey::from_ec_key(peer_key).map_err(javascript_crypto_openssl_error)?;
15858 let mut deriver = Deriver::new(&private).map_err(javascript_crypto_openssl_error)?;
15859 deriver
15860 .set_peer(&peer)
15861 .map_err(javascript_crypto_openssl_error)?;
15862 let secret = deriver
15863 .derive_to_vec()
15864 .map_err(javascript_crypto_openssl_error)?;
15865 Ok((javascript_crypto_bridge_buffer_value(&secret), true))
15866 }
15867 "getPublicKey" => {
15868 if session.key_pair.is_none() {
15869 session.key_pair =
15870 Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15871 }
15872 let mut ctx = BigNumContext::new().map_err(javascript_crypto_openssl_error)?;
15873 let bytes = session
15874 .key_pair
15875 .as_ref()
15876 .expect("ecdh key pair")
15877 .public_key()
15878 .to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx)
15879 .map_err(javascript_crypto_openssl_error)?;
15880 Ok((javascript_crypto_bridge_buffer_value(&bytes), true))
15881 }
15882 "getPrivateKey" => {
15883 if session.key_pair.is_none() {
15884 session.key_pair =
15885 Some(EcKey::generate(&group).map_err(javascript_crypto_openssl_error)?);
15886 }
15887 Ok((
15888 javascript_crypto_bridge_buffer_value(
15889 &session
15890 .key_pair
15891 .as_ref()
15892 .expect("ecdh key pair")
15893 .private_key()
15894 .to_vec(),
15895 ),
15896 true,
15897 ))
15898 }
15899 other => Err(SidecarError::InvalidState(format!(
15900 "Unsupported Diffie-Hellman method: {other}"
15901 ))),
15902 }
15903}
15904
15905fn javascript_crypto_serialize_encoded_key_value_public(
15906 key: &PKey<Public>,
15907 encoding: Option<&Value>,
15908) -> Result<Value, SidecarError> {
15909 if let Some(encoding) = encoding {
15910 let format = encoding
15911 .get("format")
15912 .and_then(Value::as_str)
15913 .unwrap_or("pem");
15914 return Ok(match format {
15915 "der" => json!({
15916 "kind": "buffer",
15917 "value": base64::engine::general_purpose::STANDARD
15918 .encode(key.public_key_to_der().map_err(javascript_crypto_openssl_error)?),
15919 }),
15920 _ => json!({
15921 "kind": "string",
15922 "value": String::from_utf8(
15923 key.public_key_to_pem().map_err(javascript_crypto_openssl_error)?,
15924 )
15925 .map_err(|error| SidecarError::InvalidState(format!("public key PEM utf8: {error}")))?,
15926 }),
15927 });
15928 }
15929 javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Public(
15930 key.to_owned(),
15931 ))
15932}
15933
15934fn javascript_crypto_serialize_encoded_key_value_private(
15935 key: &PKey<Private>,
15936 encoding: Option<&Value>,
15937) -> Result<Value, SidecarError> {
15938 if let Some(encoding) = encoding {
15939 let format = encoding
15940 .get("format")
15941 .and_then(Value::as_str)
15942 .unwrap_or("pem");
15943 return Ok(match format {
15944 "der" => json!({
15945 "kind": "buffer",
15946 "value": base64::engine::general_purpose::STANDARD
15947 .encode(key.private_key_to_der().map_err(javascript_crypto_openssl_error)?),
15948 }),
15949 _ => json!({
15950 "kind": "string",
15951 "value": String::from_utf8(
15952 key.private_key_to_pem_pkcs8().map_err(javascript_crypto_openssl_error)?,
15953 )
15954 .map_err(|error| SidecarError::InvalidState(format!("private key PEM utf8: {error}")))?,
15955 }),
15956 });
15957 }
15958 javascript_crypto_serialize_sandbox_key_object(&JavascriptCryptoKeyMaterial::Private(
15959 key.to_owned(),
15960 ))
15961}
15962
15963fn javascript_crypto_bridge_buffer_value(bytes: &[u8]) -> Value {
15964 json!({
15965 "__type": "buffer",
15966 "value": base64::engine::general_purpose::STANDARD.encode(bytes),
15967 })
15968}
15969
15970fn javascript_crypto_build_cipher_context(
15971 algorithm: &str,
15972 key: &[u8],
15973 iv: Option<&[u8]>,
15974 decrypt: bool,
15975 options: Option<&Value>,
15976) -> Result<Crypter, SidecarError> {
15977 let cipher = javascript_crypto_cipher_from_name(algorithm)?;
15978 let mode = if decrypt {
15979 Mode::Decrypt
15980 } else {
15981 Mode::Encrypt
15982 };
15983 let mut context =
15984 Crypter::new(cipher, mode, key, iv).map_err(javascript_crypto_openssl_error)?;
15985 if let Some(auto_padding) = options
15986 .and_then(|value| value.get("autoPadding"))
15987 .and_then(Value::as_bool)
15988 {
15989 context.pad(auto_padding);
15990 }
15991 if javascript_crypto_is_aead(algorithm) {
15992 if let Some(aad) = options
15993 .and_then(|value| value.get("aad"))
15994 .and_then(Value::as_str)
15995 {
15996 context
15997 .aad_update(
15998 &base64::engine::general_purpose::STANDARD
15999 .decode(aad)
16000 .map_err(|error| {
16001 SidecarError::InvalidState(format!(
16002 "cipher aad contains invalid base64: {error}"
16003 ))
16004 })?,
16005 )
16006 .map_err(javascript_crypto_openssl_error)?;
16007 }
16008 if decrypt {
16009 if let Some(auth_tag) = options
16010 .and_then(|value| value.get("authTag"))
16011 .and_then(Value::as_str)
16012 {
16013 let decoded = base64::engine::general_purpose::STANDARD
16014 .decode(auth_tag)
16015 .map_err(|error| {
16016 SidecarError::InvalidState(format!(
16017 "cipher authTag contains invalid base64: {error}"
16018 ))
16019 })?;
16020 context
16021 .set_tag(&decoded)
16022 .map_err(javascript_crypto_openssl_error)?;
16023 }
16024 }
16025 }
16026 Ok(context)
16027}
16028
16029fn javascript_crypto_requested_aead_tag_len(
16030 algorithm: &str,
16031 options: Option<&Value>,
16032) -> Result<usize, SidecarError> {
16033 if !javascript_crypto_is_aead(algorithm) {
16034 return Ok(0);
16035 }
16036 let requested = options
16037 .and_then(|value| value.get("authTagLength"))
16038 .and_then(Value::as_u64)
16039 .unwrap_or(javascript_crypto_aead_tag_len(algorithm) as u64);
16040 usize::try_from(requested).map_err(|_| {
16041 SidecarError::InvalidState(String::from("cipher authTagLength must fit within usize"))
16042 })
16043}
16044
16045fn javascript_crypto_cipher_update(
16046 context: &mut Crypter,
16047 data: &[u8],
16048) -> Result<Vec<u8>, SidecarError> {
16049 let mut output = vec![0_u8; data.len() + 32];
16050 let written = context
16051 .update(data, &mut output)
16052 .map_err(javascript_crypto_openssl_error)?;
16053 output.truncate(written);
16054 Ok(output)
16055}
16056
16057fn javascript_crypto_cipher_finalize(context: &mut Crypter) -> Result<Vec<u8>, SidecarError> {
16058 let mut output = vec![0_u8; 32];
16059 let written = context
16060 .finalize(&mut output)
16061 .map_err(javascript_crypto_openssl_error)?;
16062 output.truncate(written);
16063 Ok(output)
16064}
16065
16066fn javascript_crypto_cipher_from_name(name: &str) -> Result<Cipher, SidecarError> {
16067 match name.to_ascii_lowercase().as_str() {
16068 "aes-128-cbc" => Ok(Cipher::aes_128_cbc()),
16069 "aes-192-cbc" => Ok(Cipher::aes_192_cbc()),
16070 "aes-256-cbc" => Ok(Cipher::aes_256_cbc()),
16071 "aes-128-ctr" => Ok(Cipher::aes_128_ctr()),
16072 "aes-192-ctr" => Ok(Cipher::aes_192_ctr()),
16073 "aes-256-ctr" => Ok(Cipher::aes_256_ctr()),
16074 "aes-128-gcm" => Ok(Cipher::aes_128_gcm()),
16075 "aes-192-gcm" => Ok(Cipher::aes_192_gcm()),
16076 "aes-256-gcm" => Ok(Cipher::aes_256_gcm()),
16077 other => Err(SidecarError::InvalidState(format!(
16078 "unsupported crypto cipher algorithm {other}"
16079 ))),
16080 }
16081}
16082
16083fn javascript_crypto_is_aead(algorithm: &str) -> bool {
16084 algorithm.to_ascii_lowercase().ends_with("-gcm")
16085}
16086
16087fn javascript_crypto_aead_tag_len(_algorithm: &str) -> usize {
16088 16
16089}
16090
16091fn javascript_crypto_openssl_error(error: openssl::error::ErrorStack) -> SidecarError {
16092 SidecarError::Execution(format!("crypto operation failed: {error}"))
16093}
16094
16095fn service_javascript_kernel_stdin_sync_rpc(
16096 kernel: &mut SidecarKernel,
16097 process: &mut ActiveProcess,
16098 request: &JavascriptSyncRpcRequest,
16099) -> Result<Value, SidecarError> {
16100 let max_bytes =
16101 javascript_sync_rpc_arg_u64_optional(&request.args, 0, "__kernel_stdin_read max bytes")?
16102 .map(|value| value.clamp(1, DEFAULT_KERNEL_STDIN_READ_MAX_BYTES as u64) as usize)
16103 .unwrap_or(DEFAULT_KERNEL_STDIN_READ_MAX_BYTES);
16104 let timeout_ms =
16105 javascript_sync_rpc_arg_u64_optional(&request.args, 1, "__kernel_stdin_read timeout ms")?
16106 .unwrap_or(DEFAULT_KERNEL_STDIN_READ_TIMEOUT_MS);
16107
16108 match kernel
16109 .fd_read_with_timeout_result(
16110 EXECUTION_DRIVER_NAME,
16111 process.kernel_pid,
16112 0,
16113 max_bytes,
16114 Some(Duration::from_millis(timeout_ms)),
16115 )
16116 .map_err(kernel_error)
16117 {
16118 Ok(Some(chunk)) if !chunk.is_empty() => Ok(json!({
16119 "dataBase64": base64::engine::general_purpose::STANDARD.encode(chunk),
16120 })),
16121 Ok(Some(_)) => Ok(Value::Null),
16122 Ok(None) => Ok(json!({
16123 "done": true,
16124 })),
16125 Err(SidecarError::Kernel(error)) if error.starts_with("EAGAIN:") => Ok(Value::Null),
16126 Err(error) => Err(error),
16127 }
16128}
16129
16130fn service_javascript_pty_set_raw_mode_sync_rpc(
16131 kernel: &mut SidecarKernel,
16132 process: &mut ActiveProcess,
16133 request: &JavascriptSyncRpcRequest,
16134) -> Result<Value, SidecarError> {
16135 let enabled = javascript_sync_rpc_arg_bool(&request.args, 0, "__pty_set_raw_mode enabled")?;
16136 kernel
16137 .pty_set_discipline(
16138 EXECUTION_DRIVER_NAME,
16139 process.kernel_pid,
16140 0,
16141 LineDisciplineConfig {
16142 canonical: Some(!enabled),
16143 echo: Some(!enabled),
16144 isig: Some(!enabled),
16145 },
16146 )
16147 .map_err(kernel_error)?;
16148 Ok(Value::Null)
16149}
16150
16151fn service_javascript_kernel_stdio_write_sync_rpc(
16152 kernel: &mut SidecarKernel,
16153 process: &mut ActiveProcess,
16154 request: &JavascriptSyncRpcRequest,
16155) -> Result<Value, SidecarError> {
16156 let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_stdio_write fd")?;
16157 let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "__kernel_stdio_write chunk")?;
16158
16159 let written = match fd {
16160 1 => kernel
16161 .write_process_stdout(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
16162 .map_err(kernel_error)?,
16163 2 => kernel
16164 .write_process_stderr(EXECUTION_DRIVER_NAME, process.kernel_pid, &chunk)
16165 .map_err(kernel_error)?,
16166 other => {
16167 return Err(SidecarError::InvalidState(format!(
16168 "__kernel_stdio_write only supports fd 1/2, got {other}"
16169 )));
16170 }
16171 };
16172
16173 let event = if fd == 1 {
16174 ActiveExecutionEvent::Stdout(chunk)
16175 } else {
16176 ActiveExecutionEvent::Stderr(chunk)
16177 };
16178 process.queue_pending_execution_event(event)?;
16179
16180 Ok(json!(written))
16181}
16182
16183fn service_javascript_kernel_poll_sync_rpc(
16184 kernel: &mut SidecarKernel,
16185 process: &ActiveProcess,
16186 request: &JavascriptSyncRpcRequest,
16187) -> Result<Value, SidecarError> {
16188 let fd_requests: Vec<KernelPollFdRequest> = serde_json::from_value(
16189 request
16190 .args
16191 .first()
16192 .cloned()
16193 .unwrap_or_else(|| Value::Array(Vec::new())),
16194 )
16195 .map_err(|error| {
16196 SidecarError::InvalidState(format!(
16197 "__kernel_poll fd list must be a JSON array of {{ fd, events }} objects: {error}"
16198 ))
16199 })?;
16200 let timeout_ms =
16201 javascript_sync_rpc_arg_u64_optional(&request.args, 1, "__kernel_poll timeout ms")?
16202 .unwrap_or_default();
16203 let timeout_ms = i32::try_from(timeout_ms).map_err(|_| {
16204 SidecarError::InvalidState(String::from("__kernel_poll timeout ms must fit within i32"))
16205 })?;
16206
16207 let poll_fds = fd_requests
16208 .iter()
16209 .map(|entry| PollFd {
16210 fd: entry.fd,
16211 events: PollEvents::from_bits(entry.events),
16212 revents: PollEvents::empty(),
16213 })
16214 .collect::<Vec<_>>();
16215 let result = kernel
16216 .poll_fds(
16217 EXECUTION_DRIVER_NAME,
16218 process.kernel_pid,
16219 poll_fds,
16220 timeout_ms,
16221 )
16222 .map_err(kernel_error)?;
16223
16224 Ok(json!({
16225 "readyCount": result.ready_count,
16226 "fds": result
16227 .fds
16228 .into_iter()
16229 .map(|entry| KernelPollFdResponse {
16230 fd: entry.fd,
16231 events: entry.events.bits(),
16232 revents: entry.revents.bits(),
16233 })
16234 .collect::<Vec<_>>(),
16235 }))
16236}
16237
16238fn install_kernel_stdin_pipe(kernel: &mut SidecarKernel, pid: u32) -> Result<u32, SidecarError> {
16239 let (read_fd, write_fd) = kernel
16240 .open_pipe(EXECUTION_DRIVER_NAME, pid)
16241 .map_err(kernel_error)?;
16242 kernel
16243 .fd_dup2(EXECUTION_DRIVER_NAME, pid, read_fd, 0)
16244 .map_err(kernel_error)?;
16245 kernel
16246 .fd_close(EXECUTION_DRIVER_NAME, pid, read_fd)
16247 .map_err(kernel_error)?;
16248 Ok(write_fd)
16249}
16250
16251fn javascript_child_process_stdin_mode(request: &JavascriptChildProcessSpawnRequest) -> &str {
16252 request
16253 .options
16254 .stdio
16255 .first()
16256 .map(String::as_str)
16257 .unwrap_or("pipe")
16258}
16259
16260pub(crate) fn write_kernel_process_stdin(
16261 kernel: &mut SidecarKernel,
16262 process: &mut ActiveProcess,
16263 chunk: &[u8],
16264) -> Result<(), SidecarError> {
16265 if process.runtime == GuestRuntimeKind::JavaScript {
16266 return Ok(());
16267 }
16268 let Some(writer_fd) = process.kernel_stdin_writer_fd else {
16269 return Ok(());
16270 };
16271 kernel
16272 .fd_write(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd, chunk)
16273 .map(|_| ())
16274 .map_err(kernel_error)
16275}
16276
16277pub(crate) fn close_kernel_process_stdin(
16278 kernel: &mut SidecarKernel,
16279 process: &mut ActiveProcess,
16280) -> Result<(), SidecarError> {
16281 let Some(writer_fd) = process.kernel_stdin_writer_fd.take() else {
16282 return Ok(());
16283 };
16284 kernel
16285 .fd_close(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd)
16286 .map_err(kernel_error)
16287}
16288
16289fn parse_http_header_collection(
16290 headers: &BTreeMap<String, Value>,
16291 label: &str,
16292) -> Result<HttpHeaderCollection, SidecarError> {
16293 let mut normalized = BTreeMap::<String, Vec<String>>::new();
16294 let mut raw_pairs = Vec::new();
16295
16296 for (raw_name, value) in headers {
16297 let normalized_name = raw_name.to_ascii_lowercase();
16298 let values = match value {
16299 Value::String(text) => vec![text.clone()],
16300 Value::Array(values) => values
16301 .iter()
16302 .map(|entry| {
16303 entry.as_str().map(str::to_owned).ok_or_else(|| {
16304 SidecarError::InvalidState(format!(
16305 "{label} header {raw_name} must contain only strings"
16306 ))
16307 })
16308 })
16309 .collect::<Result<Vec<_>, _>>()?,
16310 other => {
16311 return Err(SidecarError::InvalidState(format!(
16312 "{label} header {raw_name} must be a string or string array, received {other}"
16313 )));
16314 }
16315 };
16316 raw_pairs.extend(
16317 values
16318 .iter()
16319 .cloned()
16320 .map(|entry| (raw_name.clone(), entry)),
16321 );
16322 normalized
16323 .entry(normalized_name)
16324 .or_default()
16325 .extend(values);
16326 }
16327
16328 Ok(HttpHeaderCollection {
16329 normalized,
16330 raw_pairs,
16331 })
16332}
16333
16334fn http_headers_json(headers: &HttpHeaderCollection) -> Value {
16335 let map = headers
16336 .normalized
16337 .iter()
16338 .map(|(name, values)| {
16339 let value = if values.len() == 1 {
16340 Value::String(values[0].clone())
16341 } else {
16342 Value::Array(values.iter().cloned().map(Value::String).collect())
16343 };
16344 (name.clone(), value)
16345 })
16346 .collect::<Map<String, Value>>();
16347 Value::Object(map)
16348}
16349
16350fn http_raw_headers_json(headers: &HttpHeaderCollection) -> Value {
16351 Value::Array(
16352 headers
16353 .raw_pairs
16354 .iter()
16355 .flat_map(|(name, value)| [Value::String(name.clone()), Value::String(value.clone())])
16356 .collect(),
16357 )
16358}
16359
16360fn is_loopback_request_host(host: &str) -> bool {
16361 let bare = host
16362 .strip_prefix('[')
16363 .and_then(|value| value.strip_suffix(']'))
16364 .unwrap_or(host);
16365 matches!(bare, "localhost" | "127.0.0.1" | "::1")
16366}
16367
16368fn serialize_http_loopback_request(
16369 url: &Url,
16370 options: &JavascriptHttpRequestOptions,
16371 headers: &HttpHeaderCollection,
16372) -> Result<String, SidecarError> {
16373 let body_base64 = options
16374 .body
16375 .as_ref()
16376 .map(|body| base64::engine::general_purpose::STANDARD.encode(body.as_bytes()));
16377 serde_json::to_string(&json!({
16378 "method": options.method.clone().unwrap_or_else(|| String::from("GET")),
16379 "url": http_request_target(url),
16380 "headers": http_headers_json(headers),
16381 "rawHeaders": http_raw_headers_json(headers),
16382 "bodyBase64": body_base64,
16383 }))
16384 .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
16385}
16386
16387fn http_request_target(url: &Url) -> String {
16388 let path = if url.path().is_empty() {
16389 "/"
16390 } else {
16391 url.path()
16392 };
16393 format!(
16394 "{path}{}",
16395 url.query()
16396 .map(|query| format!("?{query}"))
16397 .unwrap_or_default()
16398 )
16399}
16400
16401fn find_kernel_http_listener_process(vm: &VmState, port: u16) -> Option<String> {
16402 vm.active_processes
16403 .iter()
16404 .find_map(|(process_id, process)| {
16405 process.tcp_listeners.values().find_map(|listener| {
16406 let socket_id = listener.kernel_socket_id?;
16407 let record = vm.kernel.socket_get(socket_id)?;
16408 let local_addr = record
16409 .local_address()
16410 .and_then(|address| resolve_tcp_bind_addr(address.host(), address.port()).ok())
16411 .unwrap_or_else(|| listener.guest_local_addr());
16412 if local_addr.port() == port && is_vm_local_http_listener_addr(local_addr.ip()) {
16413 Some(process_id.to_owned())
16414 } else {
16415 None
16416 }
16417 })
16418 })
16419}
16420
16421fn is_vm_local_http_listener_addr(ip: IpAddr) -> bool {
16422 ip.is_loopback() || ip.is_unspecified()
16423}
16424
16425fn serialize_kernel_http_fetch_request(
16426 port: u16,
16427 path: &str,
16428 options: &JavascriptHttpRequestOptions,
16429 headers: &HttpHeaderCollection,
16430) -> Vec<u8> {
16431 let method = options.method.as_deref().unwrap_or("GET");
16432 let mut lines = vec![format!("{method} {path} HTTP/1.1")];
16433 let mut has_host = false;
16434 let mut has_connection = false;
16435 let mut has_content_length = false;
16436 for (name, values) in &headers.normalized {
16437 match name.as_str() {
16438 "host" => has_host = true,
16439 "connection" => has_connection = true,
16440 "content-length" => has_content_length = true,
16441 _ => {}
16442 }
16443 lines.push(format!("{name}: {}", values.join(", ")));
16444 }
16445 if !has_host {
16446 lines.push(format!("Host: 127.0.0.1:{port}"));
16447 }
16448 if !has_connection {
16449 lines.push(String::from("Connection: close"));
16450 }
16451 let body = options.body.as_deref().unwrap_or("").as_bytes();
16452 if !has_content_length && !body.is_empty() {
16453 lines.push(format!("Content-Length: {}", body.len()));
16454 }
16455 lines.push(String::new());
16456 lines.push(String::new());
16457
16458 let mut request = lines.join("\r\n").into_bytes();
16459 request.extend_from_slice(body);
16460 request
16461}
16462
16463fn parse_kernel_http_fetch_response(
16464 buffer: &[u8],
16465 peer_closed: bool,
16466 url: &str,
16467) -> Result<Option<String>, SidecarError> {
16468 let Some(header_end) = find_http_header_end(buffer) else {
16469 return Ok(None);
16470 };
16471 let header_bytes = &buffer[..header_end];
16472 let head = String::from_utf8_lossy(header_bytes);
16473 let mut lines = head.split("\r\n");
16474 let status_line = lines.next().unwrap_or_default();
16475 let mut status_parts = status_line.splitn(3, ' ');
16476 let version = status_parts.next().unwrap_or_default();
16477 if !version.starts_with("HTTP/") {
16478 return Err(SidecarError::Execution(format!(
16479 "invalid vm.fetch HTTP response status line: {status_line}"
16480 )));
16481 }
16482 let status = status_parts
16483 .next()
16484 .ok_or_else(|| {
16485 SidecarError::Execution(format!(
16486 "invalid vm.fetch HTTP response status line: {status_line}"
16487 ))
16488 })?
16489 .parse::<u16>()
16490 .map_err(|error| {
16491 SidecarError::Execution(format!(
16492 "invalid vm.fetch HTTP response status code in {status_line:?}: {error}"
16493 ))
16494 })?;
16495 let status_text = status_parts.next().unwrap_or_default();
16496 let mut headers = Vec::new();
16497 let mut raw_headers = Vec::new();
16498 let mut content_length = None;
16499 let mut transfer_encoding_values = Vec::new();
16500 for line in lines {
16501 if line.is_empty() {
16502 continue;
16503 }
16504 let Some((name, value)) = line.split_once(':') else {
16505 return Err(SidecarError::Execution(format!(
16506 "invalid vm.fetch HTTP response header line: {line}"
16507 )));
16508 };
16509 let value = value.trim().to_owned();
16510 let normalized = name.to_ascii_lowercase();
16511 if normalized == "content-length" {
16512 content_length = Some(value.parse::<usize>().map_err(|error| {
16513 SidecarError::Execution(format!(
16514 "invalid vm.fetch Content-Length header {value:?}: {error}"
16515 ))
16516 })?);
16517 } else if normalized == "transfer-encoding" {
16518 transfer_encoding_values.push(value.clone());
16519 }
16520 headers.push(json!([normalized, value.clone()]));
16521 raw_headers.push(Value::String(name.to_owned()));
16522 raw_headers.push(Value::String(value));
16523 }
16524
16525 let body_start = header_end + 4;
16526 let transfer_encoding = transfer_encoding_tokens(&transfer_encoding_values);
16527 let is_chunked = transfer_encoding.iter().any(|token| token == "chunked");
16528 let body = if is_chunked {
16529 if content_length.is_some() {
16530 return Err(SidecarError::Execution(String::from(
16531 "vm.fetch HTTP response cannot include both Transfer-Encoding: chunked and Content-Length",
16532 )));
16533 }
16534 if transfer_encoding.len() != 1 {
16535 return Err(SidecarError::Execution(format!(
16536 "unsupported vm.fetch Transfer-Encoding: {}",
16537 transfer_encoding.join(", ")
16538 )));
16539 }
16540 let Some(decoded) = decode_kernel_http_chunked_body(&buffer[body_start..])? else {
16541 return Ok(None);
16542 };
16543 decoded
16544 } else if !transfer_encoding.is_empty() {
16545 return Err(SidecarError::Execution(format!(
16546 "unsupported vm.fetch Transfer-Encoding: {}",
16547 transfer_encoding.join(", ")
16548 )));
16549 } else if let Some(content_length) = content_length {
16550 let body_end = body_start.saturating_add(content_length);
16551 if buffer.len() < body_end {
16552 return Ok(None);
16553 }
16554 buffer[body_start..body_end].to_vec()
16555 } else if peer_closed {
16556 buffer[body_start..].to_vec()
16557 } else {
16558 return Ok(None);
16559 };
16560
16561 serde_json::to_string(&json!({
16562 "status": status,
16563 "statusText": status_text,
16564 "headers": headers,
16565 "rawHeaders": raw_headers,
16566 "body": base64::engine::general_purpose::STANDARD.encode(&body),
16567 "bodyEncoding": "base64",
16568 "url": url,
16569 }))
16570 .map(Some)
16571 .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
16572}
16573
16574fn find_http_header_end(buffer: &[u8]) -> Option<usize> {
16575 buffer.windows(4).position(|window| window == b"\r\n\r\n")
16576}
16577
16578fn find_crlf(buffer: &[u8], start: usize) -> Option<usize> {
16579 buffer
16580 .get(start..)?
16581 .windows(2)
16582 .position(|window| window == b"\r\n")
16583 .map(|offset| start + offset)
16584}
16585
16586fn transfer_encoding_tokens(values: &[String]) -> Vec<String> {
16587 values
16588 .iter()
16589 .flat_map(|value| value.split(','))
16590 .map(|token| token.trim().to_ascii_lowercase())
16591 .filter(|token| !token.is_empty())
16592 .collect()
16593}
16594
16595fn decode_kernel_http_chunked_body(buffer: &[u8]) -> Result<Option<Vec<u8>>, SidecarError> {
16596 let mut offset = 0;
16597 let mut body = Vec::new();
16598 loop {
16599 let Some(line_end) = find_crlf(buffer, offset) else {
16600 return Ok(None);
16601 };
16602 let size_line = std::str::from_utf8(&buffer[offset..line_end]).map_err(|error| {
16603 SidecarError::Execution(format!(
16604 "invalid vm.fetch chunk size line encoding: {error}"
16605 ))
16606 })?;
16607 let size_part = size_line.split(';').next().unwrap_or_default();
16608 if size_part.is_empty() || !size_part.bytes().all(|byte| byte.is_ascii_hexdigit()) {
16609 return Err(SidecarError::Execution(format!(
16610 "invalid vm.fetch chunk size line: {size_line:?}"
16611 )));
16612 }
16613 let chunk_size = usize::from_str_radix(size_part, 16).map_err(|error| {
16614 SidecarError::Execution(format!(
16615 "invalid vm.fetch chunk size {size_part:?}: {error}"
16616 ))
16617 })?;
16618 let chunk_start = line_end + 2;
16619 let chunk_end = chunk_start
16620 .checked_add(chunk_size)
16621 .ok_or_else(|| SidecarError::Execution(String::from("vm.fetch chunk size overflow")))?;
16622 if chunk_size > 0 {
16623 let chunk_terminator_end = chunk_end.checked_add(2).ok_or_else(|| {
16624 SidecarError::Execution(String::from("vm.fetch chunk terminator overflow"))
16625 })?;
16626 if chunk_terminator_end > buffer.len() {
16627 return Ok(None);
16628 }
16629 if buffer.get(chunk_end..chunk_terminator_end) != Some(b"\r\n") {
16630 return Err(SidecarError::Execution(String::from(
16631 "invalid vm.fetch chunk terminator",
16632 )));
16633 }
16634 body.extend_from_slice(&buffer[chunk_start..chunk_end]);
16635 offset = chunk_terminator_end;
16636 continue;
16637 }
16638
16639 if buffer.get(chunk_start..chunk_start + 2) == Some(b"\r\n") {
16640 return Ok(Some(body));
16641 }
16642 let Some(trailer_end) = find_http_header_end(&buffer[chunk_start..]) else {
16643 return Ok(None);
16644 };
16645 let trailer_bytes = &buffer[chunk_start..chunk_start + trailer_end];
16646 let trailers = String::from_utf8_lossy(trailer_bytes);
16647 for line in trailers.split("\r\n") {
16648 if line.is_empty() {
16649 continue;
16650 }
16651 if line.starts_with(' ') || line.starts_with('\t') || !line.contains(':') {
16652 return Err(SidecarError::Execution(format!(
16653 "invalid vm.fetch chunk trailer line: {line}"
16654 )));
16655 }
16656 }
16657 return Ok(Some(body));
16658 }
16659}
16660
16661fn kernel_http_fetch_target_exit_code(error: &SidecarError) -> Option<i32> {
16662 let SidecarError::Execution(message) = error else {
16663 return None;
16664 };
16665 message
16666 .strip_prefix("vm.fetch target exited before responding (exit code ")?
16667 .strip_suffix(')')?
16668 .parse()
16669 .ok()
16670}
16671
16672#[allow(clippy::too_many_arguments)]
16673fn service_host_fetch_target_event<B>(
16674 bridge: &SharedBridge<B>,
16675 vm_id: &str,
16676 dns: &VmDnsConfig,
16677 socket_paths: &JavascriptSocketPathContext,
16678 kernel: &mut SidecarKernel,
16679 process: &mut ActiveProcess,
16680 resource_limits: &ResourceLimits,
16681 wait: Duration,
16682) -> Result<bool, SidecarError>
16683where
16684 B: NativeSidecarBridge + Send + 'static,
16685 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16686{
16687 let Some(event) = process
16688 .execution
16689 .poll_event_blocking(wait)
16690 .map_err(|error| SidecarError::Execution(error.to_string()))?
16691 else {
16692 return Ok(false);
16693 };
16694
16695 match event {
16696 ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
16697 let network_counts = process.network_resource_counts();
16698 let response = service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
16699 bridge,
16700 vm_id,
16701 dns,
16702 socket_paths,
16703 kernel,
16704 process,
16705 sync_request: &request,
16706 resource_limits,
16707 network_counts,
16708 });
16709 match response {
16710 Ok(result) => process
16711 .execution
16712 .respond_javascript_sync_rpc_success(request.id, result)
16713 .or_else(ignore_stale_javascript_sync_rpc_response)?,
16714 Err(error) => process
16715 .execution
16716 .respond_javascript_sync_rpc_error(
16717 request.id,
16718 javascript_sync_rpc_error_code(&error),
16719 error.to_string(),
16720 )
16721 .or_else(ignore_stale_javascript_sync_rpc_response)?,
16722 }
16723 }
16724 ActiveExecutionEvent::Exited(code) => {
16725 return Err(SidecarError::Execution(format!(
16726 "vm.fetch target exited before responding (exit code {code})"
16727 )));
16728 }
16729 other => {
16730 process.queue_pending_execution_event(other)?;
16731 }
16732 }
16733 Ok(true)
16734}
16735
16736fn drain_host_fetch_target_events<B>(
16737 bridge: &SharedBridge<B>,
16738 vm_id: &str,
16739 vm: &mut VmState,
16740 target_process_id: &str,
16741 socket_paths: &JavascriptSocketPathContext,
16742 resource_limits: &ResourceLimits,
16743) -> Result<(), SidecarError>
16744where
16745 B: NativeSidecarBridge + Send + 'static,
16746 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16747{
16748 for _ in 0..32 {
16749 let dns = vm.dns.clone();
16750 let Some(process) = vm.active_processes.get_mut(target_process_id) else {
16751 break;
16752 };
16753 let serviced = service_host_fetch_target_event(
16754 bridge,
16755 vm_id,
16756 &dns,
16757 socket_paths,
16758 &mut vm.kernel,
16759 process,
16760 resource_limits,
16761 Duration::from_millis(1),
16762 )?;
16763 if !serviced {
16764 break;
16765 }
16766 }
16767 Ok(())
16768}
16769
16770#[allow(clippy::too_many_arguments)]
16771fn dispatch_kernel_http_fetch<B>(
16772 bridge: &SharedBridge<B>,
16773 vm_id: &str,
16774 vm: &mut VmState,
16775 target_process_id: &str,
16776 port: u16,
16777 path: &str,
16778 options: &JavascriptHttpRequestOptions,
16779 headers: &HttpHeaderCollection,
16780 max_fetch_response_bytes: usize,
16781) -> Result<String, SidecarError>
16782where
16783 B: NativeSidecarBridge + Send + 'static,
16784 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16785{
16786 let socket_paths = build_javascript_socket_path_context(vm)?;
16787 let family = JavascriptSocketFamily::Ipv4;
16788 let local_port = allocate_guest_listen_port(
16789 0,
16790 family,
16791 &socket_paths.used_tcp_guest_ports,
16792 socket_paths.listen_policy,
16793 )?;
16794 let resource_limits = vm.kernel.resource_limits().clone();
16795 let network_counts = vm_network_resource_counts(vm);
16796 check_network_resource_limit(
16797 resource_limits.max_sockets,
16798 network_counts.sockets,
16799 2,
16800 "socket",
16801 )?;
16802 check_network_resource_limit(
16803 resource_limits.max_connections,
16804 network_counts.connections,
16805 2,
16806 "connection",
16807 )?;
16808
16809 let kernel_pid = vm
16810 .active_processes
16811 .get(target_process_id)
16812 .ok_or_else(|| {
16813 SidecarError::InvalidState(format!(
16814 "vm.fetch target process disappeared: {target_process_id}"
16815 ))
16816 })?
16817 .kernel_pid;
16818 let socket_id = vm
16819 .kernel
16820 .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, SocketSpec::tcp())
16821 .map_err(kernel_error)?;
16822
16823 let result = dispatch_kernel_http_fetch_with_socket(
16824 bridge,
16825 vm_id,
16826 vm,
16827 target_process_id,
16828 kernel_pid,
16829 socket_id,
16830 local_port,
16831 port,
16832 path,
16833 options,
16834 headers,
16835 &socket_paths,
16836 &resource_limits,
16837 max_fetch_response_bytes,
16838 );
16839 let close_result = vm
16840 .kernel
16841 .socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id)
16842 .map_err(kernel_error);
16843 let cleanup_result = if result.is_err() {
16844 drain_host_fetch_target_events(
16845 bridge,
16846 vm_id,
16847 vm,
16848 target_process_id,
16849 &socket_paths,
16850 &resource_limits,
16851 )
16852 } else {
16853 Ok(())
16854 };
16855 match (result, close_result) {
16856 (Ok(response), Ok(())) => cleanup_result.map(|()| response),
16857 (Err(error), _) => Err(error),
16858 (Ok(_), Err(error)) => Err(error),
16859 }
16860}
16861
16862#[allow(clippy::too_many_arguments)]
16863fn dispatch_kernel_http_fetch_with_socket<B>(
16864 bridge: &SharedBridge<B>,
16865 vm_id: &str,
16866 vm: &mut VmState,
16867 target_process_id: &str,
16868 kernel_pid: u32,
16869 socket_id: SocketId,
16870 local_port: u16,
16871 port: u16,
16872 path: &str,
16873 options: &JavascriptHttpRequestOptions,
16874 headers: &HttpHeaderCollection,
16875 socket_paths: &JavascriptSocketPathContext,
16876 resource_limits: &ResourceLimits,
16877 max_fetch_response_bytes: usize,
16878) -> Result<String, SidecarError>
16879where
16880 B: NativeSidecarBridge + Send + 'static,
16881 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
16882{
16883 vm.kernel
16884 .socket_bind_inet(
16885 EXECUTION_DRIVER_NAME,
16886 kernel_pid,
16887 socket_id,
16888 InetSocketAddress::new("127.0.0.1", local_port),
16889 )
16890 .map_err(kernel_error)?;
16891 vm.kernel
16892 .socket_connect_inet_loopback(
16893 EXECUTION_DRIVER_NAME,
16894 kernel_pid,
16895 socket_id,
16896 InetSocketAddress::new("127.0.0.1", port),
16897 )
16898 .map_err(kernel_error)?;
16899
16900 let request_bytes = serialize_kernel_http_fetch_request(port, path, options, headers);
16901 vm.kernel
16902 .socket_write(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, &request_bytes)
16903 .map_err(kernel_error)?;
16904
16905 let mut response_buffer = Vec::new();
16906 let mut peer_closed = false;
16907 let url = format!("http://127.0.0.1:{port}{path}");
16908 let deadline = Instant::now() + http_loopback_request_timeout();
16909 loop {
16910 if let Some(response) =
16911 parse_kernel_http_fetch_response(&response_buffer, peer_closed, &url)?
16912 {
16913 ensure_vm_fetch_response_within_limit(&response, "vm.fetch", max_fetch_response_bytes)?;
16914 return Ok(response);
16915 }
16916 if Instant::now() >= deadline {
16917 let preview = String::from_utf8_lossy(&response_buffer);
16918 return Err(SidecarError::Execution(format!(
16919 "vm.fetch timed out waiting for kernel TCP HTTP response ({} buffered bytes: {:?})",
16920 response_buffer.len(),
16921 preview.chars().take(200).collect::<String>()
16922 )));
16923 }
16924
16925 {
16926 let dns = vm.dns.clone();
16927 let process = vm
16928 .active_processes
16929 .get_mut(target_process_id)
16930 .ok_or_else(|| {
16931 SidecarError::InvalidState(format!(
16932 "vm.fetch target process disappeared: {target_process_id}"
16933 ))
16934 })?;
16935 service_host_fetch_target_event(
16936 bridge,
16937 vm_id,
16938 &dns,
16939 socket_paths,
16940 &mut vm.kernel,
16941 process,
16942 resource_limits,
16943 Duration::from_millis(5),
16944 )?;
16945 }
16946
16947 let poll = vm
16948 .kernel
16949 .poll_targets(
16950 EXECUTION_DRIVER_NAME,
16951 kernel_pid,
16952 vec![PollTargetEntry::socket(
16953 socket_id,
16954 POLLIN | POLLHUP | POLLERR,
16955 )],
16956 5,
16957 )
16958 .map_err(kernel_error)?;
16959 let revents = poll
16960 .targets
16961 .first()
16962 .map(|entry| entry.revents)
16963 .unwrap_or_else(PollEvents::empty);
16964 if revents.intersects(POLLERR) {
16965 return Err(SidecarError::Execution(String::from(
16966 "vm.fetch kernel TCP socket reported POLLERR",
16967 )));
16968 }
16969 if revents.intersects(POLLIN) {
16970 match vm
16971 .kernel
16972 .socket_read(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, 64 * 1024)
16973 {
16974 Ok(Some(bytes)) if !bytes.is_empty() => {
16975 response_buffer.extend(bytes);
16976 ensure_vm_fetch_raw_response_buffer_within_limit(
16977 response_buffer.len(),
16978 "vm.fetch",
16979 )?;
16980 }
16981 Ok(Some(_)) => {}
16982 Ok(None) => peer_closed = true,
16983 Err(error) if error.code() == "EAGAIN" => {}
16984 Err(error) => return Err(kernel_error(error)),
16985 }
16986 }
16987 if revents.intersects(POLLHUP) {
16988 peer_closed = true;
16989 }
16990 }
16991}
16992
16993fn outbound_http_response_json(url: &Url, response: ureq::Response) -> Result<Value, SidecarError> {
16994 let status = response.status();
16995 let status_text = response.status_text().to_owned();
16996 let mut header_pairs = Vec::new();
16997 let mut raw_headers = Vec::new();
16998 for raw_name in response.headers_names() {
16999 for value in response.all(&raw_name) {
17000 header_pairs.push(json!([raw_name.to_ascii_lowercase(), value]));
17001 raw_headers.push(Value::String(raw_name.clone()));
17002 raw_headers.push(Value::String(value.to_owned()));
17003 }
17004 }
17005 let mut reader = response.into_reader();
17006 let mut body = Vec::new();
17007 reader.read_to_end(&mut body).map_err(|error| {
17008 SidecarError::Execution(format!("failed to read HTTP response: {error}"))
17009 })?;
17010 serde_json::to_string(&json!({
17011 "status": status,
17012 "statusText": status_text,
17013 "headers": header_pairs,
17014 "rawHeaders": raw_headers,
17015 "body": base64::engine::general_purpose::STANDARD.encode(body),
17016 "bodyEncoding": "base64",
17017 "url": url.as_str(),
17018 }))
17019 .map(Value::String)
17020 .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
17021}
17022
17023fn split_netloc(netloc: &str) -> Option<(&str, u16)> {
17027 let (host, port) = netloc.rsplit_once(':')?;
17028 let port: u16 = port.parse().ok()?;
17029 let host = host
17030 .strip_prefix('[')
17031 .and_then(|rest| rest.strip_suffix(']'))
17032 .unwrap_or(host);
17033 Some((host, port))
17034}
17035
17036fn issue_outbound_http_request(
17037 url: &Url,
17038 options: &JavascriptHttpRequestOptions,
17039 headers: &HttpHeaderCollection,
17040 pinned_addresses: &[IpAddr],
17041) -> Result<Value, SidecarError> {
17042 let method = options.method.as_deref().unwrap_or("GET");
17043 let pinned_host = url.host_str().map(str::to_owned);
17052 let pinned: Vec<IpAddr> = pinned_addresses.to_vec();
17053 let resolver = move |netloc: &str| -> std::io::Result<Vec<SocketAddr>> {
17054 let (host, port) = split_netloc(netloc).ok_or_else(|| {
17055 std::io::Error::new(
17056 std::io::ErrorKind::InvalidInput,
17057 format!("invalid network location: {netloc}"),
17058 )
17059 })?;
17060 let expected_host = pinned_host.as_deref();
17061 if expected_host != Some(host) {
17062 return Err(std::io::Error::new(
17063 std::io::ErrorKind::PermissionDenied,
17064 format!(
17065 "EACCES: outbound HTTP resolver pinned to {expected_host:?}, refusing {host}"
17066 ),
17067 ));
17068 }
17069 if pinned.is_empty() {
17070 return Err(std::io::Error::new(
17071 std::io::ErrorKind::PermissionDenied,
17072 "EACCES: no egress-vetted address available for outbound HTTP request",
17073 ));
17074 }
17075 Ok(pinned.iter().map(|ip| SocketAddr::new(*ip, port)).collect())
17076 };
17077 let mut agent_builder = ureq::AgentBuilder::new()
17078 .resolver(resolver)
17079 .timeout_connect(Duration::from_secs(5))
17080 .timeout_read(Duration::from_secs(15))
17081 .timeout_write(Duration::from_secs(15));
17082 if url.scheme() == "https" {
17083 let tls_options = JavascriptTlsBridgeOptions {
17084 is_server: false,
17085 servername: url.host_str().map(str::to_owned),
17086 alpn_protocols: Some(vec![String::from("http/1.1")]),
17087 reject_unauthorized: options.reject_unauthorized,
17088 ..JavascriptTlsBridgeOptions::default()
17089 };
17090 agent_builder = agent_builder.tls_config(Arc::new(build_client_tls_config(&tls_options)?));
17091 }
17092 let agent = agent_builder.build();
17093 let mut request = agent.request_url(method, url);
17094 for (name, values) in &headers.normalized {
17095 if name == "host" {
17096 continue;
17097 }
17098 let header_value = values.join(", ");
17099 request = request.set(name, &header_value);
17100 }
17101 let response = match options.body.as_deref() {
17102 Some(body) => request.send_string(body),
17103 None => request.call(),
17104 };
17105
17106 match response {
17107 Ok(response) => outbound_http_response_json(url, response),
17108 Err(ureq::Error::Status(_, response)) => outbound_http_response_json(url, response),
17109 Err(ureq::Error::Transport(error)) => Err(SidecarError::Execution(format!(
17110 "ERR_HTTP_REQUEST_FAILED: {error}"
17111 ))),
17112 }
17113}
17114
17115fn wait_for_loopback_http_response<B>(
17116 request: LoopbackHttpResponseWaitRequest<'_, B>,
17117) -> Result<String, SidecarError>
17118where
17119 B: NativeSidecarBridge + Send + 'static,
17120 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17121{
17122 let LoopbackHttpResponseWaitRequest {
17123 bridge,
17124 vm_id,
17125 dns,
17126 socket_paths,
17127 kernel,
17128 process,
17129 resource_limits,
17130 request_key,
17131 } = request;
17132 let deadline = Instant::now() + http_loopback_request_timeout();
17133 loop {
17134 if let Some(response) = process
17135 .pending_http_requests
17136 .get(&request_key)
17137 .and_then(|response| response.clone())
17138 {
17139 process.pending_http_requests.remove(&request_key);
17140 return Ok(response);
17141 }
17142
17143 if Instant::now() >= deadline {
17144 process.pending_http_requests.remove(&request_key);
17145 return Err(SidecarError::Execution(String::from(
17146 "HTTP loopback request timed out waiting for net.http_respond",
17147 )));
17148 }
17149
17150 let Some(event) = process
17151 .execution
17152 .poll_event_blocking(Duration::from_millis(10))
17153 .map_err(|error| SidecarError::Execution(error.to_string()))?
17154 else {
17155 continue;
17156 };
17157
17158 match event {
17159 ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => {
17160 let network_counts = process.network_resource_counts();
17161 let response = service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest {
17162 bridge,
17163 vm_id,
17164 dns,
17165 socket_paths,
17166 kernel,
17167 process,
17168 sync_request: &request,
17169 resource_limits,
17170 network_counts,
17171 });
17172 match response {
17173 Ok(result) => process
17174 .execution
17175 .respond_javascript_sync_rpc_success(request.id, result)
17176 .or_else(ignore_stale_javascript_sync_rpc_response)?,
17177 Err(error) => process
17178 .execution
17179 .respond_javascript_sync_rpc_error(
17180 request.id,
17181 javascript_sync_rpc_error_code(&error),
17182 error.to_string(),
17183 )
17184 .or_else(ignore_stale_javascript_sync_rpc_response)?,
17185 }
17186 }
17187 ActiveExecutionEvent::Exited(code) => {
17188 process.pending_http_requests.remove(&request_key);
17189 return Err(SidecarError::Execution(format!(
17190 "HTTP loopback server exited before responding (exit code {code})"
17191 )));
17192 }
17193 ActiveExecutionEvent::Stdout(_)
17194 | ActiveExecutionEvent::Stderr(_)
17195 | ActiveExecutionEvent::PythonVfsRpcRequest(_)
17196 | ActiveExecutionEvent::SignalState { .. } => {}
17197 }
17198 }
17199}
17200
17201pub(crate) fn dispatch_loopback_http_request<B>(
17202 request: LoopbackHttpDispatchRequest<'_, B>,
17203) -> Result<String, SidecarError>
17204where
17205 B: NativeSidecarBridge + Send + 'static,
17206 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17207{
17208 let LoopbackHttpDispatchRequest {
17209 bridge,
17210 vm_id,
17211 dns,
17212 socket_paths,
17213 kernel,
17214 process,
17215 resource_limits,
17216 server_id,
17217 request_json,
17218 } = request;
17219 let request_id = {
17220 let server = process.http_servers.get_mut(&server_id).ok_or_else(|| {
17221 SidecarError::InvalidState(format!("HTTP target server disappeared: {server_id}"))
17222 })?;
17223 server.next_request_id += 1;
17224 server.next_request_id
17225 };
17226 process
17227 .pending_http_requests
17228 .insert((server_id, request_id), None);
17229 process.execution.send_javascript_stream_event(
17230 "http_request",
17231 json!({
17232 "serverId": server_id,
17233 "requestId": request_id,
17234 "request": request_json,
17235 }),
17236 )?;
17237 wait_for_loopback_http_response(LoopbackHttpResponseWaitRequest {
17238 bridge,
17239 vm_id,
17240 dns,
17241 socket_paths,
17242 kernel,
17243 process,
17244 resource_limits,
17245 request_key: (server_id, request_id),
17246 })
17247}
17248
17249fn ensure_vm_fetch_response_within_limit(
17250 response_json: &str,
17251 operation: &str,
17252 limit: usize,
17253) -> Result<(), SidecarError> {
17254 let size = response_json.len();
17255 if size > limit {
17256 return Err(SidecarError::Execution(format!(
17257 "{operation} payload is {size} bytes, limit is {limit}"
17258 )));
17259 }
17260 Ok(())
17261}
17262
17263fn ensure_vm_fetch_raw_response_buffer_within_limit(
17264 size: usize,
17265 operation: &str,
17266) -> Result<(), SidecarError> {
17267 if size > VM_FETCH_BUFFER_LIMIT_BYTES {
17268 return Err(SidecarError::Execution(format!(
17269 "{operation} raw response buffer is {size} bytes, limit is {VM_FETCH_BUFFER_LIMIT_BYTES}"
17270 )));
17271 }
17272 Ok(())
17273}
17274
17275pub(crate) fn ensure_vm_fetch_response_frame_within_limit(
17276 response: &ResponseFrame,
17277 max_frame_bytes: usize,
17278) -> Result<(), SidecarError> {
17279 let max_frame_bytes = max_frame_bytes.min(VM_FETCH_BUFFER_LIMIT_BYTES);
17280 let frame = crate::protocol::to_generated_protocol_frame(
17281 &crate::protocol::ProtocolFrame::Response(response.clone()),
17282 )
17283 .map_err(|error| SidecarError::FrameTooLarge(error.to_string()))?;
17284 let WireProtocolFrame::ResponseFrame(_) = &frame else {
17285 return Err(SidecarError::FrameTooLarge(String::from(
17286 "vm fetch response converted to non-response wire frame",
17287 )));
17288 };
17289 WireFrameCodec::new(max_frame_bytes)
17290 .encode(&frame)
17291 .map(|_| ())
17292 .map_err(|error| SidecarError::FrameTooLarge(error.to_string()))
17293}
17294
17295fn service_javascript_dns_sync_rpc<B>(
17296 bridge: &SharedBridge<B>,
17297 kernel: &SidecarKernel,
17298 vm_id: &str,
17299 dns: &VmDnsConfig,
17300 request: &JavascriptSyncRpcRequest,
17301) -> Result<Value, SidecarError>
17302where
17303 B: NativeSidecarBridge + Send + 'static,
17304 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17305{
17306 match request.method.as_str() {
17307 "dns.lookup" => {
17308 let payload = request
17309 .args
17310 .first()
17311 .cloned()
17312 .ok_or_else(|| {
17313 SidecarError::InvalidState(String::from(
17314 "dns.lookup requires a request payload",
17315 ))
17316 })
17317 .and_then(|value| {
17318 serde_json::from_value::<JavascriptDnsLookupRequest>(value).map_err(|error| {
17319 SidecarError::InvalidState(format!("invalid dns.lookup payload: {error}"))
17320 })
17321 })?;
17322 let addresses = filter_dns_ip_addrs(
17323 resolve_dns_ip_addrs(
17324 bridge,
17325 kernel,
17326 vm_id,
17327 dns,
17328 &payload.hostname,
17329 DnsLookupPolicy::CheckPermissions,
17330 )?,
17331 payload.family,
17332 )?;
17333 let addresses = filter_dns_safe_ip_addrs(addresses, &payload.hostname)?;
17334 Ok(Value::Array(
17335 addresses
17336 .into_iter()
17337 .map(|ip| {
17338 json!({
17339 "address": ip.to_string(),
17340 "family": if ip.is_ipv6() { 6 } else { 4 },
17341 })
17342 })
17343 .collect(),
17344 ))
17345 }
17346 "dns.resolve" | "dns.resolve4" | "dns.resolve6" => {
17347 let payload = request
17348 .args
17349 .first()
17350 .cloned()
17351 .ok_or_else(|| {
17352 SidecarError::InvalidState(String::from(
17353 "dns.resolve requires a request payload",
17354 ))
17355 })
17356 .and_then(|value| {
17357 serde_json::from_value::<JavascriptDnsResolveRequest>(value).map_err(|error| {
17358 SidecarError::InvalidState(format!("invalid dns.resolve payload: {error}"))
17359 })
17360 })?;
17361 let requested_type = match request.method.as_str() {
17362 "dns.resolve4" => String::from("A"),
17363 "dns.resolve6" => String::from("AAAA"),
17364 _ => payload
17365 .rrtype
17366 .as_deref()
17367 .unwrap_or("A")
17368 .to_ascii_uppercase(),
17369 };
17370 let record_type = parse_dns_record_type(&requested_type)?;
17371 let resolution = resolve_dns_records(
17372 bridge,
17373 kernel,
17374 vm_id,
17375 dns,
17376 &payload.hostname,
17377 record_type,
17378 DnsLookupPolicy::CheckPermissions,
17379 )?;
17380 dns_resolution_to_node_value(&resolution, &requested_type)
17381 }
17382 other => Err(SidecarError::InvalidState(format!(
17383 "unsupported JavaScript dns sync RPC method {other}"
17384 ))),
17385 }
17386}
17387
17388fn service_javascript_dgram_sync_rpc<B>(
17389 request: JavascriptDgramSyncRpcServiceRequest<'_, B>,
17390) -> Result<Value, SidecarError>
17391where
17392 B: NativeSidecarBridge + Send + 'static,
17393 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
17394{
17395 let JavascriptDgramSyncRpcServiceRequest {
17396 bridge,
17397 kernel,
17398 vm_id,
17399 dns,
17400 socket_paths,
17401 process,
17402 sync_request: request,
17403 resource_limits,
17404 network_counts,
17405 } = request;
17406 match request.method.as_str() {
17407 "dgram.createSocket" => {
17408 check_network_resource_limit(
17409 resource_limits.max_sockets,
17410 network_counts.sockets,
17411 1,
17412 "socket",
17413 )?;
17414 let payload = request
17415 .args
17416 .first()
17417 .cloned()
17418 .ok_or_else(|| {
17419 SidecarError::InvalidState(String::from(
17420 "dgram.createSocket requires a request payload",
17421 ))
17422 })
17423 .and_then(|value| {
17424 serde_json::from_value::<JavascriptDgramCreateSocketRequest>(value).map_err(
17425 |error| {
17426 SidecarError::InvalidState(format!(
17427 "invalid dgram.createSocket payload: {error}"
17428 ))
17429 },
17430 )
17431 })?;
17432 let family = JavascriptUdpFamily::from_socket_type(&payload.socket_type)?;
17433 let socket_id = process.allocate_udp_socket_id();
17434 process.udp_sockets.insert(
17435 socket_id.clone(),
17436 ActiveUdpSocket::new(kernel, process.kernel_pid, family)?,
17437 );
17438 Ok(json!({
17439 "socketId": socket_id,
17440 "type": family.socket_type(),
17441 }))
17442 }
17443 "dgram.bind" => {
17444 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.bind socket id")?;
17445 let payload = request
17446 .args
17447 .get(1)
17448 .cloned()
17449 .ok_or_else(|| {
17450 SidecarError::InvalidState(String::from(
17451 "dgram.bind requires a request payload",
17452 ))
17453 })
17454 .and_then(|value| {
17455 serde_json::from_value::<JavascriptDgramBindRequest>(value).map_err(|error| {
17456 SidecarError::InvalidState(format!("invalid dgram.bind payload: {error}"))
17457 })
17458 })?;
17459 let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
17460 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17461 })?;
17462 let local_addr = socket.bind(
17463 kernel,
17464 process.kernel_pid,
17465 payload.address.as_deref(),
17466 payload.port,
17467 socket_paths,
17468 )?;
17469 Ok(json!({
17470 "localAddress": local_addr.ip().to_string(),
17471 "localPort": local_addr.port(),
17472 "family": socket_addr_family(&local_addr),
17473 }))
17474 }
17475 "dgram.send" => {
17476 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.send socket id")?;
17477 let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "dgram.send payload")?;
17478 let payload = request
17479 .args
17480 .get(2)
17481 .cloned()
17482 .ok_or_else(|| {
17483 SidecarError::InvalidState(String::from(
17484 "dgram.send requires a request payload",
17485 ))
17486 })
17487 .and_then(|value| {
17488 serde_json::from_value::<JavascriptDgramSendRequest>(value).map_err(|error| {
17489 SidecarError::InvalidState(format!("invalid dgram.send payload: {error}"))
17490 })
17491 })?;
17492 let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
17493 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17494 })?;
17495 let (written, local_addr) = socket.send_to(ActiveUdpSendToRequest {
17496 bridge,
17497 kernel,
17498 kernel_pid: process.kernel_pid,
17499 vm_id,
17500 dns,
17501 host: payload.address.as_deref().unwrap_or("localhost"),
17502 port: payload.port,
17503 context: socket_paths,
17504 contents: &chunk,
17505 })?;
17506 Ok(json!({
17507 "bytes": written,
17508 "localAddress": local_addr.ip().to_string(),
17509 "localPort": local_addr.port(),
17510 "family": socket_addr_family(&local_addr),
17511 }))
17512 }
17513 "dgram.poll" => {
17514 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.poll socket id")?;
17515 let wait_ms =
17516 javascript_sync_rpc_arg_u64_optional(&request.args, 1, "dgram.poll wait ms")?
17517 .unwrap_or_default();
17518 let event = {
17519 let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
17520 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17521 })?;
17522 socket.poll(kernel, process.kernel_pid, Duration::from_millis(wait_ms))?
17523 };
17524
17525 match event {
17526 Some(JavascriptUdpSocketEvent::Message { data, remote_addr }) => {
17527 let family = JavascriptSocketFamily::from_ip(remote_addr.ip());
17528 let guest_remote_port = if is_loopback_ip(remote_addr.ip()) {
17529 socket_paths
17530 .guest_udp_port_for_host_port(family, remote_addr.port())
17531 .unwrap_or(remote_addr.port())
17532 } else {
17533 remote_addr.port()
17534 };
17535 Ok(json!({
17536 "type": "message",
17537 "data": javascript_sync_rpc_bytes_value(&data),
17538 "remoteAddress": remote_addr.ip().to_string(),
17539 "remotePort": guest_remote_port,
17540 "remoteFamily": socket_addr_family(&remote_addr),
17541 }))
17542 }
17543 Some(JavascriptUdpSocketEvent::Error { code, message }) => Ok(json!({
17544 "type": "error",
17545 "code": code,
17546 "message": message,
17547 })),
17548 None => Ok(Value::Null),
17549 }
17550 }
17551 "dgram.close" => {
17552 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "dgram.close socket id")?;
17553 let mut socket = process.udp_sockets.remove(socket_id).ok_or_else(|| {
17554 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17555 })?;
17556 socket.close(kernel, process.kernel_pid);
17557 Ok(Value::Null)
17558 }
17559 "dgram.address" => {
17560 let socket_id =
17561 javascript_sync_rpc_arg_str(&request.args, 0, "dgram.address socket id")?;
17562 let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
17563 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17564 })?;
17565 let local_addr = socket.local_addr().ok_or_else(|| {
17566 SidecarError::Execution(String::from("EBADF: bad file descriptor"))
17567 })?;
17568 javascript_net_json_string(
17569 json!({
17570 "address": local_addr.ip().to_string(),
17571 "port": local_addr.port(),
17572 "family": socket_addr_family(&local_addr),
17573 }),
17574 "dgram.address",
17575 )
17576 }
17577 "dgram.setBufferSize" => {
17578 let socket_id =
17579 javascript_sync_rpc_arg_str(&request.args, 0, "dgram.setBufferSize socket id")?;
17580 let which =
17581 javascript_sync_rpc_arg_str(&request.args, 1, "dgram.setBufferSize buffer kind")?;
17582 let size = javascript_sync_rpc_arg_u64(&request.args, 2, "dgram.setBufferSize size")?;
17583 let size = usize::try_from(size).map_err(|_| {
17584 SidecarError::InvalidState(String::from(
17585 "dgram.setBufferSize size must fit within usize",
17586 ))
17587 })?;
17588 let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| {
17589 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17590 })?;
17591 socket.set_buffer_size(which, size)?;
17592 Ok(Value::Null)
17593 }
17594 "dgram.getBufferSize" => {
17595 let socket_id =
17596 javascript_sync_rpc_arg_str(&request.args, 0, "dgram.getBufferSize socket id")?;
17597 let which =
17598 javascript_sync_rpc_arg_str(&request.args, 1, "dgram.getBufferSize buffer kind")?;
17599 let socket = process.udp_sockets.get(socket_id).ok_or_else(|| {
17600 SidecarError::InvalidState(format!("unknown UDP socket {socket_id}"))
17601 })?;
17602 let size = socket.get_buffer_size(which)?;
17603 Ok(json!(size))
17604 }
17605 other => Err(SidecarError::InvalidState(format!(
17606 "unsupported JavaScript dgram sync RPC method {other}"
17607 ))),
17608 }
17609}
17610
17611#[derive(Debug)]
17612struct ClientHttp2StreamState {
17613 send_stream: Option<h2::SendStream<Bytes>>,
17614}
17615
17616#[derive(Debug)]
17617struct ServerHttp2StreamState {
17618 send_response: Option<ServerHttp2Responder>,
17619 send_stream: Option<h2::SendStream<Bytes>>,
17620}
17621
17622#[derive(Debug)]
17623enum ServerHttp2Responder {
17624 Regular(server::SendResponse<Bytes>),
17625 Pushed(server::SendPushedResponse<Bytes>),
17626}
17627
17628const HTTP2_DEFAULT_WINDOW_SIZE: u32 = 65_535;
17629const HTTP2_POLL_DELAY: Duration = Duration::from_millis(10);
17630
17631fn http2_runtime_snapshot() -> Http2RuntimeSnapshot {
17632 Http2RuntimeSnapshot {
17633 effective_local_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
17634 local_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
17635 remote_window_size: HTTP2_DEFAULT_WINDOW_SIZE,
17636 next_stream_id: 1,
17637 outbound_queue_size: 1,
17638 deflate_dynamic_table_size: 0,
17639 inflate_dynamic_table_size: 0,
17640 }
17641}
17642
17643fn http2_snapshot_json(snapshot: &Http2SessionSnapshot) -> Result<String, SidecarError> {
17644 serde_json::to_string(snapshot)
17645 .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
17646}
17647
17648fn http2_event_value(event: &Http2BridgeEvent) -> Result<Value, SidecarError> {
17649 serde_json::to_string(event)
17650 .map(Value::String)
17651 .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
17652}
17653
17654fn push_http2_server_event(
17655 shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17656 server_id: u64,
17657 event: Http2BridgeEvent,
17658) {
17659 if let Ok(mut state) = shared.lock() {
17660 state
17661 .server_events
17662 .entry(server_id)
17663 .or_default()
17664 .push_back(event);
17665 }
17666}
17667
17668fn push_http2_session_event(
17669 shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17670 session_id: u64,
17671 event: Http2BridgeEvent,
17672) {
17673 if let Ok(mut state) = shared.lock() {
17674 state
17675 .session_events
17676 .entry(session_id)
17677 .or_default()
17678 .push_back(event);
17679 }
17680}
17681
17682fn pop_http2_event(
17683 queue: &mut BTreeMap<u64, VecDeque<Http2BridgeEvent>>,
17684 id: u64,
17685) -> Option<Http2BridgeEvent> {
17686 queue.get_mut(&id).and_then(VecDeque::pop_front)
17687}
17688
17689fn wait_for_http2_event(
17690 shared: &Arc<Mutex<crate::state::Http2SharedState>>,
17691 id: u64,
17692 is_server: bool,
17693 wait_ms: u64,
17694) -> Option<Http2BridgeEvent> {
17695 let deadline = Instant::now() + Duration::from_millis(wait_ms);
17696 loop {
17697 if let Ok(mut state) = shared.lock() {
17698 let queue = if is_server {
17699 &mut state.server_events
17700 } else {
17701 &mut state.session_events
17702 };
17703 if let Some(event) = pop_http2_event(queue, id) {
17704 return Some(event);
17705 }
17706 }
17707 if wait_ms == 0 || Instant::now() >= deadline {
17708 return None;
17709 }
17710 thread::sleep(HTTP2_POLL_DELAY);
17711 }
17712}
17713
17714fn next_http2_session_id(shared: &mut crate::state::Http2SharedState) -> u64 {
17715 shared.next_session_id += 1;
17716 shared.next_session_id
17717}
17718
17719fn next_http2_stream_id(shared: &mut crate::state::Http2SharedState) -> u64 {
17720 shared.next_stream_id += 1;
17721 shared.next_stream_id
17722}
17723
17724fn http2_reason(code: Option<u32>) -> Reason {
17725 code.unwrap_or(Reason::NO_ERROR.into()).into()
17726}
17727
17728fn http2_error_payload(message: impl Into<String>) -> String {
17729 serde_json::to_string(&json!({
17730 "name": "Error",
17731 "code": "ERR_HTTP2_ERROR",
17732 "message": message.into(),
17733 }))
17734 .unwrap_or_else(|_| {
17735 String::from(
17736 "{\"name\":\"Error\",\"code\":\"ERR_HTTP2_ERROR\",\"message\":\"HTTP/2 bridge error\"}",
17737 )
17738 })
17739}
17740
17741fn http2_socket_snapshot(local_addr: SocketAddr, remote_addr: SocketAddr) -> Http2SocketSnapshot {
17742 Http2SocketSnapshot {
17743 encrypted: false,
17744 allow_half_open: false,
17745 local_address: Some(local_addr.ip().to_string()),
17746 local_port: Some(local_addr.port()),
17747 local_family: Some(socket_addr_family(&local_addr).to_string()),
17748 remote_address: Some(remote_addr.ip().to_string()),
17749 remote_port: Some(remote_addr.port()),
17750 remote_family: Some(socket_addr_family(&remote_addr).to_string()),
17751 servername: None,
17752 alpn_protocol: Some(String::from("h2c")),
17753 }
17754}
17755
17756fn http2_wait_result(kind: &str, id: u64) -> Value {
17757 json!({
17758 "kind": kind,
17759 "id": id,
17760 })
17761}
17762
17763fn is_http2_terminal_event(event: &Http2BridgeEvent, is_server: bool, id: u64) -> bool {
17764 if is_server {
17765 event.kind == "serverClose" && event.id == id
17766 } else {
17767 event.kind == "sessionClose" && event.id == id
17768 }
17769}
17770
17771fn dispatch_http2_wait_loop(
17772 process: &ActiveProcess,
17773 id: u64,
17774 is_server: bool,
17775) -> Result<Value, SidecarError> {
17776 loop {
17777 if let Some(event) = wait_for_http2_event(&process.http2.shared, id, is_server, 50) {
17778 let payload = serde_json::to_value(&event).map_err(|error| {
17779 SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}"))
17780 })?;
17781 process
17782 .execution
17783 .send_javascript_stream_event("http2", payload.clone())?;
17784 if is_http2_terminal_event(&event, is_server, id) {
17785 return Ok(payload);
17786 }
17787 continue;
17788 }
17789
17790 let exists = process
17791 .http2
17792 .shared
17793 .lock()
17794 .map(|state| {
17795 if is_server {
17796 state.servers.contains_key(&id)
17797 } else {
17798 state.sessions.contains_key(&id)
17799 }
17800 })
17801 .unwrap_or(false);
17802 if !exists {
17803 return Ok(if is_server {
17804 http2_wait_result("serverClose", id)
17805 } else {
17806 http2_wait_result("sessionClose", id)
17807 });
17808 }
17809 }
17810}
17811
17812fn dispatch_http_wait_loop(process: &ActiveProcess, server_id: u64) -> Result<Value, SidecarError> {
17813 loop {
17814 if !process.http_servers.contains_key(&server_id) {
17815 return Ok(json!({
17816 "kind": "serverClose",
17817 "id": server_id,
17818 }));
17819 }
17820 thread::sleep(Duration::from_millis(25));
17821 }
17822}
17823
17824fn http2_settings_from_value(settings: &BTreeMap<String, Value>) -> BTreeMap<String, Value> {
17825 settings.clone()
17826}
17827
17828fn parse_http2_headers_json(
17829 headers_json: &str,
17830 label: &str,
17831) -> Result<BTreeMap<String, Value>, SidecarError> {
17832 serde_json::from_str::<BTreeMap<String, Value>>(headers_json)
17833 .map_err(|error| SidecarError::InvalidState(format!("{label} must be valid JSON: {error}")))
17834}
17835
17836fn apply_http2_header_values(
17837 header_map: &mut HeaderMap,
17838 name: &str,
17839 value: &Value,
17840) -> Result<(), SidecarError> {
17841 let header_name = HeaderName::from_bytes(name.as_bytes()).map_err(|error| {
17842 SidecarError::InvalidState(format!("invalid HTTP/2 header name {name:?}: {error}"))
17843 })?;
17844 match value {
17845 Value::Array(values) => {
17846 for value in values {
17847 apply_http2_header_values(header_map, name, value)?;
17848 }
17849 }
17850 Value::String(text) => {
17851 let value = HeaderValue::from_str(text).map_err(|error| {
17852 SidecarError::InvalidState(format!(
17853 "invalid HTTP/2 header value for {name}: {error}"
17854 ))
17855 })?;
17856 header_map.append(header_name.clone(), value);
17857 }
17858 Value::Number(number) => {
17859 let value = HeaderValue::from_str(&number.to_string()).map_err(|error| {
17860 SidecarError::InvalidState(format!(
17861 "invalid HTTP/2 numeric header value for {name}: {error}"
17862 ))
17863 })?;
17864 header_map.append(header_name.clone(), value);
17865 }
17866 Value::Bool(boolean) => {
17867 let value = HeaderValue::from_str(if *boolean { "true" } else { "false" }).map_err(
17868 |error| {
17869 SidecarError::InvalidState(format!(
17870 "invalid HTTP/2 boolean header value for {name}: {error}"
17871 ))
17872 },
17873 )?;
17874 header_map.append(header_name.clone(), value);
17875 }
17876 Value::Null => {}
17877 Value::Object(_) => {
17878 return Err(SidecarError::InvalidState(format!(
17879 "unsupported HTTP/2 header object value for {name}"
17880 )));
17881 }
17882 }
17883 Ok(())
17884}
17885
17886fn build_http2_request(headers_json: &str) -> Result<Request<()>, SidecarError> {
17887 let headers = parse_http2_headers_json(headers_json, "HTTP/2 request headers")?;
17888 let method = headers
17889 .get(":method")
17890 .and_then(Value::as_str)
17891 .unwrap_or("GET");
17892 let path = headers.get(":path").and_then(Value::as_str).unwrap_or("/");
17893 let mut builder = Request::builder()
17894 .method(Method::from_bytes(method.as_bytes()).map_err(|error| {
17895 SidecarError::InvalidState(format!("invalid HTTP/2 method {method:?}: {error}"))
17896 })?)
17897 .uri(path.parse::<Uri>().map_err(|error| {
17898 SidecarError::InvalidState(format!("invalid HTTP/2 path {path:?}: {error}"))
17899 })?);
17900 {
17901 let header_map = builder.headers_mut().expect("request header map");
17902 for (name, value) in &headers {
17903 if name.starts_with(':') {
17904 continue;
17905 }
17906 apply_http2_header_values(header_map, name, value)?;
17907 }
17908 }
17909 builder
17910 .body(())
17911 .map_err(|error| SidecarError::InvalidState(format!("invalid HTTP/2 request: {error}")))
17912}
17913
17914fn build_http2_response(headers_json: &str) -> Result<Response<()>, SidecarError> {
17915 let headers = parse_http2_headers_json(headers_json, "HTTP/2 response headers")?;
17916 let status = headers
17917 .get(":status")
17918 .and_then(Value::as_u64)
17919 .or_else(|| {
17920 headers
17921 .get(":status")
17922 .and_then(Value::as_str)
17923 .and_then(|value| value.parse::<u16>().ok().map(u64::from))
17924 })
17925 .unwrap_or(200);
17926 let mut builder = Response::builder().status(status as u16);
17927 {
17928 let header_map = builder.headers_mut().expect("response header map");
17929 for (name, value) in &headers {
17930 if name.starts_with(':') {
17931 continue;
17932 }
17933 apply_http2_header_values(header_map, name, value)?;
17934 }
17935 }
17936 builder.body(()).map_err(|error| {
17937 SidecarError::InvalidState(format!("invalid HTTP/2 response headers: {error}"))
17938 })
17939}
17940
17941fn serialize_http2_headers_map(
17942 pseudo: BTreeMap<String, Value>,
17943 headers: &HeaderMap,
17944) -> Result<String, SidecarError> {
17945 let mut serialized = pseudo;
17946 for (name, value) in headers {
17947 let name = name.as_str().to_string();
17948 let value = Value::String(
17949 value
17950 .to_str()
17951 .map_err(|error| {
17952 SidecarError::Execution(format!("invalid HTTP/2 header value: {error}"))
17953 })?
17954 .to_owned(),
17955 );
17956 match serialized.get_mut(&name) {
17957 Some(Value::Array(values)) => values.push(value),
17958 Some(existing) => {
17959 let first = existing.clone();
17960 *existing = Value::Array(vec![first, value]);
17961 }
17962 None => {
17963 serialized.insert(name, value);
17964 }
17965 }
17966 }
17967 serde_json::to_string(&serialized)
17968 .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
17969}
17970
17971fn serialize_http2_request_headers(
17972 request: &Request<h2::RecvStream>,
17973) -> Result<String, SidecarError> {
17974 let mut pseudo = BTreeMap::new();
17975 pseudo.insert(
17976 String::from(":method"),
17977 Value::String(request.method().as_str().to_string()),
17978 );
17979 pseudo.insert(
17980 String::from(":path"),
17981 Value::String(
17982 request
17983 .uri()
17984 .path_and_query()
17985 .map(|value| value.as_str().to_string())
17986 .unwrap_or_else(|| String::from("/")),
17987 ),
17988 );
17989 serialize_http2_headers_map(pseudo, request.headers())
17990}
17991
17992fn serialize_http2_response_headers(
17993 response: &Response<h2::RecvStream>,
17994) -> Result<String, SidecarError> {
17995 let mut pseudo = BTreeMap::new();
17996 pseudo.insert(
17997 String::from(":status"),
17998 Value::Number(serde_json::Number::from(response.status().as_u16())),
17999 );
18000 serialize_http2_headers_map(pseudo, response.headers())
18001}
18002
18003fn remove_http2_session_resources(
18004 shared: &Arc<Mutex<crate::state::Http2SharedState>>,
18005 session_id: u64,
18006) {
18007 if let Ok(mut state) = shared.lock() {
18008 state.sessions.remove(&session_id);
18009 state.session_events.remove(&session_id);
18010 let stream_ids = state
18011 .streams
18012 .iter()
18013 .filter_map(|(stream_id, stream)| {
18014 (stream.session_id == session_id).then_some(*stream_id)
18015 })
18016 .collect::<Vec<_>>();
18017 for stream_id in stream_ids {
18018 state.streams.remove(&stream_id);
18019 }
18020 }
18021}
18022
18023fn spawn_http2_client_session(
18024 shared: Arc<Mutex<crate::state::Http2SharedState>>,
18025 session_id: u64,
18026 remote_addr: SocketAddr,
18027 tls: Option<JavascriptTlsBridgeOptions>,
18028 snapshot: Arc<Mutex<Http2SessionSnapshot>>,
18029 mut command_rx: UnboundedReceiver<Http2SessionCommand>,
18030) {
18031 thread::spawn(move || {
18032 let runtime = match TokioRuntimeBuilder::new_current_thread()
18033 .enable_all()
18034 .build()
18035 {
18036 Ok(runtime) => runtime,
18037 Err(error) => {
18038 push_http2_session_event(
18039 &shared,
18040 session_id,
18041 Http2BridgeEvent {
18042 kind: String::from("sessionError"),
18043 id: session_id,
18044 data: Some(http2_error_payload(error.to_string())),
18045 ..Http2BridgeEvent::default()
18046 },
18047 );
18048 remove_http2_session_resources(&shared, session_id);
18049 return;
18050 }
18051 };
18052
18053 runtime.block_on(async move {
18054 let stream = match tokio::net::TcpStream::connect(remote_addr).await {
18055 Ok(stream) => stream,
18056 Err(error) => {
18057 push_http2_session_event(
18058 &shared,
18059 session_id,
18060 Http2BridgeEvent {
18061 kind: String::from("sessionError"),
18062 id: session_id,
18063 data: Some(http2_error_payload(error.to_string())),
18064 ..Http2BridgeEvent::default()
18065 },
18066 );
18067 remove_http2_session_resources(&shared, session_id);
18068 return;
18069 }
18070 };
18071
18072 let local_addr = match stream.local_addr() {
18073 Ok(addr) => addr,
18074 Err(error) => {
18075 push_http2_session_event(
18076 &shared,
18077 session_id,
18078 Http2BridgeEvent {
18079 kind: String::from("sessionError"),
18080 id: session_id,
18081 data: Some(http2_error_payload(error.to_string())),
18082 ..Http2BridgeEvent::default()
18083 },
18084 );
18085 remove_http2_session_resources(&shared, session_id);
18086 return;
18087 }
18088 };
18089
18090 {
18091 let mut snapshot_guard = snapshot.lock().expect("http2 snapshot lock");
18092 snapshot_guard.socket = http2_socket_snapshot(local_addr, remote_addr);
18093 if let Some(options) = tls.as_ref() {
18094 snapshot_guard.encrypted = true;
18095 snapshot_guard.alpn_protocol = Some(String::from("h2"));
18096 snapshot_guard.socket.encrypted = true;
18097 snapshot_guard.socket.servername = options.servername.clone();
18098 snapshot_guard.socket.alpn_protocol = Some(String::from("h2"));
18099 }
18100 snapshot_guard.state = http2_runtime_snapshot();
18101 }
18102 if let Ok(snapshot_json) =
18103 http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())
18104 {
18105 push_http2_session_event(
18106 &shared,
18107 session_id,
18108 Http2BridgeEvent {
18109 kind: String::from("sessionConnect"),
18110 id: session_id,
18111 data: Some(snapshot_json),
18112 ..Http2BridgeEvent::default()
18113 },
18114 );
18115 }
18116
18117 let io: Pin<Box<dyn Http2AsyncIo>> = if let Some(options) = tls.as_ref() {
18118 let server_name = match ServerName::try_from(
18119 options
18120 .servername
18121 .clone()
18122 .unwrap_or_else(|| String::from("localhost")),
18123 ) {
18124 Ok(server_name) => server_name,
18125 Err(_) => {
18126 push_http2_session_event(
18127 &shared,
18128 session_id,
18129 Http2BridgeEvent {
18130 kind: String::from("sessionError"),
18131 id: session_id,
18132 data: Some(http2_error_payload("invalid TLS servername")),
18133 ..Http2BridgeEvent::default()
18134 },
18135 );
18136 remove_http2_session_resources(&shared, session_id);
18137 return;
18138 }
18139 };
18140 let connector = match build_client_tls_config(options) {
18141 Ok(config) => TlsConnector::from(Arc::new(config)),
18142 Err(error) => {
18143 push_http2_session_event(
18144 &shared,
18145 session_id,
18146 Http2BridgeEvent {
18147 kind: String::from("sessionError"),
18148 id: session_id,
18149 data: Some(http2_error_payload(error.to_string())),
18150 ..Http2BridgeEvent::default()
18151 },
18152 );
18153 remove_http2_session_resources(&shared, session_id);
18154 return;
18155 }
18156 };
18157 match connector.connect(server_name, stream).await {
18158 Ok(tls_stream) => Box::pin(tls_stream),
18159 Err(error) => {
18160 push_http2_session_event(
18161 &shared,
18162 session_id,
18163 Http2BridgeEvent {
18164 kind: String::from("sessionError"),
18165 id: session_id,
18166 data: Some(http2_error_payload(error.to_string())),
18167 ..Http2BridgeEvent::default()
18168 },
18169 );
18170 remove_http2_session_resources(&shared, session_id);
18171 return;
18172 }
18173 }
18174 } else {
18175 Box::pin(stream)
18176 };
18177
18178 let (mut sender, connection) = match client::handshake(io).await {
18179 Ok(parts) => parts,
18180 Err(error) => {
18181 push_http2_session_event(
18182 &shared,
18183 session_id,
18184 Http2BridgeEvent {
18185 kind: String::from("sessionError"),
18186 id: session_id,
18187 data: Some(http2_error_payload(error.to_string())),
18188 ..Http2BridgeEvent::default()
18189 },
18190 );
18191 remove_http2_session_resources(&shared, session_id);
18192 return;
18193 }
18194 };
18195
18196 let (status_tx, mut status_rx) = unbounded_channel::<Result<(), String>>();
18197 tokio::spawn(async move {
18198 let _ = status_tx.send(connection.await.map_err(|error| error.to_string()));
18199 });
18200
18201 let streams: Arc<Mutex<BTreeMap<u64, ClientHttp2StreamState>>> =
18202 Arc::new(Mutex::new(BTreeMap::new()));
18203
18204 loop {
18205 tokio::select! {
18206 Some(result) = status_rx.recv() => {
18207 if let Err(message) = result {
18208 push_http2_session_event(
18209 &shared,
18210 session_id,
18211 Http2BridgeEvent {
18212 kind: String::from("sessionError"),
18213 id: session_id,
18214 data: Some(http2_error_payload(message)),
18215 ..Http2BridgeEvent::default()
18216 },
18217 );
18218 }
18219 push_http2_session_event(
18220 &shared,
18221 session_id,
18222 Http2BridgeEvent {
18223 kind: String::from("sessionClose"),
18224 id: session_id,
18225 ..Http2BridgeEvent::default()
18226 },
18227 );
18228 remove_http2_session_resources(&shared, session_id);
18229 break;
18230 }
18231 Some(command) = command_rx.recv() => {
18232 match command {
18233 Http2SessionCommand::Request { headers_json, options_json, respond_to } => {
18234 let request = match build_http2_request(&headers_json) {
18235 Ok(request) => request,
18236 Err(error) => {
18237 let _ = respond_to.send(Err(error.to_string()));
18238 continue;
18239 }
18240 };
18241 let options: JavascriptHttp2RequestOptions =
18242 serde_json::from_str(&options_json).unwrap_or_default();
18243 let stream_id = {
18244 let mut state = shared.lock().expect("http2 shared state");
18245 let stream_id = next_http2_stream_id(&mut state);
18246 state.streams.insert(
18247 stream_id,
18248 ActiveHttp2Stream {
18249 session_id,
18250 paused: Arc::new(AtomicBool::new(false)),
18251 },
18252 );
18253 stream_id
18254 };
18255 match sender.send_request(request, options.end_stream) {
18256 Ok((response_future, send_stream)) => {
18257 if !options.end_stream {
18258 streams
18259 .lock()
18260 .expect("http2 client streams")
18261 .insert(stream_id, ClientHttp2StreamState { send_stream: Some(send_stream) });
18262 }
18263 let shared_clone = Arc::clone(&shared);
18264 let snapshot_clone = Arc::clone(&snapshot);
18265 tokio::spawn(async move {
18266 match response_future.await {
18267 Ok(response) => {
18268 if let Ok(headers_json) = serialize_http2_response_headers(&response) {
18269 push_http2_session_event(
18270 &shared_clone,
18271 session_id,
18272 Http2BridgeEvent {
18273 kind: String::from("clientResponseHeaders"),
18274 id: stream_id,
18275 data: Some(headers_json),
18276 ..Http2BridgeEvent::default()
18277 },
18278 );
18279 }
18280 let mut body = response.into_body();
18281 while let Some(chunk) = body.data().await {
18282 match chunk {
18283 Ok(bytes) => {
18284 let paused = {
18285 let state = shared_clone.lock().expect("http2 shared state");
18286 state.streams.get(&stream_id).map(|stream| Arc::clone(&stream.paused))
18287 };
18288 if let Some(paused) = paused {
18289 while paused.load(Ordering::SeqCst) {
18290 tokio::time::sleep(HTTP2_POLL_DELAY).await;
18291 }
18292 }
18293 let _ = body.flow_control().release_capacity(bytes.len());
18294 push_http2_session_event(
18295 &shared_clone,
18296 session_id,
18297 Http2BridgeEvent {
18298 kind: String::from("clientData"),
18299 id: stream_id,
18300 data: Some(base64::engine::general_purpose::STANDARD.encode(bytes)),
18301 ..Http2BridgeEvent::default()
18302 },
18303 );
18304 }
18305 Err(error) => {
18306 push_http2_session_event(
18307 &shared_clone,
18308 session_id,
18309 Http2BridgeEvent {
18310 kind: String::from("clientError"),
18311 id: stream_id,
18312 data: Some(http2_error_payload(error.to_string())),
18313 ..Http2BridgeEvent::default()
18314 },
18315 );
18316 break;
18317 }
18318 }
18319 }
18320 {
18321 let mut snapshot = snapshot_clone.lock().expect("http2 snapshot lock");
18322 snapshot.state.next_stream_id =
18323 snapshot.state.next_stream_id.saturating_add(2);
18324 }
18325 push_http2_session_event(
18326 &shared_clone,
18327 session_id,
18328 Http2BridgeEvent {
18329 kind: String::from("clientEnd"),
18330 id: stream_id,
18331 ..Http2BridgeEvent::default()
18332 },
18333 );
18334 push_http2_session_event(
18335 &shared_clone,
18336 session_id,
18337 Http2BridgeEvent {
18338 kind: String::from("clientClose"),
18339 id: stream_id,
18340 extra_number: Some(0),
18341 ..Http2BridgeEvent::default()
18342 },
18343 );
18344 if let Ok(mut state) = shared_clone.lock() {
18345 state.streams.remove(&stream_id);
18346 }
18347 }
18348 Err(error) => {
18349 push_http2_session_event(
18350 &shared_clone,
18351 session_id,
18352 Http2BridgeEvent {
18353 kind: String::from("clientError"),
18354 id: stream_id,
18355 data: Some(http2_error_payload(error.to_string())),
18356 ..Http2BridgeEvent::default()
18357 },
18358 );
18359 push_http2_session_event(
18360 &shared_clone,
18361 session_id,
18362 Http2BridgeEvent {
18363 kind: String::from("clientClose"),
18364 id: stream_id,
18365 extra_number: Some(u32::from(Reason::INTERNAL_ERROR) as u64),
18366 ..Http2BridgeEvent::default()
18367 },
18368 );
18369 if let Ok(mut state) = shared_clone.lock() {
18370 state.streams.remove(&stream_id);
18371 }
18372 }
18373 }
18374 });
18375 let _ = respond_to.send(Ok(json!(stream_id)));
18376 }
18377 Err(error) => {
18378 if let Ok(mut state) = shared.lock() {
18379 state.streams.remove(&stream_id);
18380 }
18381 let _ = respond_to.send(Err(error.to_string()));
18382 }
18383 }
18384 }
18385 Http2SessionCommand::Settings { settings_json, respond_to } => {
18386 let settings = serde_json::from_str::<BTreeMap<String, Value>>(&settings_json)
18387 .unwrap_or_default();
18388 {
18389 let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18390 snapshot.local_settings = http2_settings_from_value(&settings);
18391 }
18392 if let Ok(headers_json) = serde_json::to_string(&settings) {
18393 push_http2_session_event(
18394 &shared,
18395 session_id,
18396 Http2BridgeEvent {
18397 kind: String::from("sessionLocalSettings"),
18398 id: session_id,
18399 data: Some(headers_json.clone()),
18400 ..Http2BridgeEvent::default()
18401 },
18402 );
18403 push_http2_session_event(
18404 &shared,
18405 session_id,
18406 Http2BridgeEvent {
18407 kind: String::from("sessionSettingsAck"),
18408 id: session_id,
18409 ..Http2BridgeEvent::default()
18410 },
18411 );
18412 }
18413 let _ = respond_to.send(Ok(Value::Null));
18414 }
18415 Http2SessionCommand::SetLocalWindowSize { size, respond_to } => {
18416 {
18417 let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18418 snapshot.state.local_window_size = size;
18419 snapshot.state.effective_local_window_size = size;
18420 }
18421 let value = snapshot
18422 .lock()
18423 .ok()
18424 .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok())
18425 .map(Value::String)
18426 .unwrap_or(Value::Null);
18427 let _ = respond_to.send(Ok(value));
18428 }
18429 Http2SessionCommand::Goaway { error_code, last_stream_id, opaque_data, respond_to } => {
18430 push_http2_session_event(
18431 &shared,
18432 session_id,
18433 Http2BridgeEvent {
18434 kind: String::from("sessionGoaway"),
18435 id: session_id,
18436 data: opaque_data.map(|value| {
18437 base64::engine::general_purpose::STANDARD.encode(value)
18438 }),
18439 extra_number: Some(error_code as u64),
18440 flags: Some(last_stream_id as u64),
18441 ..Http2BridgeEvent::default()
18442 },
18443 );
18444 let _ = respond_to.send(Ok(Value::Null));
18445 }
18446 Http2SessionCommand::Close { respond_to, .. } => {
18447 let _ = respond_to.send(Ok(Value::Null));
18448 push_http2_session_event(
18449 &shared,
18450 session_id,
18451 Http2BridgeEvent {
18452 kind: String::from("sessionClose"),
18453 id: session_id,
18454 ..Http2BridgeEvent::default()
18455 },
18456 );
18457 remove_http2_session_resources(&shared, session_id);
18458 break;
18459 }
18460 Http2SessionCommand::StreamWrite { stream_id, chunk, end_stream, respond_to } => {
18461 let result = streams
18462 .lock()
18463 .expect("http2 client streams")
18464 .get_mut(&stream_id)
18465 .and_then(|stream| stream.send_stream.as_mut())
18466 .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 client stream {stream_id}")))
18467 .and_then(|stream| stream.send_data(Bytes::from(chunk), end_stream).map_err(|error| SidecarError::Execution(error.to_string())));
18468 match result {
18469 Ok(()) => {
18470 if end_stream {
18471 streams.lock().expect("http2 client streams").remove(&stream_id);
18472 }
18473 let _ = respond_to.send(Ok(Value::Bool(true)));
18474 }
18475 Err(error) => {
18476 let _ = respond_to.send(Err(error.to_string()));
18477 }
18478 }
18479 }
18480 Http2SessionCommand::StreamClose { stream_id, error_code, respond_to } => {
18481 let mut streams = streams.lock().expect("http2 client streams");
18482 let Some(mut state) = streams.remove(&stream_id) else {
18483 let _ = respond_to.send(Err(format!("unknown HTTP/2 client stream {stream_id}")));
18484 continue;
18485 };
18486 if let Some(stream) = state.send_stream.as_mut() {
18487 stream.send_reset(http2_reason(error_code));
18488 }
18489 if let Ok(mut state) = shared.lock() {
18490 state.streams.remove(&stream_id);
18491 }
18492 push_http2_session_event(
18493 &shared,
18494 session_id,
18495 Http2BridgeEvent {
18496 kind: String::from("clientClose"),
18497 id: stream_id,
18498 extra_number: Some(u32::from(http2_reason(error_code)) as u64),
18499 ..Http2BridgeEvent::default()
18500 },
18501 );
18502 let _ = respond_to.send(Ok(Value::Null));
18503 }
18504 Http2SessionCommand::StreamRespond { respond_to, .. }
18505 | Http2SessionCommand::StreamPush { respond_to, .. }
18506 | Http2SessionCommand::StreamRespondWithFile { respond_to, .. } => {
18507 let _ = respond_to.send(Err(String::from("HTTP/2 client streams cannot send server responses")));
18508 }
18509 }
18510 }
18511 else => break,
18512 }
18513 }
18514 });
18515 });
18516}
18517
18518fn spawn_http2_server_session(
18519 shared: Arc<Mutex<crate::state::Http2SharedState>>,
18520 server_id: u64,
18521 session_id: u64,
18522 stream: TcpStream,
18523 tls: Option<JavascriptTlsBridgeOptions>,
18524 snapshot: Arc<Mutex<Http2SessionSnapshot>>,
18525 mut command_rx: UnboundedReceiver<Http2SessionCommand>,
18526) {
18527 thread::spawn(move || {
18528 let runtime = match TokioRuntimeBuilder::new_current_thread()
18529 .enable_all()
18530 .build()
18531 {
18532 Ok(runtime) => runtime,
18533 Err(error) => {
18534 push_http2_server_event(
18535 &shared,
18536 server_id,
18537 Http2BridgeEvent {
18538 kind: String::from("serverStreamError"),
18539 id: session_id,
18540 data: Some(http2_error_payload(error.to_string())),
18541 ..Http2BridgeEvent::default()
18542 },
18543 );
18544 remove_http2_session_resources(&shared, session_id);
18545 return;
18546 }
18547 };
18548
18549 runtime.block_on(async move {
18550 if let Err(error) = stream.set_nonblocking(true) {
18551 push_http2_server_event(
18552 &shared,
18553 server_id,
18554 Http2BridgeEvent {
18555 kind: String::from("serverStreamError"),
18556 id: session_id,
18557 data: Some(http2_error_payload(error.to_string())),
18558 ..Http2BridgeEvent::default()
18559 },
18560 );
18561 remove_http2_session_resources(&shared, session_id);
18562 return;
18563 }
18564 let stream = match tokio::net::TcpStream::from_std(stream) {
18565 Ok(stream) => stream,
18566 Err(error) => {
18567 push_http2_server_event(
18568 &shared,
18569 server_id,
18570 Http2BridgeEvent {
18571 kind: String::from("serverStreamError"),
18572 id: session_id,
18573 data: Some(http2_error_payload(error.to_string())),
18574 ..Http2BridgeEvent::default()
18575 },
18576 );
18577 remove_http2_session_resources(&shared, session_id);
18578 return;
18579 }
18580 };
18581 let local_addr = match stream.local_addr() {
18582 Ok(addr) => addr,
18583 Err(error) => {
18584 push_http2_server_event(
18585 &shared,
18586 server_id,
18587 Http2BridgeEvent {
18588 kind: String::from("serverStreamError"),
18589 id: session_id,
18590 data: Some(http2_error_payload(error.to_string())),
18591 ..Http2BridgeEvent::default()
18592 },
18593 );
18594 remove_http2_session_resources(&shared, session_id);
18595 return;
18596 }
18597 };
18598 let remote_addr = match stream.peer_addr() {
18599 Ok(addr) => addr,
18600 Err(error) => {
18601 push_http2_server_event(
18602 &shared,
18603 server_id,
18604 Http2BridgeEvent {
18605 kind: String::from("serverStreamError"),
18606 id: session_id,
18607 data: Some(http2_error_payload(error.to_string())),
18608 ..Http2BridgeEvent::default()
18609 },
18610 );
18611 remove_http2_session_resources(&shared, session_id);
18612 return;
18613 }
18614 };
18615 {
18616 let mut snapshot_guard = snapshot.lock().expect("http2 snapshot lock");
18617 snapshot_guard.socket = http2_socket_snapshot(local_addr, remote_addr);
18618 if tls.is_some() {
18619 snapshot_guard.encrypted = true;
18620 snapshot_guard.alpn_protocol = Some(String::from("h2"));
18621 snapshot_guard.socket.encrypted = true;
18622 snapshot_guard.socket.alpn_protocol = Some(String::from("h2"));
18623 }
18624 snapshot_guard.state = http2_runtime_snapshot();
18625 }
18626 if let Ok(snapshot_json) =
18627 http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())
18628 {
18629 push_http2_server_event(
18630 &shared,
18631 server_id,
18632 Http2BridgeEvent {
18633 kind: String::from(if tls.is_some() {
18634 "serverSecureConnection"
18635 } else {
18636 "serverConnection"
18637 }),
18638 id: server_id,
18639 data: Some(serde_json::to_string(&http2_socket_snapshot(local_addr, remote_addr)).unwrap_or_default()),
18640 ..Http2BridgeEvent::default()
18641 },
18642 );
18643 push_http2_server_event(
18644 &shared,
18645 server_id,
18646 Http2BridgeEvent {
18647 kind: String::from("serverSession"),
18648 id: server_id,
18649 data: Some(snapshot_json),
18650 extra_number: Some(session_id),
18651 ..Http2BridgeEvent::default()
18652 },
18653 );
18654 }
18655
18656 let io: Pin<Box<dyn Http2AsyncIo>> = if let Some(options) = tls.as_ref() {
18657 let acceptor = match build_server_tls_config(options) {
18658 Ok(config) => TlsAcceptor::from(Arc::new(config)),
18659 Err(error) => {
18660 push_http2_server_event(
18661 &shared,
18662 server_id,
18663 Http2BridgeEvent {
18664 kind: String::from("serverStreamError"),
18665 id: session_id,
18666 data: Some(http2_error_payload(error.to_string())),
18667 ..Http2BridgeEvent::default()
18668 },
18669 );
18670 remove_http2_session_resources(&shared, session_id);
18671 return;
18672 }
18673 };
18674 match acceptor.accept(stream).await {
18675 Ok(tls_stream) => Box::pin(tls_stream),
18676 Err(error) => {
18677 push_http2_server_event(
18678 &shared,
18679 server_id,
18680 Http2BridgeEvent {
18681 kind: String::from("serverStreamError"),
18682 id: session_id,
18683 data: Some(http2_error_payload(error.to_string())),
18684 ..Http2BridgeEvent::default()
18685 },
18686 );
18687 remove_http2_session_resources(&shared, session_id);
18688 return;
18689 }
18690 }
18691 } else {
18692 Box::pin(stream)
18693 };
18694
18695 let mut connection = match server::handshake(io).await {
18696 Ok(connection) => connection,
18697 Err(error) => {
18698 push_http2_server_event(
18699 &shared,
18700 server_id,
18701 Http2BridgeEvent {
18702 kind: String::from("serverStreamError"),
18703 id: session_id,
18704 data: Some(http2_error_payload(error.to_string())),
18705 ..Http2BridgeEvent::default()
18706 },
18707 );
18708 remove_http2_session_resources(&shared, session_id);
18709 return;
18710 }
18711 };
18712
18713 let streams: Arc<Mutex<BTreeMap<u64, ServerHttp2StreamState>>> =
18714 Arc::new(Mutex::new(BTreeMap::new()));
18715
18716 loop {
18717 tokio::select! {
18718 incoming = connection.accept() => {
18719 match incoming {
18720 Some(Ok((request, respond))) => {
18721 let headers_json = match serialize_http2_request_headers(&request) {
18722 Ok(headers) => headers,
18723 Err(error) => {
18724 push_http2_server_event(
18725 &shared,
18726 server_id,
18727 Http2BridgeEvent {
18728 kind: String::from("serverStreamError"),
18729 id: server_id,
18730 data: Some(http2_error_payload(error.to_string())),
18731 ..Http2BridgeEvent::default()
18732 },
18733 );
18734 continue;
18735 }
18736 };
18737 let stream_id = {
18738 let mut state = shared.lock().expect("http2 shared state");
18739 let stream_id = next_http2_stream_id(&mut state);
18740 state.streams.insert(
18741 stream_id,
18742 ActiveHttp2Stream {
18743 session_id,
18744 paused: Arc::new(AtomicBool::new(false)),
18745 },
18746 );
18747 stream_id
18748 };
18749 streams.lock().expect("http2 server streams").insert(
18750 stream_id,
18751 ServerHttp2StreamState {
18752 send_response: Some(ServerHttp2Responder::Regular(respond)),
18753 send_stream: None,
18754 },
18755 );
18756 let snapshot_json = snapshot
18757 .lock()
18758 .ok()
18759 .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok());
18760 push_http2_server_event(
18761 &shared,
18762 server_id,
18763 Http2BridgeEvent {
18764 kind: String::from("serverStream"),
18765 id: server_id,
18766 data: Some(stream_id.to_string()),
18767 extra: snapshot_json,
18768 extra_number: Some(session_id),
18769 extra_headers: Some(headers_json),
18770 flags: Some(0),
18771 },
18772 );
18773 let shared_clone = Arc::clone(&shared);
18774 tokio::spawn(async move {
18775 let mut body = request.into_body();
18776 while let Some(chunk) = body.data().await {
18777 match chunk {
18778 Ok(bytes) => {
18779 let paused = {
18780 let state = shared_clone.lock().expect("http2 shared state");
18781 state.streams.get(&stream_id).map(|stream| Arc::clone(&stream.paused))
18782 };
18783 if let Some(paused) = paused {
18784 while paused.load(Ordering::SeqCst) {
18785 tokio::time::sleep(HTTP2_POLL_DELAY).await;
18786 }
18787 }
18788 let _ = body.flow_control().release_capacity(bytes.len());
18789 push_http2_server_event(
18790 &shared_clone,
18791 server_id,
18792 Http2BridgeEvent {
18793 kind: String::from("serverStreamData"),
18794 id: stream_id,
18795 data: Some(base64::engine::general_purpose::STANDARD.encode(bytes)),
18796 ..Http2BridgeEvent::default()
18797 },
18798 );
18799 }
18800 Err(error) => {
18801 push_http2_server_event(
18802 &shared_clone,
18803 server_id,
18804 Http2BridgeEvent {
18805 kind: String::from("serverStreamError"),
18806 id: stream_id,
18807 data: Some(http2_error_payload(error.to_string())),
18808 ..Http2BridgeEvent::default()
18809 },
18810 );
18811 break;
18812 }
18813 }
18814 }
18815 push_http2_server_event(
18816 &shared_clone,
18817 server_id,
18818 Http2BridgeEvent {
18819 kind: String::from("serverStreamEnd"),
18820 id: stream_id,
18821 ..Http2BridgeEvent::default()
18822 },
18823 );
18824 });
18825 }
18826 Some(Err(error)) => {
18827 push_http2_server_event(
18828 &shared,
18829 server_id,
18830 Http2BridgeEvent {
18831 kind: String::from("serverStreamError"),
18832 id: server_id,
18833 data: Some(http2_error_payload(error.to_string())),
18834 ..Http2BridgeEvent::default()
18835 },
18836 );
18837 break;
18838 }
18839 None => {
18840 push_http2_server_event(
18841 &shared,
18842 server_id,
18843 Http2BridgeEvent {
18844 kind: String::from("sessionClose"),
18845 id: session_id,
18846 ..Http2BridgeEvent::default()
18847 },
18848 );
18849 remove_http2_session_resources(&shared, session_id);
18850 break;
18851 }
18852 }
18853 }
18854 Some(command) = command_rx.recv() => {
18855 match command {
18856 Http2SessionCommand::Settings { settings_json, respond_to } => {
18857 let settings = serde_json::from_str::<BTreeMap<String, Value>>(&settings_json)
18858 .unwrap_or_default();
18859 if let Some(initial_window_size) = settings
18860 .get("initialWindowSize")
18861 .and_then(Value::as_u64)
18862 {
18863 let _ = connection.set_initial_window_size(initial_window_size as u32);
18864 }
18865 {
18866 let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18867 snapshot.local_settings = http2_settings_from_value(&settings);
18868 }
18869 if let Ok(headers_json) = serde_json::to_string(&settings) {
18870 push_http2_session_event(
18871 &shared,
18872 session_id,
18873 Http2BridgeEvent {
18874 kind: String::from("sessionLocalSettings"),
18875 id: session_id,
18876 data: Some(headers_json),
18877 ..Http2BridgeEvent::default()
18878 },
18879 );
18880 }
18881 let _ = respond_to.send(Ok(Value::Null));
18882 }
18883 Http2SessionCommand::SetLocalWindowSize { size, respond_to } => {
18884 connection.set_target_window_size(size);
18885 {
18886 let mut snapshot = snapshot.lock().expect("http2 snapshot lock");
18887 snapshot.state.local_window_size = size;
18888 snapshot.state.effective_local_window_size = size;
18889 }
18890 let value = snapshot
18891 .lock()
18892 .ok()
18893 .and_then(|snapshot| http2_snapshot_json(&snapshot.clone()).ok())
18894 .map(Value::String)
18895 .unwrap_or(Value::Null);
18896 let _ = respond_to.send(Ok(value));
18897 }
18898 Http2SessionCommand::Goaway { error_code, last_stream_id, opaque_data, respond_to } => {
18899 connection.abrupt_shutdown(http2_reason(Some(error_code)));
18900 push_http2_session_event(
18901 &shared,
18902 session_id,
18903 Http2BridgeEvent {
18904 kind: String::from("sessionGoaway"),
18905 id: session_id,
18906 data: opaque_data.map(|value| {
18907 base64::engine::general_purpose::STANDARD.encode(value)
18908 }),
18909 extra_number: Some(error_code as u64),
18910 flags: Some(last_stream_id as u64),
18911 ..Http2BridgeEvent::default()
18912 },
18913 );
18914 let _ = respond_to.send(Ok(Value::Null));
18915 }
18916 Http2SessionCommand::Close { abrupt, respond_to } => {
18917 if abrupt {
18918 connection.abrupt_shutdown(Reason::NO_ERROR);
18919 } else {
18920 connection.graceful_shutdown();
18921 }
18922 let _ = respond_to.send(Ok(Value::Null));
18923 push_http2_session_event(
18924 &shared,
18925 session_id,
18926 Http2BridgeEvent {
18927 kind: String::from("sessionClose"),
18928 id: session_id,
18929 ..Http2BridgeEvent::default()
18930 },
18931 );
18932 remove_http2_session_resources(&shared, session_id);
18933 break;
18934 }
18935 Http2SessionCommand::StreamRespond { stream_id, headers_json, respond_to } => {
18936 let response = match build_http2_response(&headers_json) {
18937 Ok(response) => response,
18938 Err(error) => {
18939 let _ = respond_to.send(Err(error.to_string()));
18940 continue;
18941 }
18942 };
18943 let mut streams = streams.lock().expect("http2 server streams");
18944 let Some(state) = streams.get_mut(&stream_id) else {
18945 let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
18946 continue;
18947 };
18948 let Some(send_response) = state.send_response.as_mut() else {
18949 let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} already responded")));
18950 continue;
18951 };
18952 match match send_response {
18953 ServerHttp2Responder::Regular(send_response) => {
18954 send_response.send_response(response, false)
18955 }
18956 ServerHttp2Responder::Pushed(send_response) => {
18957 send_response.send_response(response, false)
18958 }
18959 } {
18960 Ok(send_stream) => {
18961 state.send_stream = Some(send_stream);
18962 state.send_response = None;
18963 let _ = respond_to.send(Ok(Value::Null));
18964 }
18965 Err(error) => {
18966 let _ = respond_to.send(Err(error.to_string()));
18967 }
18968 }
18969 }
18970 Http2SessionCommand::StreamPush { stream_id, headers_json, respond_to } => {
18971 let request = match build_http2_request(&headers_json) {
18972 Ok(request) => request,
18973 Err(error) => {
18974 let _ = respond_to.send(Err(error.to_string()));
18975 continue;
18976 }
18977 };
18978 let mut streams_guard = streams.lock().expect("http2 server streams");
18979 let Some(state) = streams_guard.get_mut(&stream_id) else {
18980 let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
18981 continue;
18982 };
18983 let Some(send_response) = state.send_response.as_mut() else {
18984 let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} cannot push after responding")));
18985 continue;
18986 };
18987 let ServerHttp2Responder::Regular(send_response) = send_response else {
18988 let _ = respond_to.send(Err(format!("HTTP/2 pushed stream {stream_id} cannot create nested push promises")));
18989 continue;
18990 };
18991 match send_response.push_request(request) {
18992 Ok(pushed) => {
18993 let pushed_stream_id = {
18994 let mut state = shared.lock().expect("http2 shared state");
18995 let pushed_stream_id = next_http2_stream_id(&mut state);
18996 state.streams.insert(
18997 pushed_stream_id,
18998 ActiveHttp2Stream {
18999 session_id,
19000 paused: Arc::new(AtomicBool::new(false)),
19001 },
19002 );
19003 pushed_stream_id
19004 };
19005 streams_guard.insert(
19006 pushed_stream_id,
19007 ServerHttp2StreamState {
19008 send_response: Some(ServerHttp2Responder::Pushed(pushed)),
19009 send_stream: None,
19010 },
19011 );
19012 let _ = respond_to.send(Ok(json!({
19013 "streamId": pushed_stream_id,
19014 "headers": headers_json,
19015 }).to_string().into()));
19016 }
19017 Err(error) => {
19018 let _ = respond_to.send(Err(error.to_string()));
19019 }
19020 }
19021 }
19022 Http2SessionCommand::StreamWrite { stream_id, chunk, end_stream, respond_to } => {
19023 let mut streams = streams.lock().expect("http2 server streams");
19024 let Some(state) = streams.get_mut(&stream_id) else {
19025 let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
19026 continue;
19027 };
19028 let Some(send_stream) = state.send_stream.as_mut() else {
19029 let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} has not sent response headers")));
19030 continue;
19031 };
19032 match send_stream.send_data(Bytes::from(chunk), end_stream) {
19033 Ok(()) => {
19034 if end_stream {
19035 streams.remove(&stream_id);
19036 if let Ok(mut state) = shared.lock() {
19037 state.streams.remove(&stream_id);
19038 }
19039 push_http2_server_event(
19040 &shared,
19041 server_id,
19042 Http2BridgeEvent {
19043 kind: String::from("serverStreamClose"),
19044 id: stream_id,
19045 extra_number: Some(0),
19046 ..Http2BridgeEvent::default()
19047 },
19048 );
19049 }
19050 let _ = respond_to.send(Ok(Value::Bool(true)));
19051 }
19052 Err(error) => {
19053 let _ = respond_to.send(Err(error.to_string()));
19054 }
19055 }
19056 }
19057 Http2SessionCommand::StreamClose { stream_id, error_code, respond_to } => {
19058 let mut streams_guard = streams.lock().expect("http2 server streams");
19059 let Some(mut state) = streams_guard.remove(&stream_id) else {
19060 let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
19061 continue;
19062 };
19063 let reason = http2_reason(error_code);
19064 if let Some(send_stream) = state.send_stream.as_mut() {
19065 send_stream.send_reset(reason);
19066 }
19067 if let Some(send_response) = state.send_response.as_mut() {
19068 match send_response {
19069 ServerHttp2Responder::Regular(send_response) => {
19070 send_response.send_reset(reason)
19071 }
19072 ServerHttp2Responder::Pushed(send_response) => {
19073 send_response.send_reset(reason)
19074 }
19075 }
19076 }
19077 if let Ok(mut shared_guard) = shared.lock() {
19078 shared_guard.streams.remove(&stream_id);
19079 }
19080 push_http2_server_event(
19081 &shared,
19082 server_id,
19083 Http2BridgeEvent {
19084 kind: String::from("serverStreamClose"),
19085 id: stream_id,
19086 extra_number: Some(u32::from(reason) as u64),
19087 ..Http2BridgeEvent::default()
19088 },
19089 );
19090 let _ = respond_to.send(Ok(Value::Null));
19091 }
19092 Http2SessionCommand::StreamRespondWithFile { stream_id, body, headers_json, options_json, respond_to } => {
19093 let options: JavascriptHttp2FileResponseOptions =
19094 serde_json::from_str(&options_json).unwrap_or_default();
19095 let response = match build_http2_response(&headers_json) {
19096 Ok(response) => response,
19097 Err(error) => {
19098 let _ = respond_to.send(Err(error.to_string()));
19099 continue;
19100 }
19101 };
19102 let offset = usize::try_from(options.offset.unwrap_or_default()).unwrap_or(0);
19103 let body = if offset >= body.len() {
19104 Vec::new()
19105 } else {
19106 let body = &body[offset..];
19107 match options.length {
19108 Some(length) if length >= 0 => {
19109 body[..body.len().min(length as usize)].to_vec()
19110 }
19111 _ => body.to_vec(),
19112 }
19113 };
19114 let mut streams_guard = streams.lock().expect("http2 server streams");
19115 let Some(state) = streams_guard.get_mut(&stream_id) else {
19116 let _ = respond_to.send(Err(format!("unknown HTTP/2 server stream {stream_id}")));
19117 continue;
19118 };
19119 let Some(send_response) = state.send_response.as_mut() else {
19120 let _ = respond_to.send(Err(format!("HTTP/2 server stream {stream_id} already responded")));
19121 continue;
19122 };
19123 match match send_response {
19124 ServerHttp2Responder::Regular(send_response) => {
19125 send_response.send_response(response, body.is_empty())
19126 }
19127 ServerHttp2Responder::Pushed(send_response) => {
19128 send_response.send_response(response, body.is_empty())
19129 }
19130 } {
19131 Ok(mut send_stream) => {
19132 state.send_response = None;
19133 if body.is_empty() {
19134 streams_guard.remove(&stream_id);
19135 if let Ok(mut shared_guard) = shared.lock() {
19136 shared_guard.streams.remove(&stream_id);
19137 }
19138 } else {
19139 if let Err(error) = send_stream.send_data(Bytes::from(body), true) {
19140 let _ = respond_to.send(Err(error.to_string()));
19141 continue;
19142 }
19143 streams_guard.remove(&stream_id);
19144 if let Ok(mut shared_guard) = shared.lock() {
19145 shared_guard.streams.remove(&stream_id);
19146 }
19147 }
19148 push_http2_server_event(
19149 &shared,
19150 server_id,
19151 Http2BridgeEvent {
19152 kind: String::from("serverStreamClose"),
19153 id: stream_id,
19154 extra_number: Some(0),
19155 ..Http2BridgeEvent::default()
19156 },
19157 );
19158 let _ = respond_to.send(Ok(Value::Null));
19159 }
19160 Err(error) => {
19161 let _ = respond_to.send(Err(error.to_string()));
19162 }
19163 }
19164 }
19165 Http2SessionCommand::Request { respond_to, .. } => {
19166 let _ = respond_to.send(Err(String::from("HTTP/2 server sessions cannot initiate client requests")));
19167 }
19168 }
19169 }
19170 else => break,
19171 }
19172 }
19173 });
19174 });
19175}
19176
19177fn spawn_http2_server_accept_loop(
19178 shared: Arc<Mutex<crate::state::Http2SharedState>>,
19179 server_id: u64,
19180 listener: TcpListener,
19181) {
19182 thread::spawn(move || {
19183 let listener = listener;
19184 loop {
19185 let closed = shared
19186 .lock()
19187 .ok()
19188 .and_then(|state| {
19189 state
19190 .servers
19191 .get(&server_id)
19192 .map(|server| server.closed.load(Ordering::SeqCst))
19193 })
19194 .unwrap_or(true);
19195 if closed {
19196 break;
19197 }
19198 match listener.accept() {
19199 Ok((stream, _)) => {
19200 let (command_tx, command_rx) = unbounded_channel();
19201 let (guest_local_addr, secure, tls) = {
19202 let state = shared.lock().expect("http2 shared state");
19203 let server = state.servers.get(&server_id).expect("http2 server state");
19204 (server.guest_local_addr, server.secure, server.tls.clone())
19205 };
19206 let (local_addr, remote_addr) = match (stream.local_addr(), stream.peer_addr())
19207 {
19208 (Ok(local_addr), Ok(remote_addr)) => (local_addr, remote_addr),
19209 _ => continue,
19210 };
19211 let session_snapshot = Arc::new(Mutex::new(Http2SessionSnapshot {
19212 encrypted: secure,
19213 alpn_protocol: Some(if secure {
19214 String::from("h2")
19215 } else {
19216 String::from("h2c")
19217 }),
19218 local_settings: BTreeMap::new(),
19219 remote_settings: BTreeMap::new(),
19220 state: http2_runtime_snapshot(),
19221 socket: Http2SocketSnapshot {
19222 local_address: Some(guest_local_addr.ip().to_string()),
19223 local_port: Some(guest_local_addr.port()),
19224 local_family: Some(socket_addr_family(&guest_local_addr).to_string()),
19225 remote_address: Some(remote_addr.ip().to_string()),
19226 remote_port: Some(remote_addr.port()),
19227 remote_family: Some(socket_addr_family(&remote_addr).to_string()),
19228 ..http2_socket_snapshot(local_addr, remote_addr)
19229 },
19230 ..Http2SessionSnapshot::default()
19231 }));
19232 let session_id = {
19233 let mut state = shared.lock().expect("http2 shared state");
19234 let session_id = next_http2_session_id(&mut state);
19235 state
19236 .sessions
19237 .insert(session_id, ActiveHttp2Session { command_tx });
19238 session_id
19239 };
19240 spawn_http2_server_session(
19241 Arc::clone(&shared),
19242 server_id,
19243 session_id,
19244 stream,
19245 tls,
19246 session_snapshot,
19247 command_rx,
19248 );
19249 }
19250 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
19251 thread::sleep(HTTP2_POLL_DELAY);
19252 }
19253 Err(error) => {
19254 push_http2_server_event(
19255 &shared,
19256 server_id,
19257 Http2BridgeEvent {
19258 kind: String::from("serverStreamError"),
19259 id: server_id,
19260 data: Some(http2_error_payload(error.to_string())),
19261 ..Http2BridgeEvent::default()
19262 },
19263 );
19264 thread::sleep(HTTP2_POLL_DELAY);
19265 }
19266 }
19267 }
19268 });
19269}
19270
19271fn send_http2_command(
19272 session: &ActiveHttp2Session,
19273 command: impl FnOnce(Sender<Result<Value, String>>) -> Http2SessionCommand,
19274) -> Result<Value, SidecarError> {
19275 let (respond_to, response_rx) = mpsc::channel();
19276 session.command_tx.send(command(respond_to)).map_err(|_| {
19277 SidecarError::InvalidState(String::from("HTTP/2 session command channel closed"))
19278 })?;
19279 response_rx
19280 .recv_timeout(Duration::from_secs(30))
19281 .map_err(|_| {
19282 SidecarError::Execution(String::from("timed out waiting for HTTP/2 session command"))
19283 })?
19284 .map_err(SidecarError::Execution)
19285}
19286
19287fn parse_http2_server_listen_payload(
19288 request: &JavascriptSyncRpcRequest,
19289) -> Result<JavascriptHttp2ServerListenRequest, SidecarError> {
19290 let payload_json =
19291 javascript_sync_rpc_arg_str(&request.args, 0, "net.http2_server_listen payload")?;
19292 serde_json::from_str(payload_json).map_err(|error| {
19293 SidecarError::InvalidState(format!(
19294 "net.http2_server_listen payload must be valid JSON: {error}"
19295 ))
19296 })
19297}
19298
19299fn parse_http2_connect_payload(
19300 request: &JavascriptSyncRpcRequest,
19301) -> Result<JavascriptHttp2SessionConnectRequest, SidecarError> {
19302 let payload_json =
19303 javascript_sync_rpc_arg_str(&request.args, 0, "net.http2_session_connect payload")?;
19304 serde_json::from_str(payload_json).map_err(|error| {
19305 SidecarError::InvalidState(format!(
19306 "net.http2_session_connect payload must be valid JSON: {error}"
19307 ))
19308 })
19309}
19310
19311fn http2_session_for_id(
19312 process: &ActiveProcess,
19313 session_id: u64,
19314) -> Result<ActiveHttp2Session, SidecarError> {
19315 let shared = process
19316 .http2
19317 .shared
19318 .lock()
19319 .map_err(|_| SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned")))?;
19320 shared
19321 .sessions
19322 .get(&session_id)
19323 .cloned()
19324 .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 session {session_id}")))
19325}
19326
19327fn http2_stream_for_id(
19328 process: &ActiveProcess,
19329 stream_id: u64,
19330) -> Result<ActiveHttp2Stream, SidecarError> {
19331 let shared = process
19332 .http2
19333 .shared
19334 .lock()
19335 .map_err(|_| SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned")))?;
19336 shared
19337 .streams
19338 .get(&stream_id)
19339 .cloned()
19340 .ok_or_else(|| SidecarError::InvalidState(format!("unknown HTTP/2 stream {stream_id}")))
19341}
19342
19343fn service_javascript_http2_sync_rpc<B>(
19344 request: JavascriptHttp2SyncRpcServiceRequest<'_, B>,
19345) -> Result<Value, SidecarError>
19346where
19347 B: NativeSidecarBridge + Send + 'static,
19348 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19349{
19350 let JavascriptHttp2SyncRpcServiceRequest {
19351 bridge,
19352 kernel,
19353 vm_id,
19354 dns,
19355 socket_paths,
19356 process,
19357 sync_request: request,
19358 resource_limits,
19359 network_counts,
19360 } = request;
19361 match request.method.as_str() {
19362 "net.http2_server_listen" => {
19363 check_network_resource_limit(
19364 resource_limits.max_sockets,
19365 network_counts.sockets,
19366 1,
19367 "socket",
19368 )?;
19369 let payload = parse_http2_server_listen_payload(request)?;
19370 let (family, bind_host, guest_host) =
19371 normalize_tcp_listen_host(payload.host.as_deref())?;
19372 let requested_port = payload.port.unwrap_or(0);
19373 bridge.require_network_access(
19374 vm_id,
19375 NetworkOperation::Listen,
19376 format_tcp_resource(bind_host, requested_port),
19377 )?;
19378 let port = allocate_guest_listen_port(
19379 requested_port,
19380 family,
19381 &socket_paths.used_tcp_guest_ports,
19382 socket_paths.listen_policy,
19383 )?;
19384 let mut listener =
19385 ActiveTcpListener::bind(bind_host, guest_host, port, payload.backlog)?;
19386 let guest_local_addr = listener.guest_local_addr();
19387 let closed = Arc::new(AtomicBool::new(false));
19388 {
19389 let mut state = process.http2.shared.lock().map_err(|_| {
19390 SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19391 })?;
19392 state.servers.insert(
19393 payload.server_id,
19394 ActiveHttp2Server {
19395 actual_local_addr: listener.local_addr(),
19396 guest_local_addr,
19397 secure: payload.secure,
19398 tls: payload.tls.clone().map(|mut tls| {
19399 tls.is_server = payload.secure;
19400 if payload.secure && tls.alpn_protocols.is_none() {
19401 tls.alpn_protocols = Some(vec![String::from("h2")]);
19402 }
19403 tls
19404 }),
19405 closed: Arc::clone(&closed),
19406 },
19407 );
19408 state.server_events.entry(payload.server_id).or_default();
19409 }
19410 spawn_http2_server_accept_loop(
19411 Arc::clone(&process.http2.shared),
19412 payload.server_id,
19413 listener.listener.take().ok_or_else(|| {
19414 SidecarError::InvalidState(String::from(
19415 "HTTP/2 listener missing host TCP socket",
19416 ))
19417 })?,
19418 );
19419 javascript_net_json_string(
19420 json!({
19421 "address": {
19422 "address": guest_local_addr.ip().to_string(),
19423 "family": socket_addr_family(&guest_local_addr),
19424 "port": guest_local_addr.port(),
19425 }
19426 }),
19427 "net.http2_server_listen",
19428 )
19429 }
19430 "net.http2_server_poll" => {
19431 let server_id =
19432 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_poll server id")?;
19433 let wait_ms = javascript_sync_rpc_arg_u64_optional(
19434 &request.args,
19435 1,
19436 "net.http2_server_poll wait ms",
19437 )?
19438 .unwrap_or_default();
19439 match wait_for_http2_event(&process.http2.shared, server_id, true, wait_ms) {
19440 Some(event) => http2_event_value(&event),
19441 None => Ok(Value::Null),
19442 }
19443 }
19444 "net.http2_server_wait" => {
19445 let server_id =
19446 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_wait server id")?;
19447 dispatch_http2_wait_loop(process, server_id, true)
19448 }
19449 "net.http2_server_close" => {
19450 let server_id =
19451 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_server_close server id")?;
19452 let server = {
19453 let mut state = process.http2.shared.lock().map_err(|_| {
19454 SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19455 })?;
19456 state.servers.remove(&server_id)
19457 }
19458 .ok_or_else(|| {
19459 SidecarError::InvalidState(format!("unknown HTTP/2 server {server_id}"))
19460 })?;
19461 server.closed.store(true, Ordering::SeqCst);
19462 push_http2_server_event(
19463 &process.http2.shared,
19464 server_id,
19465 Http2BridgeEvent {
19466 kind: String::from("serverClose"),
19467 id: server_id,
19468 ..Http2BridgeEvent::default()
19469 },
19470 );
19471 Ok(Value::Null)
19472 }
19473 "net.http2_server_respond" => {
19474 let server_id = javascript_sync_rpc_arg_u64(
19475 &request.args,
19476 0,
19477 "net.http2_server_respond server id",
19478 )?;
19479 let request_id = javascript_sync_rpc_arg_u64(
19480 &request.args,
19481 1,
19482 "net.http2_server_respond request id",
19483 )?;
19484 let response_json =
19485 javascript_sync_rpc_arg_str(&request.args, 2, "net.http2_server_respond payload")?;
19486 ensure_vm_fetch_response_within_limit(
19487 response_json,
19488 "net.http2_server_respond",
19489 VM_FETCH_BUFFER_LIMIT_BYTES,
19490 )?;
19491 serde_json::from_str::<Value>(response_json).map_err(|error| {
19492 SidecarError::Execution(format!(
19493 "net.http2_server_respond payload must be valid JSON: {error}"
19494 ))
19495 })?;
19496 let Some(pending) = process
19497 .pending_http_requests
19498 .get_mut(&(server_id, request_id))
19499 else {
19500 return Err(SidecarError::InvalidState(format!(
19501 "unknown pending HTTP/2 request {request_id} for server {server_id}"
19502 )));
19503 };
19504 *pending = Some(response_json.to_owned());
19505 Ok(Value::Bool(true))
19506 }
19507 "net.http2_session_connect" => {
19508 check_network_resource_limit(
19509 resource_limits.max_sockets,
19510 network_counts.sockets,
19511 1,
19512 "socket",
19513 )?;
19514 check_network_resource_limit(
19515 resource_limits.max_connections,
19516 network_counts.connections,
19517 1,
19518 "connection",
19519 )?;
19520 let payload = parse_http2_connect_payload(request)?;
19521 let authority = payload.authority.clone().unwrap_or_else(|| {
19522 format!(
19523 "{}://{}:{}",
19524 payload.protocol.as_deref().unwrap_or("http"),
19525 payload.host.as_deref().unwrap_or("localhost"),
19526 payload.port.unwrap_or(80)
19527 )
19528 });
19529 let url = Url::parse(&authority).map_err(|error| {
19530 SidecarError::InvalidState(format!(
19531 "invalid HTTP/2 authority {authority:?}: {error}"
19532 ))
19533 })?;
19534 let secure = url.scheme() == "https" || payload.protocol.as_deref() == Some("https:");
19535 let host = payload
19536 .host
19537 .as_deref()
19538 .or_else(|| url.host_str())
19539 .unwrap_or("localhost");
19540 let port = payload.port.or_else(|| url.port()).unwrap_or(80);
19541 bridge.require_network_access(
19542 vm_id,
19543 NetworkOperation::Http,
19544 format_tcp_resource(host, port),
19545 )?;
19546 let resolved = {
19547 let shared = process.http2.shared.lock().map_err(|_| {
19548 SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19549 })?;
19550 shared
19551 .servers
19552 .values()
19553 .find(|server| {
19554 is_loopback_request_host(host) && server.guest_local_addr.port() == port
19555 })
19556 .map(|server| ResolvedTcpConnectAddr {
19557 actual_addr: server.actual_local_addr,
19558 guest_remote_addr: server.guest_local_addr,
19559 use_kernel_loopback: false,
19560 })
19561 };
19562 let resolved = match resolved {
19563 Some(resolved) => resolved,
19564 None => {
19565 resolve_tcp_connect_addr(bridge, kernel, vm_id, dns, host, port, socket_paths)?
19566 }
19567 };
19568 let (command_tx, command_rx) = unbounded_channel();
19569 let snapshot = Arc::new(Mutex::new(Http2SessionSnapshot {
19570 encrypted: secure,
19571 alpn_protocol: Some(String::from(if secure { "h2" } else { "h2c" })),
19572 local_settings: http2_settings_from_value(&payload.settings),
19573 remote_settings: BTreeMap::new(),
19574 state: http2_runtime_snapshot(),
19575 socket: Http2SocketSnapshot {
19576 encrypted: secure,
19577 remote_address: Some(resolved.guest_remote_addr.ip().to_string()),
19578 remote_port: Some(resolved.guest_remote_addr.port()),
19579 remote_family: Some(
19580 socket_addr_family(&resolved.guest_remote_addr).to_string(),
19581 ),
19582 servername: if secure {
19583 payload
19584 .tls
19585 .as_ref()
19586 .and_then(|tls| tls.servername.clone())
19587 .or_else(|| Some(host.to_string()))
19588 } else {
19589 None
19590 },
19591 alpn_protocol: Some(String::from(if secure { "h2" } else { "h2c" })),
19592 ..Http2SocketSnapshot::default()
19593 },
19594 ..Http2SessionSnapshot::default()
19595 }));
19596 let session_id = {
19597 let mut state = process.http2.shared.lock().map_err(|_| {
19598 SidecarError::InvalidState(String::from("HTTP/2 state lock poisoned"))
19599 })?;
19600 let session_id = next_http2_session_id(&mut state);
19601 state
19602 .sessions
19603 .insert(session_id, ActiveHttp2Session { command_tx });
19604 state.session_events.entry(session_id).or_default();
19605 session_id
19606 };
19607 spawn_http2_client_session(
19608 Arc::clone(&process.http2.shared),
19609 session_id,
19610 resolved.actual_addr,
19611 if secure {
19612 Some(payload.tls.unwrap_or(JavascriptTlsBridgeOptions {
19613 is_server: false,
19614 servername: Some(host.to_string()),
19615 alpn_protocols: Some(vec![String::from("h2")]),
19616 ..JavascriptTlsBridgeOptions::default()
19617 }))
19618 } else {
19619 None
19620 },
19621 Arc::clone(&snapshot),
19622 command_rx,
19623 );
19624 let snapshot_json =
19625 http2_snapshot_json(&snapshot.lock().expect("http2 snapshot lock").clone())?;
19626 javascript_net_json_string(
19627 json!({
19628 "sessionId": session_id,
19629 "state": snapshot_json,
19630 }),
19631 "net.http2_session_connect",
19632 )
19633 }
19634 "net.http2_session_request" => {
19635 let session_id = javascript_sync_rpc_arg_u64(
19636 &request.args,
19637 0,
19638 "net.http2_session_request session id",
19639 )?;
19640 let headers_json =
19641 javascript_sync_rpc_arg_str(&request.args, 1, "net.http2_session_request headers")?;
19642 let options_json =
19643 javascript_sync_rpc_arg_str(&request.args, 2, "net.http2_session_request options")?;
19644 let session = http2_session_for_id(process, session_id)?;
19645 send_http2_command(&session, |respond_to| Http2SessionCommand::Request {
19646 headers_json: headers_json.to_owned(),
19647 options_json: options_json.to_owned(),
19648 respond_to,
19649 })
19650 }
19651 "net.http2_session_settings" => {
19652 let session_id = javascript_sync_rpc_arg_u64(
19653 &request.args,
19654 0,
19655 "net.http2_session_settings session id",
19656 )?;
19657 let settings_json = javascript_sync_rpc_arg_str(
19658 &request.args,
19659 1,
19660 "net.http2_session_settings settings",
19661 )?;
19662 let session = http2_session_for_id(process, session_id)?;
19663 send_http2_command(&session, |respond_to| Http2SessionCommand::Settings {
19664 settings_json: settings_json.to_owned(),
19665 respond_to,
19666 })
19667 }
19668 "net.http2_session_set_local_window_size" => {
19669 let session_id = javascript_sync_rpc_arg_u64(
19670 &request.args,
19671 0,
19672 "net.http2_session_set_local_window_size session id",
19673 )?;
19674 let window_size = javascript_sync_rpc_arg_u64(
19675 &request.args,
19676 1,
19677 "net.http2_session_set_local_window_size window size",
19678 )?;
19679 let session = http2_session_for_id(process, session_id)?;
19680 send_http2_command(&session, |respond_to| {
19681 Http2SessionCommand::SetLocalWindowSize {
19682 size: window_size as u32,
19683 respond_to,
19684 }
19685 })
19686 }
19687 "net.http2_session_goaway" => {
19688 let session_id = javascript_sync_rpc_arg_u64(
19689 &request.args,
19690 0,
19691 "net.http2_session_goaway session id",
19692 )?;
19693 let error_code = javascript_sync_rpc_arg_u64(
19694 &request.args,
19695 1,
19696 "net.http2_session_goaway error code",
19697 )?;
19698 let last_stream_id = javascript_sync_rpc_arg_u64(
19699 &request.args,
19700 2,
19701 "net.http2_session_goaway last stream id",
19702 )?;
19703 let opaque_data = request
19704 .args
19705 .get(3)
19706 .and_then(Value::as_str)
19707 .map(|value| {
19708 base64::engine::general_purpose::STANDARD
19709 .decode(value)
19710 .map_err(|error| {
19711 SidecarError::InvalidState(format!("invalid GOAWAY payload: {error}"))
19712 })
19713 })
19714 .transpose()?;
19715 let session = http2_session_for_id(process, session_id)?;
19716 send_http2_command(&session, |respond_to| Http2SessionCommand::Goaway {
19717 error_code: error_code as u32,
19718 last_stream_id: last_stream_id as u32,
19719 opaque_data,
19720 respond_to,
19721 })
19722 }
19723 "net.http2_session_close" | "net.http2_session_destroy" => {
19724 let session_id = javascript_sync_rpc_arg_u64(
19725 &request.args,
19726 0,
19727 "net.http2_session_close session id",
19728 )?;
19729 let session = http2_session_for_id(process, session_id)?;
19730 send_http2_command(&session, |respond_to| Http2SessionCommand::Close {
19731 abrupt: request.method == "net.http2_session_destroy",
19732 respond_to,
19733 })
19734 }
19735 "net.http2_session_poll" => {
19736 let session_id =
19737 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_session_poll session id")?;
19738 let wait_ms = javascript_sync_rpc_arg_u64_optional(
19739 &request.args,
19740 1,
19741 "net.http2_session_poll wait ms",
19742 )?
19743 .unwrap_or_default();
19744 match wait_for_http2_event(&process.http2.shared, session_id, false, wait_ms) {
19745 Some(event) => http2_event_value(&event),
19746 None => Ok(Value::Null),
19747 }
19748 }
19749 "net.http2_session_wait" => {
19750 let session_id =
19751 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_session_wait session id")?;
19752 dispatch_http2_wait_loop(process, session_id, false)
19753 }
19754 "net.http2_stream_respond" => {
19755 let stream_id = javascript_sync_rpc_arg_u64(
19756 &request.args,
19757 0,
19758 "net.http2_stream_respond stream id",
19759 )?;
19760 let headers_json =
19761 javascript_sync_rpc_arg_str(&request.args, 1, "net.http2_stream_respond headers")?;
19762 let stream = http2_stream_for_id(process, stream_id)?;
19763 let session = http2_session_for_id(process, stream.session_id)?;
19764 send_http2_command(&session, |respond_to| Http2SessionCommand::StreamRespond {
19765 stream_id,
19766 headers_json: headers_json.to_owned(),
19767 respond_to,
19768 })
19769 }
19770 "net.http2_stream_push_stream" => {
19771 let stream_id = javascript_sync_rpc_arg_u64(
19772 &request.args,
19773 0,
19774 "net.http2_stream_push_stream stream id",
19775 )?;
19776 let headers_json = javascript_sync_rpc_arg_str(
19777 &request.args,
19778 1,
19779 "net.http2_stream_push_stream headers",
19780 )?;
19781 let _options_json = javascript_sync_rpc_arg_str(
19782 &request.args,
19783 2,
19784 "net.http2_stream_push_stream options",
19785 )?;
19786 let stream = http2_stream_for_id(process, stream_id)?;
19787 let session = http2_session_for_id(process, stream.session_id)?;
19788 send_http2_command(&session, |respond_to| Http2SessionCommand::StreamPush {
19789 stream_id,
19790 headers_json: headers_json.to_owned(),
19791 respond_to,
19792 })
19793 }
19794 "net.http2_stream_write" => {
19795 let stream_id =
19796 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_write stream id")?;
19797 let chunk =
19798 javascript_sync_rpc_base64_arg(&request.args, 1, "net.http2_stream_write data")?;
19799 let stream = http2_stream_for_id(process, stream_id)?;
19800 let session = http2_session_for_id(process, stream.session_id)?;
19801 send_http2_command(&session, |respond_to| Http2SessionCommand::StreamWrite {
19802 stream_id,
19803 chunk,
19804 end_stream: false,
19805 respond_to,
19806 })
19807 }
19808 "net.http2_stream_end" => {
19809 let stream_id =
19810 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_end stream id")?;
19811 let chunk = request
19812 .args
19813 .get(1)
19814 .and_then(Value::as_str)
19815 .map(|value| {
19816 base64::engine::general_purpose::STANDARD
19817 .decode(value)
19818 .map_err(|error| {
19819 SidecarError::InvalidState(format!(
19820 "invalid HTTP/2 stream payload: {error}"
19821 ))
19822 })
19823 })
19824 .transpose()?
19825 .unwrap_or_default();
19826 let stream = http2_stream_for_id(process, stream_id)?;
19827 let session = http2_session_for_id(process, stream.session_id)?;
19828 send_http2_command(&session, |respond_to| Http2SessionCommand::StreamWrite {
19829 stream_id,
19830 chunk,
19831 end_stream: true,
19832 respond_to,
19833 })
19834 }
19835 "net.http2_stream_close" => {
19836 let stream_id =
19837 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_close stream id")?;
19838 let code = javascript_sync_rpc_arg_u64_optional(
19839 &request.args,
19840 1,
19841 "net.http2_stream_close error code",
19842 )?
19843 .map(|value| value as u32);
19844 let stream = http2_stream_for_id(process, stream_id)?;
19845 let session = http2_session_for_id(process, stream.session_id)?;
19846 send_http2_command(&session, |respond_to| Http2SessionCommand::StreamClose {
19847 stream_id,
19848 error_code: code,
19849 respond_to,
19850 })
19851 }
19852 "net.http2_stream_pause" => {
19853 let stream_id =
19854 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_pause stream id")?;
19855 let stream = http2_stream_for_id(process, stream_id)?;
19856 stream.paused.store(true, Ordering::SeqCst);
19857 Ok(Value::Null)
19858 }
19859 "net.http2_stream_resume" => {
19860 let stream_id =
19861 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http2_stream_resume stream id")?;
19862 let stream = http2_stream_for_id(process, stream_id)?;
19863 stream.paused.store(false, Ordering::SeqCst);
19864 Ok(Value::Null)
19865 }
19866 "net.http2_stream_respond_with_file" => {
19867 let stream_id = javascript_sync_rpc_arg_u64(
19868 &request.args,
19869 0,
19870 "net.http2_stream_respond_with_file stream id",
19871 )?;
19872 let path = javascript_sync_rpc_arg_str(
19873 &request.args,
19874 1,
19875 "net.http2_stream_respond_with_file path",
19876 )?;
19877 let headers_json = javascript_sync_rpc_arg_str(
19878 &request.args,
19879 2,
19880 "net.http2_stream_respond_with_file headers",
19881 )?;
19882 let options_json = javascript_sync_rpc_arg_str(
19883 &request.args,
19884 3,
19885 "net.http2_stream_respond_with_file options",
19886 )?;
19887 let stream = http2_stream_for_id(process, stream_id)?;
19888 let session = http2_session_for_id(process, stream.session_id)?;
19889 let guest_path = resolve_http2_file_response_guest_path(process, path);
19890 let body = kernel.read_file(&guest_path).map_err(kernel_error)?;
19891 send_http2_command(&session, |respond_to| {
19892 Http2SessionCommand::StreamRespondWithFile {
19893 stream_id,
19894 body,
19895 headers_json: headers_json.to_owned(),
19896 options_json: options_json.to_owned(),
19897 respond_to,
19898 }
19899 })
19900 }
19901 other => Err(SidecarError::InvalidState(format!(
19902 "unsupported JavaScript HTTP/2 sync RPC method {other}"
19903 ))),
19904 }
19905}
19906
19907const JAVASCRIPT_NET_POLL_MAX_WAIT: Duration = Duration::from_millis(50);
19908const EXITED_PROCESS_SNAPSHOT_RETENTION: Duration = Duration::from_secs(2);
19909
19910fn resolve_http2_file_response_guest_path(process: &ActiveProcess, path: &str) -> String {
19911 if Path::new(path).is_absolute() {
19912 normalize_path(path)
19913 } else {
19914 normalize_path(&format!("{}/{}", process.guest_cwd, path))
19915 }
19916}
19917
19918pub(crate) fn clamp_javascript_net_poll_wait(wait_ms: u64) -> Duration {
19919 if wait_ms == 0 {
19922 Duration::ZERO
19923 } else {
19924 Duration::from_millis(wait_ms).min(JAVASCRIPT_NET_POLL_MAX_WAIT)
19925 }
19926}
19927
19928pub(crate) fn service_javascript_net_sync_rpc<B>(
19929 request: JavascriptNetSyncRpcServiceRequest<'_, B>,
19930) -> Result<Value, SidecarError>
19931where
19932 B: NativeSidecarBridge + Send + 'static,
19933 BridgeError<B>: fmt::Debug + Send + Sync + 'static,
19934{
19935 let JavascriptNetSyncRpcServiceRequest {
19936 bridge,
19937 vm_id,
19938 dns,
19939 socket_paths,
19940 kernel,
19941 process,
19942 sync_request: request,
19943 resource_limits,
19944 network_counts,
19945 } = request;
19946 match request.method.as_str() {
19947 "net.http_listen" => {
19948 check_network_resource_limit(
19949 resource_limits.max_sockets,
19950 network_counts.sockets,
19951 1,
19952 "socket",
19953 )?;
19954 let payload_json =
19955 javascript_sync_rpc_arg_str(&request.args, 0, "net.http_listen payload")?;
19956 let payload: JavascriptHttpListenRequest =
19957 serde_json::from_str(payload_json).map_err(|error| {
19958 SidecarError::InvalidState(format!(
19959 "net.http_listen payload must be valid JSON: {error}"
19960 ))
19961 })?;
19962 let (family, bind_host, guest_host) =
19963 normalize_tcp_listen_host(payload.hostname.as_deref())?;
19964 let requested_port = payload.port.unwrap_or(0);
19965 bridge.require_network_access(
19966 vm_id,
19967 NetworkOperation::Listen,
19968 format_tcp_resource(bind_host, requested_port),
19969 )?;
19970 let port = allocate_guest_listen_port(
19971 requested_port,
19972 family,
19973 &socket_paths.used_tcp_guest_ports,
19974 socket_paths.listen_policy,
19975 )?;
19976 let mut listener = ActiveTcpListener::bind(
19977 bind_host,
19978 guest_host,
19979 port,
19980 Some(DEFAULT_JAVASCRIPT_NET_BACKLOG),
19981 )?;
19982 let guest_local_addr = listener.guest_local_addr();
19983 process.http_servers.insert(
19984 payload.server_id,
19985 ActiveHttpServer {
19986 listener: listener.listener.take().ok_or_else(|| {
19987 SidecarError::InvalidState(String::from(
19988 "HTTP listener missing host TCP socket",
19989 ))
19990 })?,
19991 guest_local_addr,
19992 next_request_id: 0,
19993 },
19994 );
19995 serde_json::to_string(&json!({
19996 "address": {
19997 "address": guest_local_addr.ip().to_string(),
19998 "family": socket_addr_family(&guest_local_addr),
19999 "port": guest_local_addr.port(),
20000 }
20001 }))
20002 .map(Value::String)
20003 .map_err(|error| SidecarError::Execution(format!("ERR_AGENTOS_NODE_SYNC_RPC: {error}")))
20004 }
20005 "net.http_close" => {
20006 let server_id =
20007 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_close server id")?;
20008 let server = process.http_servers.remove(&server_id).ok_or_else(|| {
20009 SidecarError::InvalidState(format!("unknown HTTP server {server_id}"))
20010 })?;
20011 drop(server.listener);
20012 process
20013 .pending_http_requests
20014 .retain(|(pending_server_id, _), _| *pending_server_id != server_id);
20015 Ok(Value::Null)
20016 }
20017 "net.http_wait" => {
20018 let server_id =
20019 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_wait server id")?;
20020 dispatch_http_wait_loop(process, server_id)
20021 }
20022 "net.http_respond" => {
20023 let server_id =
20024 javascript_sync_rpc_arg_u64(&request.args, 0, "net.http_respond server id")?;
20025 let request_id =
20026 javascript_sync_rpc_arg_u64(&request.args, 1, "net.http_respond request id")?;
20027 let response_json =
20028 javascript_sync_rpc_arg_str(&request.args, 2, "net.http_respond payload")?;
20029 ensure_vm_fetch_response_within_limit(
20030 response_json,
20031 "net.http_respond",
20032 VM_FETCH_BUFFER_LIMIT_BYTES,
20033 )?;
20034 serde_json::from_str::<Value>(response_json).map_err(|error| {
20035 SidecarError::Execution(format!(
20036 "net.http_respond payload must be valid JSON: {error}"
20037 ))
20038 })?;
20039 let Some(pending) = process
20040 .pending_http_requests
20041 .get_mut(&(server_id, request_id))
20042 else {
20043 return Err(SidecarError::InvalidState(format!(
20044 "unknown pending HTTP request {request_id} for server {server_id}"
20045 )));
20046 };
20047 *pending = Some(response_json.to_owned());
20048 Ok(Value::Null)
20049 }
20050 "net.reserve_tcp_port" => {
20051 let payload = request
20052 .args
20053 .first()
20054 .cloned()
20055 .ok_or_else(|| {
20056 SidecarError::InvalidState(String::from(
20057 "net.reserve_tcp_port requires a request payload",
20058 ))
20059 })
20060 .and_then(|value| {
20061 serde_json::from_value::<JavascriptNetReserveTcpPortRequest>(value).map_err(
20062 |error| {
20063 SidecarError::InvalidState(format!(
20064 "invalid net.reserve_tcp_port payload: {error}"
20065 ))
20066 },
20067 )
20068 })?;
20069 let (family, _bind_host, guest_host) =
20070 normalize_tcp_listen_host(payload.host.as_deref())?;
20071 let requested_port = payload.port.unwrap_or(0);
20072 let port = allocate_guest_listen_port(
20073 requested_port,
20074 family,
20075 &socket_paths.used_tcp_guest_ports,
20076 socket_paths.listen_policy,
20077 )?;
20078 let reservation_id = process.allocate_tcp_port_reservation_id();
20079 process
20080 .tcp_port_reservations
20081 .insert(reservation_id.clone(), (family, port));
20082 Ok(json!({
20083 "reservationId": reservation_id,
20084 "localAddress": guest_host,
20085 "localPort": port,
20086 "family": match family {
20087 JavascriptSocketFamily::Ipv4 => "IPv4",
20088 JavascriptSocketFamily::Ipv6 => "IPv6",
20089 },
20090 }))
20091 }
20092 "net.release_tcp_port" => {
20093 let reservation_id =
20094 javascript_sync_rpc_arg_str(&request.args, 0, "net.release_tcp_port reservation")?;
20095 process.tcp_port_reservations.remove(reservation_id);
20096 Ok(Value::Null)
20097 }
20098 "net.connect" => {
20099 check_network_resource_limit(
20100 resource_limits.max_sockets,
20101 network_counts.sockets,
20102 1,
20103 "socket",
20104 )?;
20105 check_network_resource_limit(
20106 resource_limits.max_connections,
20107 network_counts.connections,
20108 1,
20109 "connection",
20110 )?;
20111 let payload = request
20112 .args
20113 .first()
20114 .cloned()
20115 .ok_or_else(|| {
20116 SidecarError::InvalidState(String::from(
20117 "net.connect requires a request payload",
20118 ))
20119 })
20120 .and_then(|value| {
20121 serde_json::from_value::<JavascriptNetConnectRequest>(value).map_err(|error| {
20122 SidecarError::InvalidState(format!("invalid net.connect payload: {error}"))
20123 })
20124 })?;
20125 if let Some(path) = payload.path.as_deref() {
20126 let guest_path = normalize_path(path);
20127 let host_path = resolve_guest_socket_host_path(socket_paths, &guest_path);
20128 let socket = ActiveUnixSocket::connect(&host_path, &guest_path)?;
20129 let socket_id = process.allocate_unix_socket_id();
20130 process.unix_sockets.insert(socket_id.clone(), socket);
20131 Ok(json!({
20132 "socketId": socket_id,
20133 "remotePath": guest_path,
20134 }))
20135 } else {
20136 let port = payload.port.ok_or_else(|| {
20137 SidecarError::InvalidState(String::from(
20138 "net.connect requires either a path or port",
20139 ))
20140 })?;
20141 let host = payload.host.as_deref().unwrap_or("localhost");
20142 let local_reservation = payload.local_reservation.as_deref().and_then(|id| {
20143 process
20144 .tcp_port_reservations
20145 .remove(id)
20146 .map(|reservation| (id.to_owned(), reservation))
20147 });
20148 bridge.require_network_access(
20149 vm_id,
20150 NetworkOperation::Http,
20151 format_tcp_resource(host, port),
20152 )?;
20153 if is_loopback_socket_host(host) {
20154 let families = [JavascriptSocketFamily::Ipv4, JavascriptSocketFamily::Ipv6];
20155 if let Some((family, target)) = families.iter().find_map(|family| {
20156 socket_paths
20157 .http_loopback_target(*family, port)
20158 .map(|target| (*family, target))
20159 }) {
20160 if let Some((reservation_id, reservation)) = local_reservation {
20161 process
20162 .tcp_port_reservations
20163 .insert(reservation_id, reservation);
20164 }
20165 let remote_address = match family {
20166 JavascriptSocketFamily::Ipv4 => "127.0.0.1",
20167 JavascriptSocketFamily::Ipv6 => "::1",
20168 };
20169 return Ok(json!({
20170 "loopbackHttpTarget": {
20171 "processId": target.process_id.clone(),
20172 "serverId": target.server_id,
20173 "host": remote_address,
20174 "port": port,
20175 },
20176 "localAddress": match family {
20177 JavascriptSocketFamily::Ipv4 => "127.0.0.1",
20178 JavascriptSocketFamily::Ipv6 => "::1",
20179 },
20180 "localPort": payload.local_port.unwrap_or(0),
20181 "remoteAddress": remote_address,
20182 "remotePort": port,
20183 "remoteFamily": match family {
20184 JavascriptSocketFamily::Ipv4 => "IPv4",
20185 JavascriptSocketFamily::Ipv6 => "IPv6",
20186 },
20187 }));
20188 }
20189 }
20190 let connect_result = ActiveTcpSocket::connect(ActiveTcpConnectRequest {
20191 bridge,
20192 kernel,
20193 kernel_pid: process.kernel_pid,
20194 vm_id,
20195 dns,
20196 host,
20197 port,
20198 local_address: payload.local_address.as_deref(),
20199 local_port: payload.local_port,
20200 local_reservation: local_reservation
20201 .as_ref()
20202 .map(|(_, reservation)| *reservation),
20203 context: socket_paths,
20204 });
20205 if let Err(error) = connect_result {
20206 if let Some((reservation_id, reservation)) = local_reservation {
20207 process
20208 .tcp_port_reservations
20209 .insert(reservation_id, reservation);
20210 }
20211 return Err(error);
20212 }
20213 let socket = connect_result?;
20214 let socket_id = process.allocate_tcp_socket_id();
20215 let local_addr = socket.guest_local_addr;
20216 let remote_addr = socket.guest_remote_addr;
20217 process.tcp_sockets.insert(socket_id.clone(), socket);
20218 Ok(json!({
20219 "socketId": socket_id,
20220 "localAddress": local_addr.ip().to_string(),
20221 "localPort": local_addr.port(),
20222 "remoteAddress": remote_addr.ip().to_string(),
20223 "remotePort": remote_addr.port(),
20224 "remoteFamily": socket_addr_family(&remote_addr),
20225 }))
20226 }
20227 }
20228 "net.listen" => {
20229 check_network_resource_limit(
20230 resource_limits.max_sockets,
20231 network_counts.sockets,
20232 1,
20233 "socket",
20234 )?;
20235 let payload = request
20236 .args
20237 .first()
20238 .cloned()
20239 .ok_or_else(|| {
20240 SidecarError::InvalidState(String::from(
20241 "net.listen requires a request payload",
20242 ))
20243 })
20244 .and_then(|value| match value {
20245 Value::String(json) => {
20246 serde_json::from_str::<JavascriptNetListenRequest>(&json).map_err(|error| {
20247 SidecarError::InvalidState(format!(
20248 "invalid net.listen payload: {error}"
20249 ))
20250 })
20251 }
20252 other => serde_json::from_value::<JavascriptNetListenRequest>(other).map_err(
20253 |error| {
20254 SidecarError::InvalidState(format!(
20255 "invalid net.listen payload: {error}"
20256 ))
20257 },
20258 ),
20259 })?;
20260 if let Some(path) = payload.path.as_deref() {
20261 let guest_path = normalize_path(path);
20262 if kernel.exists(&guest_path).map_err(kernel_error)? {
20263 return Err(sidecar_net_error(std::io::Error::from_raw_os_error(
20264 libc::EADDRINUSE,
20265 )));
20266 }
20267
20268 let host_path = resolve_guest_socket_host_path(socket_paths, &guest_path);
20269 let on_host_mount =
20270 host_mount_path_for_guest_path_from_mounts(&socket_paths.mounts, &guest_path)
20271 .is_some();
20272 let listener = ActiveUnixListener::bind(&host_path, &guest_path, payload.backlog)?;
20273 if !on_host_mount {
20274 ensure_kernel_parent_directories(kernel, &guest_path)?;
20275 kernel
20276 .write_file(&guest_path, Vec::new())
20277 .map_err(kernel_error)?;
20278 }
20279 let listener_id = process.allocate_unix_listener_id();
20280 process.unix_listeners.insert(listener_id.clone(), listener);
20281 Ok(json!({
20282 "serverId": listener_id,
20283 "path": guest_path,
20284 }))
20285 } else {
20286 let (family, bind_host, guest_host) =
20287 normalize_tcp_listen_host(payload.host.as_deref())?;
20288 let requested_port = payload.port.unwrap_or(0);
20289 bridge.require_network_access(
20290 vm_id,
20291 NetworkOperation::Listen,
20292 format_tcp_resource(bind_host, requested_port),
20293 )?;
20294 let local_reservation = payload.local_reservation.as_deref().and_then(|id| {
20295 process
20296 .tcp_port_reservations
20297 .remove(id)
20298 .map(|reservation| (id.to_owned(), reservation))
20299 });
20300 let port = if requested_port != 0
20301 && local_reservation
20302 .as_ref()
20303 .map(|(_, reservation)| *reservation)
20304 == Some((family, requested_port))
20305 {
20306 requested_port
20307 } else {
20308 allocate_guest_listen_port(
20309 requested_port,
20310 family,
20311 &socket_paths.used_tcp_guest_ports,
20312 socket_paths.listen_policy,
20313 )?
20314 };
20315 let listener_result = ActiveTcpListener::bind_kernel(
20316 kernel,
20317 process.kernel_pid,
20318 guest_host,
20319 port,
20320 payload.backlog,
20321 );
20322 if let Err(error) = listener_result {
20323 if let Some((reservation_id, reservation)) = local_reservation {
20324 process
20325 .tcp_port_reservations
20326 .insert(reservation_id, reservation);
20327 }
20328 return Err(error);
20329 }
20330 let listener = listener_result?;
20331 let listener_id = process.allocate_tcp_listener_id();
20332 let local_addr = listener.guest_local_addr();
20333 process.tcp_listeners.insert(listener_id.clone(), listener);
20334 Ok(json!({
20335 "serverId": listener_id,
20336 "localAddress": local_addr.ip().to_string(),
20337 "localPort": local_addr.port(),
20338 "family": socket_addr_family(&local_addr),
20339 }))
20340 }
20341 }
20342 "net.poll" => {
20343 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.poll socket id")?;
20344 let wait_ms =
20345 javascript_sync_rpc_arg_u64_optional(&request.args, 1, "net.poll wait ms")?
20346 .unwrap_or_default();
20347 let wait = clamp_javascript_net_poll_wait(wait_ms);
20348 let event = if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20349 socket.poll(kernel, process.kernel_pid, wait)?
20350 } else if let Some(socket) = process.unix_sockets.get_mut(socket_id) {
20351 socket.poll(wait)?
20352 } else {
20353 return Err(SidecarError::InvalidState(format!(
20354 "unknown net socket {socket_id}"
20355 )));
20356 };
20357
20358 match event {
20359 Some(JavascriptTcpSocketEvent::Data(chunk)) => Ok(json!({
20360 "type": "data",
20361 "data": javascript_sync_rpc_bytes_value(&chunk),
20362 })),
20363 Some(JavascriptTcpSocketEvent::End) => Ok(json!({
20364 "type": "end",
20365 })),
20366 Some(JavascriptTcpSocketEvent::Error { code, message }) => Ok(json!({
20367 "type": "error",
20368 "code": code,
20369 "message": message,
20370 })),
20371 Some(JavascriptTcpSocketEvent::Close { had_error }) => {
20372 if let Some(socket) = process.tcp_sockets.remove(socket_id) {
20373 if let Some(listener_id) = socket.listener_id.as_deref() {
20374 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20375 listener.release_connection(socket_id);
20376 }
20377 }
20378 } else if let Some(socket) = process.unix_sockets.remove(socket_id) {
20379 if let Some(listener_id) = socket.listener_id.as_deref() {
20380 if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20381 listener.release_connection(socket_id);
20382 }
20383 }
20384 }
20385 Ok(json!({
20386 "type": "close",
20387 "hadError": had_error,
20388 }))
20389 }
20390 None => Ok(Value::Null),
20391 }
20392 }
20393 "net.socket_wait_connect" => {
20394 let socket_id =
20395 javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_wait_connect socket id")?;
20396 if let Some(socket) = process.tcp_sockets.get(socket_id) {
20397 javascript_net_json_string(socket.socket_info(), "net.socket_wait_connect")
20398 } else {
20399 let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
20400 SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20401 })?;
20402 javascript_net_json_string(socket.socket_info(), "net.socket_wait_connect")
20403 }
20404 }
20405 "net.socket_read" => {
20406 let socket_id =
20407 javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_read socket id")?;
20408 if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20409 javascript_net_read_value(socket.poll(
20410 kernel,
20411 process.kernel_pid,
20412 Duration::ZERO,
20413 )?)
20414 } else {
20415 let socket = process.unix_sockets.get_mut(socket_id).ok_or_else(|| {
20416 SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20417 })?;
20418 javascript_net_read_value(socket.poll(Duration::ZERO)?)
20419 }
20420 }
20421 "net.socket_set_no_delay" => {
20422 let socket_id =
20423 javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_set_no_delay socket id")?;
20424 let enable =
20425 javascript_sync_rpc_arg_bool(&request.args, 1, "net.socket_set_no_delay enabled")?;
20426 if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20427 socket.set_no_delay(enable)?;
20428 } else if !process.unix_sockets.contains_key(socket_id) {
20429 return Err(SidecarError::InvalidState(format!(
20430 "unknown net socket {socket_id}"
20431 )));
20432 }
20433 Ok(Value::Null)
20434 }
20435 "net.socket_set_keep_alive" => {
20436 let socket_id = javascript_sync_rpc_arg_str(
20437 &request.args,
20438 0,
20439 "net.socket_set_keep_alive socket id",
20440 )?;
20441 let enable = javascript_sync_rpc_arg_bool(
20442 &request.args,
20443 1,
20444 "net.socket_set_keep_alive enabled",
20445 )?;
20446 let initial_delay_secs = javascript_sync_rpc_arg_u64_optional(
20447 &request.args,
20448 2,
20449 "net.socket_set_keep_alive initial delay seconds",
20450 )?;
20451 if let Some(socket) = process.tcp_sockets.get_mut(socket_id) {
20452 socket.set_keep_alive(enable, initial_delay_secs)?;
20453 } else if !process.unix_sockets.contains_key(socket_id) {
20454 return Err(SidecarError::InvalidState(format!(
20455 "unknown net socket {socket_id}"
20456 )));
20457 }
20458 Ok(Value::Null)
20459 }
20460 "net.socket_upgrade_tls" => {
20461 let socket_id =
20462 javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_upgrade_tls socket id")?;
20463 let options_json =
20464 javascript_sync_rpc_arg_str(&request.args, 1, "net.socket_upgrade_tls options")?;
20465 let options: JavascriptTlsBridgeOptions =
20466 serde_json::from_str(options_json).map_err(|error| {
20467 SidecarError::InvalidState(format!(
20468 "net.socket_upgrade_tls options must be valid JSON: {error}"
20469 ))
20470 })?;
20471 let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20472 SidecarError::InvalidState(format!(
20473 "unknown TCP socket {socket_id} for TLS upgrade"
20474 ))
20475 })?;
20476 socket.upgrade_tls(vm_id, kernel, options)?;
20477 Ok(Value::Null)
20478 }
20479 "net.socket_get_tls_client_hello" => {
20480 let socket_id = javascript_sync_rpc_arg_str(
20481 &request.args,
20482 0,
20483 "net.socket_get_tls_client_hello socket id",
20484 )?;
20485 let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20486 SidecarError::InvalidState(format!(
20487 "unknown TCP socket {socket_id} for TLS client hello query"
20488 ))
20489 })?;
20490 socket.tls_client_hello_json(vm_id, kernel)
20491 }
20492 "net.socket_tls_query" => {
20493 let socket_id =
20494 javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_tls_query socket id")?;
20495 let query =
20496 javascript_sync_rpc_arg_str(&request.args, 1, "net.socket_tls_query query")?;
20497 let detailed = request
20498 .args
20499 .get(2)
20500 .and_then(Value::as_bool)
20501 .unwrap_or(false);
20502 let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20503 SidecarError::InvalidState(format!("unknown TCP socket {socket_id} for TLS query"))
20504 })?;
20505 socket.tls_query(query, detailed)
20506 }
20507 "net.server_poll" => {
20508 let listener_id =
20509 javascript_sync_rpc_arg_str(&request.args, 0, "net.server_poll listener id")?;
20510 let wait_ms =
20511 javascript_sync_rpc_arg_u64_optional(&request.args, 1, "net.server_poll wait ms")?
20512 .unwrap_or_default();
20513 let tcp_event = if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20514 Some(listener.poll(kernel, process.kernel_pid, Duration::from_millis(wait_ms))?)
20515 } else {
20516 None
20517 };
20518
20519 if let Some(event) = tcp_event {
20520 return match event {
20521 Some(JavascriptTcpListenerEvent::Connection(pending)) => {
20522 let PendingTcpSocket {
20523 stream,
20524 kernel_socket_id,
20525 preallocated,
20526 guest_local_addr,
20527 guest_remote_addr,
20528 } = pending;
20529 if !preallocated {
20530 if let Err(error) = check_network_resource_limit(
20531 resource_limits.max_sockets,
20532 network_counts.sockets,
20533 1,
20534 "socket",
20535 )
20536 .and_then(|()| {
20537 check_network_resource_limit(
20538 resource_limits.max_connections,
20539 network_counts.connections,
20540 1,
20541 "connection",
20542 )
20543 }) {
20544 if let Some(stream) = stream {
20545 let _ = stream.shutdown(Shutdown::Both);
20546 }
20547 return Ok(json!({
20548 "type": "error",
20549 "code": "EAGAIN",
20550 "message": error.to_string(),
20551 }));
20552 }
20553 }
20554 let socket = if let Some(stream) = stream {
20555 ActiveTcpSocket::from_stream(
20556 stream,
20557 Some(listener_id.to_string()),
20558 guest_local_addr,
20559 guest_remote_addr,
20560 )?
20561 } else {
20562 ActiveTcpSocket::from_kernel(
20563 kernel_socket_id.ok_or_else(|| {
20564 SidecarError::InvalidState(String::from(
20565 "kernel TCP accept missing socket id",
20566 ))
20567 })?,
20568 Some(listener_id.to_string()),
20569 guest_local_addr,
20570 guest_remote_addr,
20571 )
20572 };
20573 let socket_id = process.allocate_tcp_socket_id();
20574 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20575 listener.register_connection(&socket_id);
20576 }
20577 process.tcp_sockets.insert(socket_id.clone(), socket);
20578 Ok(json!({
20579 "type": "connection",
20580 "socketId": socket_id,
20581 "localAddress": guest_local_addr.ip().to_string(),
20582 "localPort": guest_local_addr.port(),
20583 "remoteAddress": guest_remote_addr.ip().to_string(),
20584 "remotePort": guest_remote_addr.port(),
20585 "remoteFamily": socket_addr_family(&guest_remote_addr),
20586 }))
20587 }
20588 Some(JavascriptTcpListenerEvent::Error { code, message }) => Ok(json!({
20589 "type": "error",
20590 "code": code,
20591 "message": message,
20592 })),
20593 None => Ok(Value::Null),
20594 };
20595 }
20596
20597 let event = {
20598 let listener = process.unix_listeners.get_mut(listener_id).ok_or_else(|| {
20599 SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20600 })?;
20601 listener.poll(Duration::from_millis(wait_ms))?
20602 };
20603
20604 match event {
20605 Some(JavascriptUnixListenerEvent::Connection(pending)) => {
20606 if let Err(error) = check_network_resource_limit(
20607 resource_limits.max_sockets,
20608 network_counts.sockets,
20609 1,
20610 "socket",
20611 )
20612 .and_then(|()| {
20613 check_network_resource_limit(
20614 resource_limits.max_connections,
20615 network_counts.connections,
20616 1,
20617 "connection",
20618 )
20619 }) {
20620 let _ = pending.stream.shutdown(Shutdown::Both);
20621 return Ok(json!({
20622 "type": "error",
20623 "code": "EAGAIN",
20624 "message": error.to_string(),
20625 }));
20626 }
20627 let socket = ActiveUnixSocket::from_stream(
20628 pending.stream,
20629 Some(listener_id.to_string()),
20630 pending.local_path.clone(),
20631 pending.remote_path.clone(),
20632 )?;
20633 let socket_id = process.allocate_unix_socket_id();
20634 if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20635 listener.register_connection(&socket_id);
20636 }
20637 process.unix_sockets.insert(socket_id.clone(), socket);
20638 Ok(json!({
20639 "type": "connection",
20640 "socketId": socket_id,
20641 "localPath": pending.local_path,
20642 "remotePath": pending.remote_path,
20643 }))
20644 }
20645 Some(JavascriptUnixListenerEvent::Error { code, message }) => Ok(json!({
20646 "type": "error",
20647 "code": code,
20648 "message": message,
20649 })),
20650 None => Ok(Value::Null),
20651 }
20652 }
20653 "net.server_accept" => {
20654 let listener_id =
20655 javascript_sync_rpc_arg_str(&request.args, 0, "net.server_accept listener id")?;
20656 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20657 return match listener.poll(kernel, process.kernel_pid, Duration::ZERO)? {
20658 Some(JavascriptTcpListenerEvent::Connection(pending)) => {
20659 let PendingTcpSocket {
20660 stream,
20661 kernel_socket_id,
20662 preallocated,
20663 guest_local_addr,
20664 guest_remote_addr,
20665 } = pending;
20666 if !preallocated {
20667 check_network_resource_limit(
20668 resource_limits.max_sockets,
20669 network_counts.sockets,
20670 1,
20671 "socket",
20672 )?;
20673 check_network_resource_limit(
20674 resource_limits.max_connections,
20675 network_counts.connections,
20676 1,
20677 "connection",
20678 )?;
20679 }
20680 let info = json!({
20681 "localAddress": guest_local_addr.ip().to_string(),
20682 "localPort": guest_local_addr.port(),
20683 "localFamily": socket_addr_family(&guest_local_addr),
20684 "remoteAddress": guest_remote_addr.ip().to_string(),
20685 "remotePort": guest_remote_addr.port(),
20686 "remoteFamily": socket_addr_family(&guest_remote_addr),
20687 });
20688 let socket = if let Some(stream) = stream {
20689 ActiveTcpSocket::from_stream(
20690 stream,
20691 Some(listener_id.to_string()),
20692 guest_local_addr,
20693 guest_remote_addr,
20694 )?
20695 } else {
20696 ActiveTcpSocket::from_kernel(
20697 kernel_socket_id.ok_or_else(|| {
20698 SidecarError::InvalidState(String::from(
20699 "kernel TCP accept missing socket id",
20700 ))
20701 })?,
20702 Some(listener_id.to_string()),
20703 guest_local_addr,
20704 guest_remote_addr,
20705 )
20706 };
20707 let socket_id = process.allocate_tcp_socket_id();
20708 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20709 listener.register_connection(&socket_id);
20710 }
20711 process.tcp_sockets.insert(socket_id.clone(), socket);
20712 javascript_net_json_string(
20713 json!({
20714 "socketId": socket_id,
20715 "info": info,
20716 }),
20717 "net.server_accept",
20718 )
20719 }
20720 Some(JavascriptTcpListenerEvent::Error { code, message }) => {
20721 let detail = code.unwrap_or_else(|| String::from("server accept"));
20722 Err(SidecarError::Execution(format!("{detail}: {message}")))
20723 }
20724 None => Ok(javascript_net_timeout_value()),
20725 };
20726 }
20727
20728 let listener = process.unix_listeners.get_mut(listener_id).ok_or_else(|| {
20729 SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20730 })?;
20731 match listener.poll(Duration::ZERO)? {
20732 Some(JavascriptUnixListenerEvent::Connection(pending)) => {
20733 check_network_resource_limit(
20734 resource_limits.max_sockets,
20735 network_counts.sockets,
20736 1,
20737 "socket",
20738 )?;
20739 check_network_resource_limit(
20740 resource_limits.max_connections,
20741 network_counts.connections,
20742 1,
20743 "connection",
20744 )?;
20745 let info = json!({
20746 "localPath": pending.local_path.clone(),
20747 "remotePath": pending.remote_path.clone(),
20748 });
20749 let socket = ActiveUnixSocket::from_stream(
20750 pending.stream,
20751 Some(listener_id.to_string()),
20752 pending.local_path,
20753 pending.remote_path,
20754 )?;
20755 let socket_id = process.allocate_unix_socket_id();
20756 if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20757 listener.register_connection(&socket_id);
20758 }
20759 process.unix_sockets.insert(socket_id.clone(), socket);
20760 javascript_net_json_string(
20761 json!({
20762 "socketId": socket_id,
20763 "info": info,
20764 }),
20765 "net.server_accept",
20766 )
20767 }
20768 Some(JavascriptUnixListenerEvent::Error { code, message }) => {
20769 let detail = code.unwrap_or_else(|| String::from("server accept"));
20770 Err(SidecarError::Execution(format!("{detail}: {message}")))
20771 }
20772 None => Ok(javascript_net_timeout_value()),
20773 }
20774 }
20775 "net.server_connections" => {
20776 let listener_id = javascript_sync_rpc_arg_str(
20777 &request.args,
20778 0,
20779 "net.server_connections listener id",
20780 )?;
20781 if let Some(listener) = process.tcp_listeners.get(listener_id) {
20782 Ok(json!(listener.active_connection_count()))
20783 } else {
20784 let listener = process.unix_listeners.get(listener_id).ok_or_else(|| {
20785 SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20786 })?;
20787 Ok(json!(listener.active_connection_count()))
20788 }
20789 }
20790 "net.upgrade_socket_write" => {
20791 let socket_id = javascript_sync_rpc_arg_str(
20792 &request.args,
20793 0,
20794 "net.upgrade_socket_write socket id",
20795 )?;
20796 let chunk =
20797 javascript_sync_rpc_base64_arg(&request.args, 1, "net.upgrade_socket_write chunk")?;
20798 let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20799 SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
20800 })?;
20801 socket
20802 .write_all(kernel, process.kernel_pid, &chunk)
20803 .map(|written| json!(written))
20804 }
20805 "net.upgrade_socket_end" => {
20806 let socket_id =
20807 javascript_sync_rpc_arg_str(&request.args, 0, "net.upgrade_socket_end socket id")?;
20808 let socket = process.tcp_sockets.get(socket_id).ok_or_else(|| {
20809 SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
20810 })?;
20811 socket.shutdown_write(kernel, process.kernel_pid)?;
20812 Ok(Value::Null)
20813 }
20814 "net.upgrade_socket_destroy" => {
20815 let socket_id = javascript_sync_rpc_arg_str(
20816 &request.args,
20817 0,
20818 "net.upgrade_socket_destroy socket id",
20819 )?;
20820 let socket = process.tcp_sockets.remove(socket_id).ok_or_else(|| {
20821 SidecarError::InvalidState(format!("unknown TCP socket {socket_id}"))
20822 })?;
20823 if let Some(listener_id) = socket.listener_id.as_deref() {
20824 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20825 listener.release_connection(socket_id);
20826 }
20827 }
20828 let _ = socket.close(kernel, process.kernel_pid);
20829 Ok(Value::Null)
20830 }
20831 "net.write" => {
20832 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.write socket id")?;
20833 let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "net.write chunk")?;
20834 if let Some(socket) = process.tcp_sockets.get(socket_id) {
20835 socket
20836 .write_all(kernel, process.kernel_pid, &chunk)
20837 .map(|written| json!(written))
20838 } else {
20839 let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
20840 SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20841 })?;
20842 socket.write_all(&chunk).map(|written| json!(written))
20843 }
20844 }
20845 "net.shutdown" => {
20846 let socket_id =
20847 javascript_sync_rpc_arg_str(&request.args, 0, "net.shutdown socket id")?;
20848 if let Some(socket) = process.tcp_sockets.get(socket_id) {
20849 socket.shutdown_write(kernel, process.kernel_pid)?;
20850 } else {
20851 let socket = process.unix_sockets.get(socket_id).ok_or_else(|| {
20852 SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20853 })?;
20854 socket.shutdown_write()?;
20855 }
20856 Ok(Value::Null)
20857 }
20858 "net.destroy" => {
20859 let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.destroy socket id")?;
20860 if let Some(socket) = process.tcp_sockets.remove(socket_id) {
20861 if let Some(listener_id) = socket.listener_id.as_deref() {
20862 if let Some(listener) = process.tcp_listeners.get_mut(listener_id) {
20863 listener.release_connection(socket_id);
20864 }
20865 }
20866 let _ = socket.close(kernel, process.kernel_pid);
20867 Ok(Value::Null)
20868 } else {
20869 let socket = process.unix_sockets.remove(socket_id).ok_or_else(|| {
20870 SidecarError::InvalidState(format!("unknown net socket {socket_id}"))
20871 })?;
20872 if let Some(listener_id) = socket.listener_id.as_deref() {
20873 if let Some(listener) = process.unix_listeners.get_mut(listener_id) {
20874 listener.release_connection(socket_id);
20875 }
20876 }
20877 let _ = socket.close();
20878 Ok(Value::Null)
20879 }
20880 }
20881 "net.server_close" => {
20882 let listener_id =
20883 javascript_sync_rpc_arg_str(&request.args, 0, "net.server_close listener id")?;
20884 if let Some(listener) = process.tcp_listeners.remove(listener_id) {
20885 listener.close(kernel, process.kernel_pid)?;
20886 Ok(Value::Null)
20887 } else {
20888 let listener = process.unix_listeners.remove(listener_id).ok_or_else(|| {
20889 SidecarError::InvalidState(format!("unknown net listener {listener_id}"))
20890 })?;
20891 listener.close()?;
20892 Ok(Value::Null)
20893 }
20894 }
20895 "tls.get_ciphers" => javascript_net_json_string(
20896 Value::Array(
20897 tls_provider()
20898 .cipher_suites
20899 .iter()
20900 .filter_map(|suite| {
20901 suite
20902 .suite()
20903 .as_str()
20904 .map(|value| Value::String(value.to_owned()))
20905 })
20906 .collect(),
20907 ),
20908 "tls.get_ciphers",
20909 ),
20910 _ => Err(SidecarError::InvalidState(format!(
20911 "unsupported JavaScript net sync RPC method {}",
20912 request.method
20913 ))),
20914 }
20915}
20916
20917fn signal_name_for_stream_event(signal: i32) -> Option<&'static str> {
20918 match signal {
20919 libc::SIGHUP => Some("SIGHUP"),
20920 libc::SIGINT => Some("SIGINT"),
20921 libc::SIGUSR1 => Some("SIGUSR1"),
20922 libc::SIGALRM => Some("SIGALRM"),
20923 libc::SIGCONT => Some("SIGCONT"),
20924 libc::SIGTERM => Some("SIGTERM"),
20925 libc::SIGCHLD => Some("SIGCHLD"),
20926 libc::SIGWINCH => Some("SIGWINCH"),
20927 _ => None,
20928 }
20929}
20930
20931pub(crate) fn canonical_signal_name(signal: i32) -> Option<&'static str> {
20932 match signal {
20933 1 => Some("SIGHUP"),
20934 2 => Some("SIGINT"),
20935 3 => Some("SIGQUIT"),
20936 4 => Some("SIGILL"),
20937 5 => Some("SIGTRAP"),
20938 6 => Some("SIGABRT"),
20939 7 => Some("SIGBUS"),
20940 8 => Some("SIGFPE"),
20941 9 => Some("SIGKILL"),
20942 10 => Some("SIGUSR1"),
20943 11 => Some("SIGSEGV"),
20944 12 => Some("SIGUSR2"),
20945 13 => Some("SIGPIPE"),
20946 14 => Some("SIGALRM"),
20947 15 => Some("SIGTERM"),
20948 17 => Some("SIGCHLD"),
20949 18 => Some("SIGCONT"),
20950 19 => Some("SIGSTOP"),
20951 20 => Some("SIGTSTP"),
20952 21 => Some("SIGTTIN"),
20953 22 => Some("SIGTTOU"),
20954 23 => Some("SIGURG"),
20955 24 => Some("SIGXCPU"),
20956 25 => Some("SIGXFSZ"),
20957 26 => Some("SIGVTALRM"),
20958 27 => Some("SIGPROF"),
20959 28 => Some("SIGWINCH"),
20960 29 => Some("SIGIO"),
20961 30 => Some("SIGPWR"),
20962 31 => Some("SIGSYS"),
20963 _ => None,
20964 }
20965}
20966
20967fn dispatch_v8_process_signal(process: &ActiveProcess, signal: i32) -> Result<bool, SidecarError> {
20968 let Some(signal_name) = signal_name_for_stream_event(signal) else {
20969 return Ok(false);
20970 };
20971 process.execution.send_javascript_stream_event(
20972 "signal",
20973 json!({
20974 "signal": signal_name,
20975 "number": signal,
20976 "action": "default",
20977 }),
20978 )?;
20979 Ok(true)
20980}
20981
20982fn dispatch_v8_session_signal_async(session: V8SessionHandle, signal: i32) {
20983 let Some(signal_name) = signal_name_for_stream_event(signal).map(str::to_owned) else {
20984 return;
20985 };
20986 thread::spawn(move || {
20987 thread::sleep(Duration::from_millis(1));
20988 let payload = v8_runtime::json_to_cbor_payload(&json!({
20989 "signal": signal_name,
20990 "number": signal,
20991 "action": "default",
20992 }))
20993 .unwrap_or_default();
20994 let _ = session.send_stream_event("signal", payload);
20995 });
20996}
20997
20998pub(crate) fn parse_signal(signal: &str) -> Result<i32, SidecarError> {
20999 let trimmed = signal.trim();
21000 if trimmed.is_empty() {
21001 return Err(SidecarError::InvalidState(String::from(
21002 "kill_process requires a non-empty signal",
21003 )));
21004 }
21005
21006 if let Ok(value) = trimmed.parse::<i32>() {
21007 return match value {
21008 0..=31 => Ok(value),
21009 _ => Err(SidecarError::InvalidState(format!(
21010 "unsupported kill_process signal {signal}"
21011 ))),
21012 };
21013 }
21014
21015 let upper = trimmed.to_ascii_uppercase();
21016 let normalized = upper.strip_prefix("SIG").unwrap_or(&upper);
21017
21018 signal_number_from_name(normalized).ok_or_else(|| {
21019 SidecarError::InvalidState(format!("unsupported kill_process signal {signal}"))
21020 })
21021}
21022
21023fn signal_number_from_name(signal: &str) -> Option<i32> {
21024 match signal {
21025 "0" => Some(0),
21026 "HUP" => Some(1),
21027 "INT" => Some(2),
21028 "QUIT" => Some(3),
21029 "ILL" => Some(4),
21030 "TRAP" => Some(5),
21031 "ABRT" | "IOT" => Some(6),
21032 "BUS" => Some(7),
21033 "FPE" => Some(8),
21034 "KILL" => Some(9),
21035 "USR1" => Some(10),
21036 "SEGV" => Some(11),
21037 "USR2" => Some(12),
21038 "PIPE" => Some(13),
21039 "ALRM" => Some(14),
21040 "TERM" => Some(15),
21041 "STKFLT" => Some(16),
21042 "CHLD" => Some(17),
21043 "CONT" => Some(18),
21044 "STOP" => Some(19),
21045 "TSTP" => Some(20),
21046 "TTIN" => Some(21),
21047 "TTOU" => Some(22),
21048 "URG" => Some(23),
21049 "XCPU" => Some(24),
21050 "XFSZ" => Some(25),
21051 "VTALRM" => Some(26),
21052 "PROF" => Some(27),
21053 "WINCH" => Some(28),
21054 "IO" | "POLL" => Some(29),
21055 "PWR" => Some(30),
21056 "SYS" => Some(31),
21057 _ => None,
21058 }
21059}
21060
21061pub(crate) fn runtime_child_is_alive(child_pid: u32) -> Result<bool, SidecarError> {
21062 Ok(runtime_child_exit_status(child_pid)?.is_none())
21063}
21064
21065#[cfg(not(target_os = "macos"))]
21066fn runtime_child_exit_status(child_pid: u32) -> Result<Option<i32>, SidecarError> {
21067 if child_pid == 0 {
21068 return Ok(Some(0));
21069 }
21070
21071 let wait_flags = WaitPidFlag::WNOHANG
21072 | WaitPidFlag::WNOWAIT
21073 | WaitPidFlag::WEXITED
21074 | WaitPidFlag::WUNTRACED
21075 | WaitPidFlag::WCONTINUED;
21076 match wait_on_child(WaitId::Pid(Pid::from_raw(child_pid as i32)), wait_flags) {
21077 Ok(WaitStatus::StillAlive)
21078 | Ok(WaitStatus::Stopped(_, _))
21079 | Ok(WaitStatus::Continued(_)) => Ok(None),
21080 Ok(WaitStatus::Exited(_, status)) => Ok(Some(status)),
21081 Ok(WaitStatus::Signaled(_, signal, _)) => Ok(Some(128 + signal as i32)),
21082 #[cfg(any(target_os = "linux", target_os = "android"))]
21083 Ok(WaitStatus::PtraceEvent(_, _, _) | WaitStatus::PtraceSyscall(_)) => Ok(None),
21084 Err(nix::errno::Errno::ECHILD) => Ok(Some(0)),
21085 Err(error) => Err(SidecarError::Execution(format!(
21086 "failed to inspect guest runtime process {child_pid}: {error}"
21087 ))),
21088 }
21089}
21090
21091#[cfg(target_os = "macos")]
21097fn runtime_child_exit_status(child_pid: u32) -> Result<Option<i32>, SidecarError> {
21098 if child_pid == 0 {
21099 return Ok(Some(0));
21100 }
21101
21102 match waitpid(Pid::from_raw(child_pid as i32), Some(WaitPidFlag::WNOHANG)) {
21103 Ok(WaitStatus::StillAlive)
21104 | Ok(WaitStatus::Stopped(_, _))
21105 | Ok(WaitStatus::Continued(_)) => Ok(None),
21106 Ok(WaitStatus::Exited(_, status)) => Ok(Some(status)),
21107 Ok(WaitStatus::Signaled(_, signal, _)) => Ok(Some(128 + signal as i32)),
21108 Err(nix::errno::Errno::ECHILD) => Ok(Some(0)),
21109 Err(error) => Err(SidecarError::Execution(format!(
21110 "failed to inspect guest runtime process {child_pid}: {error}"
21111 ))),
21112 }
21113}
21114
21115pub(crate) fn signal_runtime_process(child_pid: u32, signal: i32) -> Result<(), SidecarError> {
21116 if child_pid == 0 {
21117 return Ok(());
21118 }
21119
21120 if !runtime_child_is_alive(child_pid)? {
21121 return Ok(());
21122 }
21123
21124 if signal == 0 {
21125 return Ok(());
21126 }
21127
21128 let parsed = Signal::try_from(signal).map_err(|_| {
21129 SidecarError::InvalidState(format!("unsupported kill_process signal {signal}"))
21130 })?;
21131 let result = send_signal(Pid::from_raw(child_pid as i32), Some(parsed));
21132
21133 match result {
21134 Ok(()) => Ok(()),
21135 Err(nix::errno::Errno::ESRCH) => Ok(()),
21136 Err(error) => Err(SidecarError::Execution(format!(
21137 "failed to signal guest runtime process {child_pid}: {error}"
21138 ))),
21139 }
21140}
21141
21142pub(crate) fn error_code(error: &SidecarError) -> &'static str {
21143 match error {
21144 SidecarError::InvalidState(_) => "invalid_state",
21145 SidecarError::ProtocolVersionMismatch(_) => "protocol_version_mismatch",
21146 SidecarError::BridgeVersionMismatch(_) => "bridge_version_mismatch",
21147 SidecarError::Conflict(_) => "conflict",
21148 SidecarError::Unauthorized(_) => "unauthorized",
21149 SidecarError::Unsupported(_) => "unsupported",
21150 SidecarError::FrameTooLarge(_) => "frame_too_large",
21151 SidecarError::Kernel(_) => "kernel_error",
21152 SidecarError::Plugin(_) => "plugin_error",
21153 SidecarError::Execution(_) => "execution_error",
21154 SidecarError::Bridge(_) => "bridge_error",
21155 SidecarError::Io(_) => "io_error",
21156 }
21157}
21158
21159fn guest_errno_code(message: &str) -> Option<&str> {
21160 const TRUSTED_PREFIXES: &[&str] = &[
21161 "ERR_AGENTOS_NODE_SYNC_RPC",
21162 "ERR_AGENTOS_PYTHON_VFS_RPC",
21163 "ERR_AGENTOS_BRIDGE",
21164 ];
21165
21166 let mut segments = message.split(':').map(str::trim);
21167 let first = segments.next()?;
21168 if is_guest_errno_segment(first) {
21169 return Some(first);
21170 }
21171
21172 if TRUSTED_PREFIXES.contains(&first) {
21173 let second = segments.next()?;
21174 if is_guest_errno_segment(second) {
21175 return Some(second);
21176 }
21177 }
21178
21179 None
21180}
21181
21182fn is_guest_errno_segment(segment: &str) -> bool {
21183 segment.len() >= 2
21184 && segment.starts_with('E')
21185 && !segment.starts_with("ERR_")
21186 && segment[1..]
21187 .bytes()
21188 .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_')
21189}
21190
21191pub(crate) fn javascript_sync_rpc_error_code(error: &SidecarError) -> String {
21192 let message = error.to_string();
21193 if let Some(code) = guest_errno_code(&message) {
21194 return code.to_owned();
21195 }
21196 if message.starts_with("ERR_NATIVE_BINARY_NOT_SUPPORTED:") {
21197 return String::from("ERR_NATIVE_BINARY_NOT_SUPPORTED");
21198 }
21199
21200 let lower = message.to_ascii_lowercase();
21201 if lower.contains("no such file or directory")
21202 || lower.contains("entry not found")
21203 || lower.contains("not found")
21204 {
21205 return String::from("ENOENT");
21206 }
21207 if lower.contains("permission denied") {
21208 return String::from("EACCES");
21209 }
21210 if lower.contains("already exists")
21211 || lower.contains("already registered")
21212 || lower.contains("file exists")
21213 {
21214 return String::from("EEXIST");
21215 }
21216 if lower.contains("invalid argument") {
21217 return String::from("EINVAL");
21218 }
21219
21220 String::from("ERR_AGENTOS_NODE_SYNC_RPC")
21221}
21222
21223pub(crate) fn ignore_stale_javascript_sync_rpc_response(
21224 error: SidecarError,
21225) -> Result<(), SidecarError> {
21226 match error {
21227 SidecarError::Execution(message)
21228 if message.ends_with("is no longer pending")
21229 && message.starts_with("sync RPC request ") =>
21230 {
21231 Ok(())
21232 }
21233 SidecarError::Execution(message) => {
21234 let lower = message.to_ascii_lowercase();
21235 if lower.contains("sync rpc response")
21236 && (lower.contains("broken pipe") || lower.contains("channel closed unexpectedly"))
21237 {
21238 Ok(())
21239 } else {
21240 Err(SidecarError::Execution(message))
21241 }
21242 }
21243 other => Err(other),
21244 }
21245}
21246
21247#[cfg(test)]
21248mod error_code_tests {
21249 use super::{guest_errno_code, javascript_sync_rpc_error_code, SidecarError};
21250
21251 #[test]
21252 fn guest_errno_code_rejects_guest_controlled_errno_segments() {
21253 assert_eq!(guest_errno_code("user said 'EACCES: denied'"), None);
21254 assert_eq!(
21255 guest_errno_code("prefix: user said 'EPERM': more text"),
21256 None
21257 );
21258 assert_eq!(guest_errno_code("ERR_AGENTOS_FAKE: EACCES: denied"), None);
21259 }
21260
21261 #[test]
21262 fn guest_errno_code_accepts_trusted_secure_exec_prefixes() {
21263 assert_eq!(
21264 guest_errno_code("ERR_AGENTOS_NODE_SYNC_RPC: EACCES: permission denied on /foo"),
21265 Some("EACCES")
21266 );
21267 assert_eq!(
21268 guest_errno_code("ERR_AGENTOS_PYTHON_VFS_RPC: ENOENT: missing file"),
21269 Some("ENOENT")
21270 );
21271 assert_eq!(guest_errno_code("EEXIST: already exists"), Some("EEXIST"));
21272 }
21273
21274 #[test]
21275 fn javascript_sync_rpc_error_code_ignores_spoofed_errnos() {
21276 let error = SidecarError::Execution(String::from("user said 'EACCES: denied'"));
21277 assert_eq!(
21278 javascript_sync_rpc_error_code(&error),
21279 "ERR_AGENTOS_NODE_SYNC_RPC"
21280 );
21281 }
21282
21283 #[test]
21284 fn javascript_sync_rpc_error_code_preserves_real_sidecar_errnos() {
21285 let error = SidecarError::Execution(String::from(
21286 "ERR_AGENTOS_NODE_SYNC_RPC: EACCES: permission denied on /foo",
21287 ));
21288 assert_eq!(javascript_sync_rpc_error_code(&error), "EACCES");
21289 }
21290
21291 #[test]
21292 fn javascript_sync_rpc_error_code_maps_file_exists_messages() {
21293 let error = SidecarError::Io(String::from(
21294 "failed to create mapped guest directory /.next/server: File exists (os error 17)",
21295 ));
21296 assert_eq!(javascript_sync_rpc_error_code(&error), "EEXIST");
21297 }
21298
21299 #[test]
21300 fn javascript_sync_rpc_error_code_preserves_native_binary_rejections() {
21301 let error = SidecarError::Execution(String::from(
21302 "ERR_NATIVE_BINARY_NOT_SUPPORTED: refused to execute native ELF guest binary at /tmp/fake-rg inside the VM",
21303 ));
21304 assert_eq!(
21305 javascript_sync_rpc_error_code(&error),
21306 "ERR_NATIVE_BINARY_NOT_SUPPORTED"
21307 );
21308 }
21309}
21310#[cfg(test)]
21311mod ssrf_egress_classifier_tests {
21312 use super::{
21322 filter_dns_safe_ip_addrs, is_loopback_ip, restricted_non_loopback_ip_range, SidecarError,
21323 };
21324 use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
21325
21326 fn assert_restricted(ip: IpAddr, expected_label: &str) {
21327 let classification = restricted_non_loopback_ip_range(ip);
21328 assert!(
21329 classification.is_some(),
21330 "{ip} must be classified as a restricted egress target"
21331 );
21332 let (_cidr, label) = classification.unwrap();
21333 assert_eq!(
21334 label, expected_label,
21335 "{ip} should be labelled {expected_label}, got {label}"
21336 );
21337 }
21338
21339 fn assert_dns_denied(ip: IpAddr, label: &str) {
21340 match filter_dns_safe_ip_addrs(vec![ip], "attacker.example") {
21341 Err(SidecarError::Execution(message)) => assert!(
21342 message.starts_with("EACCES:"),
21343 "{label}: egress filter must deny with EACCES, got: {message}"
21344 ),
21345 other => panic!("{label}: expected EACCES denial, got {other:?}"),
21346 }
21347 }
21348
21349 #[test]
21351 fn classifier_denies_unspecified_and_cgnat_targets() {
21352 assert_restricted(IpAddr::V4(Ipv4Addr::UNSPECIFIED), "unspecified");
21354 assert_restricted(IpAddr::V6(Ipv6Addr::UNSPECIFIED), "unspecified");
21356
21357 assert_restricted(
21359 IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1)),
21360 "carrier-grade-nat",
21361 );
21362 assert_restricted(
21363 IpAddr::V4(Ipv4Addr::new(100, 127, 255, 254)),
21364 "carrier-grade-nat",
21365 );
21366
21367 assert!(
21369 restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(100, 63, 255, 255)))
21370 .is_none(),
21371 "100.63.255.255 is outside CGNAT and must remain allowed"
21372 );
21373 assert!(
21374 restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(100, 128, 0, 0))).is_none(),
21375 "100.128.0.0 is outside CGNAT and must remain allowed"
21376 );
21377
21378 assert_dns_denied(IpAddr::V4(Ipv4Addr::UNSPECIFIED), "0.0.0.0 (unspecified)");
21380 assert_dns_denied(IpAddr::V6(Ipv6Addr::UNSPECIFIED), ":: (unspecified)");
21381 assert_dns_denied(
21382 IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1)),
21383 "100.64.0.1 (CGNAT)",
21384 );
21385 }
21386
21387 #[test]
21389 fn classifier_denies_ipv6_spelled_metadata_addresses() {
21390 let mapped = "::ffff:169.254.169.254".parse::<Ipv6Addr>().unwrap();
21393 assert_restricted(IpAddr::V6(mapped), "link-local");
21394
21395 let compat = "::169.254.169.254".parse::<Ipv6Addr>().unwrap();
21396 assert_restricted(IpAddr::V6(compat), "link-local");
21397
21398 assert_restricted(
21400 IpAddr::V6("::10.0.0.1".parse::<Ipv6Addr>().unwrap()),
21401 "private",
21402 );
21403 assert_restricted(
21404 IpAddr::V6("::100.64.0.1".parse::<Ipv6Addr>().unwrap()),
21405 "carrier-grade-nat",
21406 );
21407
21408 assert_eq!(
21412 restricted_non_loopback_ip_range(IpAddr::V6(Ipv6Addr::UNSPECIFIED)),
21413 Some(("::/128", "unspecified")),
21414 ":: must classify as unspecified, not via the IPv4-compat path"
21415 );
21416 assert!(
21417 restricted_non_loopback_ip_range(IpAddr::V6(Ipv6Addr::LOCALHOST)).is_none()
21418 || is_loopback_ip(IpAddr::V6(Ipv6Addr::LOCALHOST)),
21419 "::1 must not be classified as a restricted IPv4-compatible target"
21420 );
21421 assert!(
21422 restricted_non_loopback_ip_range(IpAddr::V6("::8.8.8.8".parse::<Ipv6Addr>().unwrap()))
21423 .is_none(),
21424 "::8.8.8.8 (public IPv4-compatible) must remain allowed"
21425 );
21426
21427 assert_dns_denied(
21429 IpAddr::V6("::169.254.169.254".parse::<Ipv6Addr>().unwrap()),
21430 "::169.254.169.254 (IPv4-compat metadata)",
21431 );
21432 }
21433
21434 #[test]
21436 fn classifier_denies_reserved_and_multicast_targets() {
21437 assert_restricted(IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)), "multicast");
21441 assert_restricted(IpAddr::V4(Ipv4Addr::new(239, 255, 255, 255)), "multicast");
21442 assert_restricted(IpAddr::V4(Ipv4Addr::new(240, 0, 0, 1)), "reserved");
21443 assert_restricted(IpAddr::V4(Ipv4Addr::BROADCAST), "reserved");
21445
21446 assert_restricted(
21448 IpAddr::V6("::224.0.0.1".parse::<Ipv6Addr>().unwrap()),
21449 "multicast",
21450 );
21451 assert_restricted(
21452 IpAddr::V6("::240.0.0.1".parse::<Ipv6Addr>().unwrap()),
21453 "reserved",
21454 );
21455
21456 assert!(
21458 restricted_non_loopback_ip_range(IpAddr::V4(Ipv4Addr::new(223, 255, 255, 255)))
21459 .is_none(),
21460 "223.255.255.255 is outside 224/4 and must remain allowed"
21461 );
21462
21463 assert_dns_denied(
21465 IpAddr::V4(Ipv4Addr::new(240, 0, 0, 1)),
21466 "240.0.0.1 (reserved)",
21467 );
21468 assert_dns_denied(
21469 IpAddr::V4(Ipv4Addr::new(224, 0, 0, 1)),
21470 "224.0.0.1 (multicast)",
21471 );
21472 }
21473}
21474
21475#[cfg(test)]
21484mod dns_rebinding_pin_tests {
21485 use super::{issue_outbound_http_request, split_netloc, JavascriptHttpRequestOptions};
21486 use std::collections::BTreeMap;
21487 use std::io::{Read, Write};
21488 use std::net::{IpAddr, Ipv4Addr, TcpListener};
21489 use std::thread;
21490 use url::Url;
21491
21492 fn empty_headers() -> super::HttpHeaderCollection {
21493 super::parse_http_header_collection(&BTreeMap::new(), "test headers")
21494 .expect("empty header collection")
21495 }
21496
21497 fn options() -> JavascriptHttpRequestOptions {
21498 JavascriptHttpRequestOptions {
21499 method: Some(String::from("GET")),
21500 headers: BTreeMap::new(),
21501 body: None,
21502 reject_unauthorized: None,
21503 }
21504 }
21505
21506 #[test]
21507 fn split_netloc_handles_hostnames_and_bracketed_ipv6() {
21508 assert_eq!(
21509 split_netloc("attacker.example:80"),
21510 Some(("attacker.example", 80))
21511 );
21512 assert_eq!(split_netloc("[::1]:443"), Some(("::1", 443)));
21513 assert_eq!(split_netloc("10.0.0.1:8080"), Some(("10.0.0.1", 8080)));
21514 assert_eq!(split_netloc("no-port"), None);
21515 assert_eq!(split_netloc("host:notaport"), None);
21516 }
21517
21518 #[test]
21524 fn outbound_http_connect_is_pinned_to_vetted_ip() {
21525 let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).expect("bind loopback server");
21526 let port = listener.local_addr().expect("local addr").port();
21527 let server = thread::spawn(move || {
21528 let (mut stream, _) = listener.accept().expect("accept");
21529 let mut buf = [0u8; 1024];
21530 let _ = stream.read(&mut buf);
21531 stream
21532 .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhi")
21533 .expect("write response");
21534 let _ = stream.flush();
21535 });
21536
21537 let url = Url::parse(&format!("http://attacker.example:{port}/")).expect("url");
21538 let pinned = vec![IpAddr::V4(Ipv4Addr::LOCALHOST)];
21539 let result = issue_outbound_http_request(&url, &options(), &empty_headers(), &pinned)
21540 .expect("pinned request should reach the vetted loopback target");
21541 let payload = result.as_str().expect("string payload");
21542 assert!(
21543 payload.contains("\"status\":200"),
21544 "expected 200 from pinned target, got: {payload}"
21545 );
21546 server.join().expect("server thread");
21547 }
21548
21549 #[test]
21553 fn outbound_http_refuses_when_no_vetted_address() {
21554 let url = Url::parse("https://attacker.example/").expect("url");
21555 let error = issue_outbound_http_request(&url, &options(), &empty_headers(), &[])
21556 .expect_err("empty pinned set must be refused");
21557 let message = error.to_string();
21558 assert!(
21559 message.contains("EACCES") || message.contains("ERR_HTTP_REQUEST_FAILED"),
21560 "expected an egress refusal, got: {message}"
21561 );
21562 }
21563}